| 1 | package release |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "encoding/binary" |
| 6 | "errors" |
| 7 | "sort" |
| 8 | ) |
| 9 | |
| 10 | // ErrELFBounds is every way CheckELFBounds refuses an artefact. |
| 11 | var ErrELFBounds = errors.New("release: artefact is not a bounded linux-arm64 ELF") |
| 12 | |
| 13 | // ELF limits for CheckELFBounds. A Go linux-arm64 binary has a few dozen |
| 14 | // sections and about ten program headers; these leave ample room and bound |
| 15 | // the metadata any later parser can be made to walk. |
| 16 | const ( |
| 17 | elfMaxSections = 256 |
| 18 | elfMaxPrograms = 64 |
| 19 | elfHeaderSize = 64 |
| 20 | elfPhentSize = 56 |
| 21 | elfShentSize = 64 |
| 22 | elfMachAArch64 = 183 |
| 23 | elfStrTab = 3 // SHT_STRTAB |
| 24 | elfNoBits = 8 // SHT_NOBITS: no file contents |
| 25 | elfMaxNameTab = 64 << 10 |
| 26 | elfMaxName = 256 |
| 27 | elfCompressed = 0x800 // SHF_COMPRESSED |
| 28 | elfXIndex = 0xffff |
| 29 | ) |
| 30 | |
| 31 | // CheckELFBounds is the bounded pre-check run before debug/buildinfo parses |
| 32 | // an artefact in a root process (controller ruling F1). It reads only the |
| 33 | // fixed-size headers, by offset, with every count and offset checked |
| 34 | // against len(b), and accepts only: |
| 35 | // - ELF64, little-endian, version 1, EM_AARCH64; |
| 36 | // - a program header table of at most 64 entries of 56 bytes, and a |
| 37 | // section header table of 1 to 256 entries of 64 bytes, each inside the |
| 38 | // file, not overlapping each other or the ELF header, with no extended |
| 39 | // numbering (e_shnum 0, e_shstrndx 0xffff); |
| 40 | // - every segment's file range inside the file; |
| 41 | // - every section's file range inside the file, no section flagged |
| 42 | // SHF_COMPRESSED (whose decompression is unbounded by the file size), |
| 43 | // and no two sections with contents overlapping; |
| 44 | // - a section-name table (e_shstrndx) of type SHT_STRTAB, 1 byte to |
| 45 | // 64 KiB, every section's name offset inside it, and every name at |
| 46 | // most 256 bytes up to its NUL. |
| 47 | // |
| 48 | // debug/elf copies each section's name out of the name table, so names |
| 49 | // that all point at one huge string would multiply it by the section count |
| 50 | // (Astra's re-review, controller ruling F1). With the caps, the names it |
| 51 | // copies are at most 256 x 256 bytes, no section is decompressed, and every |
| 52 | // range lies inside the bounded file. No further parser hardening is in |
| 53 | // scope for this branch. |
| 54 | func CheckELFBounds(b []byte) error { |
| 55 | n := uint64(len(b)) |
| 56 | if n < elfHeaderSize || string(b[:4]) != "\x7fELF" || b[4] != 2 || b[5] != 1 || b[6] != 1 { |
| 57 | return ErrELFBounds |
| 58 | } |
| 59 | le := binary.LittleEndian |
| 60 | if le.Uint16(b[18:]) != elfMachAArch64 || le.Uint32(b[20:]) != 1 || le.Uint16(b[52:]) != elfHeaderSize { |
| 61 | return ErrELFBounds |
| 62 | } |
| 63 | phoff, shoff := le.Uint64(b[32:]), le.Uint64(b[40:]) |
| 64 | phentsize, phnum := uint64(le.Uint16(b[54:])), uint64(le.Uint16(b[56:])) |
| 65 | shentsize, shnum := uint64(le.Uint16(b[58:])), uint64(le.Uint16(b[60:])) |
| 66 | shstrndx := uint64(le.Uint16(b[62:])) |
| 67 | if phnum > elfMaxPrograms || phnum > 0 && phentsize != elfPhentSize || shnum == 0 || shnum > elfMaxSections || shentsize != elfShentSize || shstrndx == elfXIndex || shstrndx >= shnum { |
| 68 | return ErrELFBounds |
| 69 | } |
| 70 | within := func(off, size uint64) bool { return off <= n && size <= n-off } |
| 71 | phEnd, shEnd := phoff+phnum*elfPhentSize, shoff+shnum*elfShentSize |
| 72 | if phnum > 0 && (!within(phoff, phnum*elfPhentSize) || phoff < elfHeaderSize) { |
| 73 | return ErrELFBounds |
| 74 | } |
| 75 | if !within(shoff, shnum*elfShentSize) || shoff < elfHeaderSize || phnum > 0 && shoff < phEnd && phoff < shEnd { |
| 76 | return ErrELFBounds |
| 77 | } |
| 78 | for i := uint64(0); i < phnum; i++ { |
| 79 | h := b[phoff+i*elfPhentSize:] |
| 80 | if !within(le.Uint64(h[8:]), le.Uint64(h[32:])) { // p_offset, p_filesz |
| 81 | return ErrELFBounds |
| 82 | } |
| 83 | } |
| 84 | type span struct{ off, end uint64 } |
| 85 | var spans []span |
| 86 | for i := uint64(0); i < shnum; i++ { |
| 87 | h := b[shoff+i*elfShentSize:] |
| 88 | typ, flags, off, size := le.Uint32(h[4:]), le.Uint64(h[8:]), le.Uint64(h[24:]), le.Uint64(h[32:]) |
| 89 | if flags&elfCompressed != 0 { |
| 90 | return ErrELFBounds |
| 91 | } |
| 92 | if typ == elfNoBits || size == 0 { |
| 93 | continue |
| 94 | } |
| 95 | if !within(off, size) { |
| 96 | return ErrELFBounds |
| 97 | } |
| 98 | spans = append(spans, span{off, off + size}) |
| 99 | } |
| 100 | names := b[shoff+shstrndx*elfShentSize:] |
| 101 | if le.Uint32(names[4:]) != elfStrTab { |
| 102 | return ErrELFBounds |
| 103 | } |
| 104 | nameOff, nameSize := le.Uint64(names[24:]), le.Uint64(names[32:]) |
| 105 | if nameSize == 0 || nameSize > elfMaxNameTab || !within(nameOff, nameSize) { |
| 106 | return ErrELFBounds |
| 107 | } |
| 108 | table := b[nameOff : nameOff+nameSize] |
| 109 | for i := uint64(0); i < shnum; i++ { |
| 110 | at := uint64(le.Uint32(b[shoff+i*elfShentSize:])) |
| 111 | if at >= nameSize { |
| 112 | return ErrELFBounds |
| 113 | } |
| 114 | name := table[at:] |
| 115 | if len(name) > elfMaxName+1 { |
| 116 | name = name[:elfMaxName+1] |
| 117 | } |
| 118 | if bytes.IndexByte(name, 0) < 0 { |
| 119 | return ErrELFBounds |
| 120 | } |
| 121 | } |
| 122 | sort.Slice(spans, func(i, j int) bool { return spans[i].off < spans[j].off }) |
| 123 | for i := 1; i < len(spans); i++ { |
| 124 | if spans[i].off < spans[i-1].end { |
| 125 | return ErrELFBounds |
| 126 | } |
| 127 | } |
| 128 | return nil |
| 129 | } |
| 130 | |