package kconfig import ( "bufio" "bytes" "compress/gzip" "fmt" "io" "math" "os" "strconv" "strings" "github.com/cilium/ebpf/btf" "github.com/cilium/ebpf/internal" ) // Find find a kconfig file on the host. // It first reads from /boot/config- of the current running kernel and tries // /proc/config.gz if nothing was found in /boot. // If none of the file provide a kconfig, it returns an error. func Find() (*os.File, error) { kernelRelease, err := internal.KernelRelease() if err != nil { return nil, fmt.Errorf("cannot get kernel release: %w", err) } path := "/boot/config-" + kernelRelease f, err := os.Open(path) if err == nil { return f, nil } f, err = os.Open("/proc/config.gz") if err == nil { return f, nil } return nil, fmt.Errorf("neither %s nor /proc/config.gz provide a kconfig", path) } // Parse parses the kconfig file for which a reader is given. // All the CONFIG_* which are in filter and which are set set will be // put in the returned map as key with their corresponding value as map value. // If filter is nil, no filtering will occur. // If the kconfig file is not valid, error will be returned. func Parse(source io.ReaderAt, filter map[string]struct{}) (map[string]string, error) { var r io.Reader zr, err := gzip.NewReader(io.NewSectionReader(source, 0, math.MaxInt64)) if err != nil { r = io.NewSectionReader(source, 0, math.MaxInt64) } else { // Source is gzip compressed, transparently decompress. r = zr } ret := make(map[string]string, len(filter)) s := bufio.NewScanner(r) for s.Scan() { line := s.Bytes() err = processKconfigLine(line, ret, filter) if err != nil { return nil, fmt.Errorf("cannot parse line: %w", err) } if filter != nil && len(ret) == len(filter) { break } } if err := s.Err(); err != nil { return nil, fmt.Errorf("cannot parse: %w", err) } if zr != nil { return ret, zr.Close() } return ret, nil } // Golang translation of libbpf bpf_object__process_kconfig_line(): // https://github.com/libbpf/libbpf/blob/fbd60dbff51c870f5e80a17c4f2fd639eb80af90/src/libbpf.c#L1874 // It does the same checks but does not put the data inside the BPF map. func processKconfigLine(line []byte, m map[string]string, filter map[string]struct{}) error { // Ignore empty lines and "# CONFIG_* is not set". if !bytes.HasPrefix(line, []byte("CONFIG_")) { return nil } key, value, found := bytes.Cut(line, []byte{'='}) if !found { return fmt.Errorf("line %q does not contain separator '='", line) } if len(value) == 0 { return fmt.Errorf("line %q has no value", line) } if filter != nil { // NB: map[string(key)] gets special optimisation help from the compiler // and doesn't allocate. Don't turn this into a variable. _, ok := filter[string(key)] if !ok { return nil } } // This can seem odd, but libbpf only sets the value the first time the key is // met: // https://github.com/torvalds/linux/blob/0d85b27b0cc6/tools/lib/bpf/libbpf.c#L1906-L1908 _, ok := m[string(key)] if !ok { m[string(key)] = string(value) } return nil } // PutValue translates the value given as parameter depending on the BTF // type, the translated value is then written to the byte array. func PutValue(data []byte, typ btf.Type, value string) error { typ = btf.UnderlyingType(typ) switch value { case "y", "n", "m": return putValueTri(data, typ, value) default: if strings.HasPrefix(value, `"`) { return putValueString(data, typ, value) } return putValueNumber(data, typ, value) } } // Golang translation of libbpf_tristate enum: // https://github.com/libbpf/libbpf/blob/fbd60dbff51c870f5e80a17c4f2fd639eb80af90/src/bpf_helpers.h#L169 type triState int const ( TriNo triState = 0 TriYes triState = 1 TriModule triState = 2 ) func putValueTri(data []byte, typ btf.Type, value string) error { switch v := typ.(type) { case *btf.Int: if v.Encoding != btf.Bool { return fmt.Errorf("cannot add tri value, expected btf.Bool, got: %v", v.Encoding) } if v.Size != 1 { return fmt.Errorf("cannot add tri value, expected size of 1 byte, got: %d", v.Size) } switch value { case "y": data[0] = 1 case "n": data[0] = 0 default: return fmt.Errorf("cannot use %q for btf.Bool", value) } case *btf.Enum: if v.Name != "libbpf_tristate" { return fmt.Errorf("cannot use enum %q, only libbpf_tristate is supported", v.Name) } var tri triState switch value { case "y": tri = TriYes case "m": tri = TriModule case "n": tri = TriNo default: return fmt.Errorf("value %q is not support for libbpf_tristate", value) } internal.NativeEndian.PutUint64(data, uint64(tri)) default: return fmt.Errorf("cannot add number value, expected btf.Int or btf.Enum, got: %T", v) } return nil } func putValueString(data []byte, typ btf.Type, value string) error { array, ok := typ.(*btf.Array) if !ok { return fmt.Errorf("cannot add string value, expected btf.Array, got %T", array) } contentType, ok := btf.UnderlyingType(array.Type).(*btf.Int) if !ok { return fmt.Errorf("cannot add string value, expected array of btf.Int, got %T", contentType) } // Any Int, which is not bool, of one byte could be used to store char: // https://github.com/torvalds/linux/blob/1a5304fecee5/tools/lib/bpf/libbpf.c#L3637-L3638 if contentType.Size != 1 && contentType.Encoding != btf.Bool { return fmt.Errorf("cannot add string value, expected array of btf.Int of size 1, got array of btf.Int of size: %v", contentType.Size) } if !strings.HasPrefix(value, `"`) || !strings.HasSuffix(value, `"`) { return fmt.Errorf(`value %q must start and finish with '"'`, value) } str := strings.Trim(value, `"`) // We need to trim string if the bpf array is smaller. if uint32(len(str)) >= array.Nelems { str = str[:array.Nelems] } // Write the string content to .kconfig. copy(data, str) return nil } func putValueNumber(data []byte, typ btf.Type, value string) error { integer, ok := typ.(*btf.Int) if !ok { return fmt.Errorf("cannot add number value, expected *btf.Int, got: %T", integer) } size := integer.Size sizeInBits := size * 8 var n uint64 var err error if integer.Encoding == btf.Signed { parsed, e := strconv.ParseInt(value, 0, int(sizeInBits)) n = uint64(parsed) err = e } else { parsed, e := strconv.ParseUint(value, 0, int(sizeInBits)) n = uint64(parsed) err = e } if err != nil { return fmt.Errorf("cannot parse value: %w", err) } switch size { case 1: data[0] = byte(n) case 2: internal.NativeEndian.PutUint16(data, uint16(n)) case 4: internal.NativeEndian.PutUint32(data, uint32(n)) case 8: internal.NativeEndian.PutUint64(data, uint64(n)) default: return fmt.Errorf("size (%d) is not valid, expected: 1, 2, 4 or 8", size) } return nil }