diff --git a/pkg/sbf/elf.go b/pkg/sbf/elf.go index 8146809..89ecff8 100644 --- a/pkg/sbf/elf.go +++ b/pkg/sbf/elf.go @@ -1,6 +1,7 @@ package sbf import ( + "bufio" "bytes" "debug/elf" "encoding/binary" @@ -8,18 +9,35 @@ import ( "io" "math" "math/bits" + "strings" ) // TODO Fuzz // TODO Differential fuzz against rbpf type Executable struct { - Header elf.Header64 - Load elf.Prog64 - ShStr elf.Section64 + Header elf.Header64 + Load elf.Prog64 + ShShstrtab elf.Section64 + ShSymtab *elf.Section64 + ShStrtab *elf.Section64 + ShDynstr *elf.Section64 } +// Bounds checks +const ( + // 64 MiB max program size. + // Allows loader to use unchecked math when adding 32-bit offsets. + maxFileLen = 1 << 26 + + maxSectionNameLen = 16 + maxSymbolNameLen = 1024 +) + func LoadProgram(buf []byte) (*Executable, error) { + if len(buf) > maxFileLen { + return nil, fmt.Errorf("ELF file too large") + } l := loader{ rd: bytes.NewReader(buf), fileSize: uint64(len(buf)), @@ -27,6 +45,8 @@ func LoadProgram(buf []byte) (*Executable, error) { return l.load() } +const EF_SBF_V2 = 0x20 + type loader struct { rd io.ReaderAt fileSize uint64 @@ -44,11 +64,12 @@ func (l *loader) load() (*Executable, error) { if err := l.loadProgramHeaderTable(); err != nil { return nil, err } - if err := l.validateSectionHeaderTable(); err != nil { + if err := l.readSectionHeaderTable(); err != nil { + return nil, err + } + if err := l.parseSections(); err != nil { return nil, err } - // TODO load section name section header - // TODO parse sections // TODO parse dynamic segment return l.elf, nil } @@ -59,6 +80,16 @@ const ( shentsize = 0x40 ) +func (l *loader) newPhTableIter() *tableIter[elf.Prog64] { + eh := &l.elf.Header + return newTableIterator[elf.Prog64](l, eh.Phoff, eh.Phnum, phentsize) +} + +func (l *loader) newShTableIter() *tableIter[elf.Section64] { + eh := &l.elf.Header + return newTableIterator[elf.Section64](l, eh.Shoff, eh.Shnum, shentsize) +} + func (l *loader) readHeader() error { var hdrBuf [ehsize]byte if _, err := io.ReadFull(io.NewSectionReader(l.rd, 0, ehsize), hdrBuf[:]); err != nil { @@ -78,14 +109,14 @@ func (l *loader) validateHeader() error { if elf.Class(ident[elf.EI_CLASS]) != elf.ELFCLASS64 || elf.Data(ident[elf.EI_DATA]) != elf.ELFDATA2LSB || elf.Version(ident[elf.EI_VERSION]) != elf.EV_CURRENT || - elf.OSABI(ident[elf.EI_OSABI]) != elf.ELFOSABI_NONE { + elf.OSABI(ident[elf.EI_OSABI]) != elf.ELFOSABI_NONE || + elf.Machine(eh.Machine) != elf.EM_BPF || + elf.Type(eh.Type) != elf.ET_DYN { return fmt.Errorf("incompatible binary") } // note: EI_PAD and EI_ABIVERSION are ignored - if elf.Machine(eh.Machine) != elf.EM_BPF || - elf.Type(eh.Type) != elf.ET_DYN || - eh.Version != uint32(elf.EV_CURRENT) || + if eh.Version != uint32(elf.EV_CURRENT) || eh.Ehsize != ehsize || eh.Phentsize != phentsize || eh.Shentsize != shentsize || @@ -108,20 +139,9 @@ func (l *loader) validateHeader() error { // scan the program header table and remember the last PT_LOAD segment func (l *loader) loadProgramHeaderTable() error { - eh := &l.elf.Header - phoff := eh.Phoff - - for i := uint16(0); i < eh.Phnum; i++ { - if phoff+phentsize > math.MaxInt64 { - return io.ErrUnexpectedEOF - } - rd := io.NewSectionReader(l.rd, int64(phoff), phentsize) - - var ph elf.Prog64 - if err := binary.Read(rd, binary.LittleEndian, &ph); err != nil { - return err - } - phoff += phentsize + iter := l.newPhTableIter() + for iter.Next() && iter.Err() == nil { + ph := iter.Item() if elf.ProgType(ph.Type) != elf.PT_LOAD { continue @@ -139,37 +159,30 @@ func (l *loader) loadProgramHeaderTable() error { l.elf.Load = ph } - - return nil + return iter.Err() } -func (l *loader) validateSectionHeaderTable() error { +// reads and validates the section header table. +// remembers the section header table. +func (l *loader) readSectionHeaderTable() error { eh := &l.elf.Header - shoff := eh.Shoff + iter := l.newShTableIter() + sectionDataOff := uint64(0) - offset := uint64(0) - for i := uint16(0); i < eh.Shnum; i++ { - if shoff+shentsize > math.MaxInt64 { - return io.ErrUnexpectedEOF - } - rd := io.NewSectionReader(l.rd, int64(shoff), shentsize) + if !iter.Next() { + return fmt.Errorf("missing section 0") + } + if elf.SectionType(iter.Item().Type) != elf.SHT_NULL { + return fmt.Errorf("section 0 is not SHT_NULL") + } - var sh elf.Section64 - if err := binary.Read(rd, binary.LittleEndian, &sh); err != nil { - return err - } - shoff += shentsize - - if i == 0 { - if elf.SectionType(sh.Type) != elf.SHT_NULL { - return fmt.Errorf("section 0 is not SHT_NULL") - } - continue - } + for iter.Next() && iter.Err() == nil { + i, sh := iter.Index(), iter.Item() if elf.SectionType(sh.Type) == elf.SHT_NOBITS { continue } + // Ensure section data is not overlapping with ELF headers shend, overflow := bits.Add64(sh.Off, sh.Size, 0) if overflow != 0 { return fmt.Errorf("integer overflow in section %d", i) @@ -183,16 +196,154 @@ func (l *loader) validateSectionHeaderTable() error { if isOverlap(eh.Shoff, uint64(eh.Shnum)*shentsize, sh.Off, sh.Size) { return fmt.Errorf("section %d overlaps with section header", i) } - if eh.Shoff < offset { + + // More checks + if eh.Shoff < sectionDataOff { return fmt.Errorf("sections not in order") } if shend > l.fileSize { return fmt.Errorf("section %d out of bounds", i) } - offset = shend + + // Remember section header string table. + if eh.Shstrndx != uint16(elf.SHN_UNDEF) && eh.Shstrndx == i { + l.elf.ShShstrtab = sh + } + + sectionDataOff = shend + } + // TODO validate offset and size (?) + if elf.SectionType(l.elf.ShShstrtab.Type) != elf.SHT_STRTAB { + return fmt.Errorf("invalid .shstrtab") + } + return iter.Err() +} + +func (l *loader) getString(strtab *elf.Section64, stroff uint32, maxLen uint16) (string, error) { + if elf.SectionType(strtab.Type) != elf.SHT_STRTAB { + return "", fmt.Errorf("invalid strtab") + } + offset := strtab.Off + uint64(stroff) + if offset > l.fileSize || offset+uint64(maxLen) > l.fileSize { + return "", io.ErrUnexpectedEOF + } + rd := bufio.NewReader(io.NewSectionReader(l.rd, int64(offset), int64(maxLen))) + var builder strings.Builder + for { + b, err := rd.ReadByte() + if err != nil { + return "", err + } + if b == 0 { + break + } + builder.WriteByte(b) + } + return builder.String(), nil +} + +// Iterate sections and remember special sections by name. +func (l *loader) parseSections() error { + shShstrtab := &l.elf.ShShstrtab + iter := l.newShTableIter() + for iter.Next() && iter.Err() == nil { + sh := iter.Item() + sectionName, err := l.getString(shShstrtab, sh.Name, maxSectionNameLen) + if err != nil { + return fmt.Errorf("getString: %w", err) + } + + // Remember special section or error if it already exists. + setSection := func(shPtr **elf.Section64) error { + if *shPtr != nil { + return fmt.Errorf("duplicate section: %s", sectionName) + } + *shPtr = new(elf.Section64) + **shPtr = sh + return nil + } + switch sectionName { + case ".symtab": + err = setSection(&l.elf.ShSymtab) + case ".strtab": + err = setSection(&l.elf.ShStrtab) + case ".dynstr": + err = setSection(&l.elf.ShDynstr) + } + if err != nil { + return err + } + } + return iter.Err() +} + +// tableIter is a memory-efficient iterator over densely packed tables of statically sized items. +// Such as the ELF program header and section header tables. +type tableIter[T any] struct { + l *loader + off uint64 + i uint16 // one ahead + count uint16 + elemSize uint16 + elem T + err error +} + +// newTableIterator creates a new tableIter at `off` for `count` elements of `elemSize` len. +func newTableIterator[T any](l *loader, off uint64, count uint16, elemSize uint16) *tableIter[T] { + return &tableIter[T]{ + l: l, + off: off, + count: count, + elemSize: elemSize, + } +} + +// Next reads one element. +// +// Returns true on success, false if table end has been reached or error occurred. +// The caller should abort iteration on error. +func (it *tableIter[T]) Next() (ok bool) { + ok, it.err = it.getNext() + if ok && it.err != nil { + panic("unreachable") + } + return +} + +// Index returns the current table index. +func (it *tableIter[T]) Index() uint16 { + return it.i - 1 +} + +// Err returns the current error. +func (it *tableIter[T]) Err() error { + return it.err +} + +// Item returns the current element read. +// +// Next must be called before. +func (it *tableIter[T]) Item() T { + return it.elem +} + +func (it *tableIter[T]) getNext() (bool, error) { + if it.i >= it.count { + return false, nil + } + if it.off >= math.MaxInt64 || it.off+uint64(it.elemSize) > math.MaxInt64 { + return false, io.ErrUnexpectedEOF } - return nil + rd := io.NewSectionReader(it.l.rd, int64(it.off), int64(it.elemSize)) + if err := binary.Read(rd, binary.LittleEndian, &it.elem); err != nil { + return false, err + } + + it.off += uint64(it.elemSize) + it.i++ + return true, nil } func isOverlap(startA uint64, sizeA uint64, startB uint64, sizeB uint64) bool { diff --git a/pkg/sbf/elf_test.go b/pkg/sbf/elf_test.go index 3e78139..3b8da7a 100644 --- a/pkg/sbf/elf_test.go +++ b/pkg/sbf/elf_test.go @@ -48,6 +48,50 @@ func TestLoadProgram_Noop(t *testing.T) { Memsz: 208, Align: 4096, }, - ShStr: elf.Section64{}, + ShShstrtab: elf.Section64{ + Name: 82, + Type: uint32(elf.SHT_STRTAB), + Flags: 0, + Addr: 0, + Off: 8648, + Size: 100, + Addralign: 1, + }, + ShSymtab: &elf.Section64{ + Name: 74, + Type: uint32(elf.SHT_SYMTAB), + Flags: 0, + Addr: 0, + Off: 8504, + Size: 144, + Link: 12, + Info: 3, + Addralign: 8, + Entsize: 24, + }, + ShStrtab: &elf.Section64{ + Name: 92, + Type: uint32(elf.SHT_STRTAB), + Flags: 0, + Addr: 0, + Off: 8748, + Size: 39, + Link: 0, + Info: 0, + Addralign: 1, + Entsize: 0, + }, + ShDynstr: &elf.Section64{ + Name: 25, + Type: uint32(elf.SHT_STRTAB), + Flags: uint64(elf.DF_SYMBOLIC), + Addr: 624, + Off: 624, + Size: 23, + Link: 0, + Info: 0, + Addralign: 1, + Entsize: 0, + }, }, exe) }