Revision bffc6cdc5fe46c1c52a82d447111a63123e3be63 authored by Martynas Pumputis on 07 November 2023, 12:47:40 UTC, committed by Martynas Pumputis on 07 November 2023, 12:47:40 UTC
Signed-off-by: Martynas Pumputis <m@lambda.lt>
1 parent 518d7cc
Raw File
elf.go
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of Cilium

package elf

import (
	"debug/elf"
	"encoding/binary"
	"fmt"
	"io"
	"os"
	"strings"
	"unsafe"

	"github.com/sirupsen/logrus"

	"github.com/cilium/cilium/pkg/lock"
)

var (
	ignoredPrefixes []string
)

// ELF is an in-memory representation of a BPF ELF object from the filesystem.
type ELF struct {
	metadata *elf.File
	file     *os.File
	symbols  symbols
	log      *logrus.Entry

	// lock concurrent writes of the ELF. This library probably isn't far
	// off from allowing concurrent Write() execution, but it's just not
	// supported for now.
	lock.Mutex
}

// newReader creates a new reader that can safely seek the input in parallel.
func newReader(ra io.ReaderAt) *io.SectionReader {
	// If 1<<63-1 is good enough for pkg/debug/elf, it's good enough for us.
	return io.NewSectionReader(ra, 0, 1<<63-1)
}

// NewELF returns a new object from the specified reader.
//
// The ELF binary is expected to start at position 0 in the specified reader.
func NewELF(ra io.ReaderAt, scopedLog *logrus.Entry) (*ELF, error) {
	ef, err := elf.NewFile(ra)
	if err != nil {
		return nil, fmt.Errorf("unable to open ELF: %s", err)
	}

	// EM_NONE is generated by older Clang (eg 3.8.x), which we currently
	// use in Travis. We should be able to drop that part pretty soon.
	if ef.Machine != elf.EM_NONE && ef.Machine != elf.EM_BPF {
		return nil, fmt.Errorf("unsupported ELF machine type %s", ef.Machine)
	}

	result := &ELF{
		metadata: ef,
		log:      scopedLog,
	}
	if err := result.symbols.extractFrom(ef); err != nil {
		return nil, fmt.Errorf("unable to read ELF symbols: %s", err)
	}

	return result, nil
}

// Open an ELF file from the specified path.
func Open(path string) (*ELF, error) {
	scopedLog := log.WithField(srcPath, path)

	f, err := os.Open(path)
	if err != nil {
		return nil, &os.PathError{
			Op:   "failed to open ELF file",
			Path: path,
			Err:  err,
		}
	}

	result, err := NewELF(f, scopedLog)
	if err != nil {
		if err2 := f.Close(); err2 != nil {
			scopedLog.WithError(err).Warning("Failed to close ELF")
		}
		return nil, &os.PathError{
			Op:   "failed to parse ELF file",
			Path: path,
			Err:  err,
		}
	}
	result.file = f
	return result, nil
}

// Close closes the ELF. If the File was created using NewELF directly instead
// of Open, Close has no effect.
func (elf *ELF) Close() (err error) {
	if elf.file != nil {
		err = elf.file.Close()
	}
	return err
}

func (elf *ELF) writeValue(w io.WriteSeeker, offset uint64, value []byte) error {
	if _, err := w.Seek(int64(offset), io.SeekStart); err != nil {
		return err
	}
	return binary.Write(w, elf.metadata.ByteOrder, value)
}

// copy the ELF from the reader to the writer, substituting the constants with
// names specified in 'intOptions' with their corresponding values, and the
// strings specified in 'strOptions' with their corresponding values.
//
// Keys in the 'intOptions' / 'strOptions' maps are case-sensitive.
func (elf *ELF) copy(w io.WriteSeeker, r *io.SectionReader, intOptions map[string]uint64, strOptions map[string]string) error {
	if len(intOptions) == 0 && len(strOptions) == 0 {
		// Copy the remaining portion of the file
		if _, err := io.Copy(w, r); err != nil {
			return err
		}
		return nil
	}

	// Copy the ELF's contents, we overwrite at specific offsets later.
	if _, err := io.Copy(w, r); err != nil {
		return err
	}

	processedOptions := make(map[string]struct{}, len(intOptions)+len(strOptions))

processSymbols:
	for _, symbol := range elf.symbols.sort() {
		// Use the symbol's name without config prefix for lookups and logging.
		name := strings.TrimPrefix(symbol.name, configPrefix)

		scopedLog := log.WithField("symbol", name)

		// Figure out the value to substitute
		var value []byte
		switch symbol.kind {
		case symbolData:
			// intOptions doesn't contain symbols with the config prefix, but symbols
			// found in the ELF do.
			if v, exists := intOptions[name]; exists {
				value = make([]byte, symbol.size)
				switch uintptr(symbol.size) {
				case unsafe.Sizeof(uint64(0)):
					elf.metadata.ByteOrder.PutUint64(value, v)
				case unsafe.Sizeof(uint32(0)):
					elf.metadata.ByteOrder.PutUint32(value, uint32(v))
				case unsafe.Sizeof(uint16(0)):
					elf.metadata.ByteOrder.PutUint16(value, uint16(v))
				default:
					return fmt.Errorf("substitute symbol %s: unsupported size %d", name, symbol.size)
				}
			}

		case symbolString:
			v, exists := strOptions[name]
			if exists {
				if uint64(len(v)) != symbol.size {
					return fmt.Errorf("symbol substitution value %q (len %d) must equal length of symbol name %q (len %d)", v, len(v), name, symbol.size)
				}
				value = []byte(v)
			}
		}

		if value == nil {
			for _, prefix := range ignoredPrefixes {
				if strings.HasPrefix(name, prefix) {
					continue processSymbols
				}
			}
			scopedLog.Warning("Skipping symbol substitution")
			continue processSymbols
		}

		// Encode the value at the given offset in the destination file.
		if err := elf.writeValue(w, symbol.offset, value); err != nil {
			return fmt.Errorf("failed to substitute %s: %s", name, err)
		}

		if symbol.offsetBTF != 0 {
			if err := elf.writeValue(w, symbol.offsetBTF, value); err != nil {
				return fmt.Errorf("failed to substitute %s BTF: %s", name, err)
			}
		}

		processedOptions[name] = struct{}{}
	}

	// Check for additional options that weren't applied
	for symbol := range strOptions {
		if _, processed := processedOptions[symbol]; !processed {
			return fmt.Errorf("no such string %q in ELF", symbol)
		}
	}
	for symbol := range intOptions {
		if _, processed := processedOptions[symbol]; !processed {
			return fmt.Errorf("no such symbol %q in ELF", symbol)
		}
	}

	return nil
}

// Write the received ELF to a new file at the specified location, with the
// specified options (indexed by name) substituted:
//   - intOptions: 32-bit values substituted in the data section.
//   - strOptions: strings susbtituted in the string table. For each key/value
//     pair, both key and value must be same length.
//
// Only one goroutine may Write() the same *ELF concurrently.
//
// On success, writes the new file to the specified path.
// On failure, returns an error and no file is left on the filesystem.
func (elf *ELF) Write(path string, intOptions map[string]uint64, strOptions map[string]string) error {
	elf.Lock()
	defer elf.Unlock()

	scopedLog := elf.log.WithField(dstPath, path)

	f, err := os.Create(path)
	if err != nil {
		return &os.PathError{
			Op:   "failed to create ELF file",
			Path: path,
			Err:  err,
		}
	}

	defer func() {
		if err2 := f.Close(); err2 != nil {
			scopedLog.WithError(err).Warning("Failed to close new ELF")
		}
		if err != nil {
			if err2 := os.RemoveAll(path); err2 != nil {
				scopedLog.WithError(err).Warning("Failed to clean up new ELF path on error")
			}
		}
	}()

	reader := newReader(elf.file)
	if err = elf.copy(f, reader, intOptions, strOptions); err != nil {
		return &os.PathError{
			Op:   "failed to write ELF file:",
			Path: path,
			Err:  err,
		}
	}
	if err = f.Sync(); err != nil {
		return &os.PathError{
			Op:   "failed to sync ELF file:",
			Path: path,
			Err:  err,
		}
	}

	scopedLog.WithError(err).Debugf("Finished writing ELF")
	return nil
}

// IgnoreSymbolPrefixes configures the ELF package to ignore symbols that have
// any of the specified prefixes. It must be called by only one thread at a time.
//
// This slice will be iterated once per ELF.Write(), so try not to let it grow
// out of hand...
func IgnoreSymbolPrefixes(prefixes []string) {
	ignoredPrefixes = prefixes
}
back to top