sbf: add ELF section parser
This commit is contained in:
parent
cc798726bc
commit
fade5aa0b8
249
pkg/sbf/elf.go
249
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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue