diff --git a/helpers/kernel_features.go b/helpers/kernel_features.go index 10acca55..8e4e7d88 100644 --- a/helpers/kernel_features.go +++ b/helpers/kernel_features.go @@ -2,6 +2,7 @@ package helpers import ( "bufio" + "bytes" "compress/gzip" "fmt" "io" @@ -178,64 +179,77 @@ type KernelConfig struct { } // InitKernelConfig inits external KernelConfig object -func InitKernelConfig() *KernelConfig { - config := KernelConfig{ - configs: make(map[KernelConfigOption]interface{}), - needed: make(map[KernelConfigOption]interface{}), +func InitKernelConfig() (*KernelConfig, error) { + x := unix.Utsname{} + if err := unix.Uname(&x); err != nil { + return nil, fmt.Errorf("could not get utsname") } - if err := config.initKernelConfig(); err != nil { - return nil + + releaseVersion := bytes.TrimRight(x.Release[:], "\x00") + releaseFilePath := fmt.Sprintf("/boot/config-%s", releaseVersion) + + config := KernelConfig{} + + if err1 := config.initKernelConfig(releaseFilePath); err1 != nil { + if err2 := config.initKernelConfig("/proc/config.gz"); err2 != nil { + return nil, err2 + } + + return nil, err1 } - return &config + return &config, nil } // initKernelConfig inits internal KernelConfig data by calling appropriate readConfigFromXXX function -func (k *KernelConfig) initKernelConfig() error { - k.configs = make(map[KernelConfigOption]interface{}) +func (k *KernelConfig) initKernelConfig(configFilePath string) error { + var err error + var file *os.File - x := unix.Utsname{} - if err := unix.Uname(&x); err != nil { - return fmt.Errorf("could not determine uname release: %w", err) + if file, err = os.Open(configFilePath); err != nil { + return fmt.Errorf("could not open %v: %w", configFilePath, err) } + defer file.Close() - if err := k.readConfigFromBootConfigRelease(string(x.Release[:])); err != nil { - if err2 := k.readConfigFromProcConfigGZ(); err != nil { - return err2 - } - + head := make([]byte, 2) + if _, err = file.Read(head); err != nil { return err } - return nil -} - -// readConfigFromProcConfigGZ prepares io.Reader for readConfigFromGZScanner (/proc/config.gz) -func (k *KernelConfig) readConfigFromProcConfigGZ() error { - configFile, err := os.Open("/proc/config.gz") - if err != nil { - return fmt.Errorf("could not open /proc/config.gz: %w", err) + // check if its a gziped file + if head[0] == 0x1f && head[1] == 0x8b { + return k.readConfigFromProcConfigGZ(configFilePath) } - return k.readConfigFromGZScanner(configFile) + // assume it is a text file + return k.readConfigFromBootConfigRelease(configFilePath) } -// readConfigFromScanner prepares io.Reader for readConfigFromScanner (/boot/config-$(uname -r)) -func (k *KernelConfig) readConfigFromBootConfigRelease(release string) error { - path := fmt.Sprintf("/boot/config-%s", strings.TrimRight(release, "\x00")) +// readConfigFromBootConfigRelease prepares io.Reader (/boot/config-$(uname -r)) for readConfigFromScanner +func (k *KernelConfig) readConfigFromBootConfigRelease(filePath string) error { + file, _ := os.Open(filePath) // already checked + k.readConfigFromScanner(file) + file.Close() - configFile, err := os.Open(path) - if err != nil { - return fmt.Errorf("could not open %s: %w", path, err) - } + return nil +} - k.readConfigFromScanner(configFile) +// readConfigFromProcConfigGZ prepares gziped io.Reader (/proc/config.gz) for readConfigFromScanner +func (k *KernelConfig) readConfigFromProcConfigGZ(filePath string) error { + file, _ := os.Open(filePath) // already checked + zreader, _ := gzip.NewReader(file) + k.readConfigFromScanner(zreader) + zreader.Close() + file.Close() return nil } // readConfigFromScanner reads all existing KernelConfigOption's and KernelConfigOptionValue's from given io.Reader func (k *KernelConfig) readConfigFromScanner(reader io.Reader) { + k.configs = make(map[KernelConfigOption]interface{}) + k.needed = make(map[KernelConfigOption]interface{}) + scanner := bufio.NewScanner(reader) for scanner.Scan() { kv := strings.Split(scanner.Text(), "=") @@ -257,18 +271,6 @@ func (k *KernelConfig) readConfigFromScanner(reader io.Reader) { } } -// readConfigFromGZScanner reads all existing KernelConfigOption's and KernelConfigOptionValue's from a gzip io.Reader -func (k *KernelConfig) readConfigFromGZScanner(reader io.Reader) error { - zreader, err := gzip.NewReader(reader) - if err != nil { - return err - } - - k.readConfigFromScanner(zreader) - - return nil -} - // GetValue will return a KernelConfigOptionValue for a given KernelConfigOption when this is a BUILTIN or a MODULE func (k *KernelConfig) GetValue(option KernelConfigOption) (KernelConfigOptionValue, error) { value, ok := k.configs[option].(KernelConfigOptionValue) @@ -276,7 +278,7 @@ func (k *KernelConfig) GetValue(option KernelConfigOption) (KernelConfigOptionVa return value, nil } - return UNDEFINED, fmt.Errorf("given option's value (%s) is a string", option) + return UNDEFINED, fmt.Errorf("given option's value (%s) is undefined", option) } // GetValueString will return a KernelConfigOptionValue for a given KernelConfigOption when this is actually a string @@ -308,8 +310,8 @@ func (k *KernelConfig) Exists(option KernelConfigOption) bool { // ExistsValue will return true if a given KernelConfigOption was found in provided KernelConfig // AND its value is the same as the one provided by KernelConfigOptionValue func (k *KernelConfig) ExistsValue(option KernelConfigOption, value interface{}) bool { - if _, ok := k.configs[option]; ok { - switch k.configs[option].(type) { + if cfg, ok := k.configs[option]; ok { + switch cfg.(type) { case KernelConfigOptionValue: if value == ANY { return true @@ -348,5 +350,7 @@ func (k *KernelConfig) CheckMissing() []KernelConfigOption { // kernelConfig.AddNeeded(helpers.CONFIG_HZ, "250") // func (k *KernelConfig) AddNeeded(option KernelConfigOption, value interface{}) { - k.needed[option] = value + if _, ok := KernelConfigKeyIDToString[option]; ok { + k.needed[option] = value + } } diff --git a/helpers/kernel_features_test.go b/helpers/kernel_features_test.go index 678ea1aa..11f574f3 100644 --- a/helpers/kernel_features_test.go +++ b/helpers/kernel_features_test.go @@ -1,98 +1,74 @@ package helpers import ( - "errors" - "testing" - "github.com/stretchr/testify/assert" + "testing" ) -func TestGetProcGZConfigByPath(t *testing.T) { - testCases := []struct { - name string - goldenFilePath string - expectedMap KernelConfig - expectedError error - }{ - { - name: "non-existent", - goldenFilePath: "foobarblahblahblah", - expectedMap: KernelConfig{}, - expectedError: errors.New("could not open foobarblahblahblah: open foobarblahblahblah: no such file or directory"), - }, - { - name: "invalid zip format", - goldenFilePath: "testdata/tarred_config.tar", - expectedMap: KernelConfig{}, - expectedError: errors.New("gzip: invalid header"), - }, - { - name: "standard config", - goldenFilePath: "testdata/config_standard.gz", - expectedMap: KernelConfig{CONFIG_BPF: "y", CONFIG_BPF_JIT_ALWAYS_ON: "y", CONFIG_BPF_LSM: "y", CONFIG_BPF_PRELOAD: "y", CONFIG_BPF_PRELOAD_UMD: "m", CONFIG_BPF_SYSCALL: "y", CONFIG_IPV6_SEG6_BPF: "y", CONFIG_NETFILTER_XT_MATCH_BPF: "m"}, - expectedError: nil, - }, - { - name: "config with comments in it", - goldenFilePath: "testdata/comments_config.gz", - expectedMap: KernelConfig{CONFIG_BPF: "y", CONFIG_BPF_SYSCALL: "y", CONFIG_BPF_PRELOAD_UMD: "m"}, - expectedError: nil, - }, - } +func TestGetKernelConfigValue(t *testing.T) { - for _, tt := range testCases { - t.Run(tt.name, func(t *testing.T) { - var kconfig KernelConfig = make(map[uint32]string) - err := kconfig.getProcGZConfigByPath(tt.goldenFilePath) - assert.Equal(t, tt.expectedError, err) - assert.Equal(t, tt.expectedMap, kconfig) - }) - } -} + allConfigFiles := []string{"testdata/config_standard.gz", "testdata/config_comments.gz", "testdata/config_comments"} -func TestGetKernelConfigValue(t *testing.T) { testCases := []struct { - name string - key uint32 - conf KernelConfig - expectedError error - expectedValue string + testName string + givenOptions []KernelConfigOption + givenValues []interface{} // might either be KernelConfigOptionValue or String + missingOptions []KernelConfigOption // options that will be missing from given config files }{ { - name: "Value present", - key: CONFIG_BPF, - conf: KernelConfig{CONFIG_BPF: "y"}, - expectedError: nil, - expectedValue: "y", + testName: "option ok", + givenOptions: []KernelConfigOption{CONFIG_BPF}, + givenValues: []interface{}{BUILTIN}, + missingOptions: []KernelConfigOption{}, }, { - name: "Value present", - key: CONFIG_BPF, - conf: KernelConfig{CONFIG_BPFILTER: "foo", CONFIG_BPF: "y"}, - expectedError: nil, - expectedValue: "y", + testName: "multiple options ok", + givenOptions: []KernelConfigOption{CONFIG_BPF, CONFIG_BPF_SYSCALL, CONFIG_TEST_BPF, CONFIG_HZ}, + givenValues: []interface{}{BUILTIN, BUILTIN, MODULE, "250"}, + missingOptions: []KernelConfigOption{}, }, { - name: "nil conf", - key: CONFIG_BPF, - conf: nil, - expectedError: errors.New("kernel config value does not exist, it's possible this option is not present in your kernel version or the KernelConfig has not been initialized"), - expectedValue: "", + testName: "multiple options ok with single not ok", + givenOptions: []KernelConfigOption{CONFIG_BPF, CONFIG_BPF_SYSCALL, CONFIG_TEST_BPF, CONFIG_HZ}, + givenValues: []interface{}{MODULE, BUILTIN, MODULE, "250"}, + missingOptions: []KernelConfigOption{CONFIG_BPF}, }, { - name: "Value not present", - key: CONFIG_BPF_JIT, - conf: KernelConfig{CONFIG_BPF: "y"}, - expectedError: errors.New("kernel config value does not exist, it's possible this option is not present in your kernel version or the KernelConfig has not been initialized"), - expectedValue: "", + testName: "multiple options ok with multiple not ok", + givenOptions: []KernelConfigOption{CONFIG_BPF, CONFIG_BPF_SYSCALL, CONFIG_TEST_BPF, CONFIG_HZ, CONFIG_HZ}, + givenValues: []interface{}{MODULE, BUILTIN, MODULE, "250", "500"}, + missingOptions: []KernelConfigOption{CONFIG_BPF, CONFIG_HZ}, + }, + { + testName: "undefined value", + givenOptions: []KernelConfigOption{0xFFFFFFFF}, + givenValues: []interface{}{UNDEFINED}, // non-existing values will be ignored + missingOptions: []KernelConfigOption{}, }, } - for _, tt := range testCases { - t.Run(tt.name, func(t *testing.T) { - v, err := tt.conf.GetKernelConfigValue(tt.key) - assert.Equal(t, tt.expectedError, err) - assert.Equal(t, tt.expectedValue, v) + for _, tt := range testCases { // for each of the test cases run: + t.Run(tt.testName, func(test *testing.T) { // a test named testName with the following func(): + for _, configFile := range allConfigFiles { + var err error + + // initialize the KernelConfig object + config := KernelConfig{} + err = config.initKernelConfig(configFile) + assert.Equal(test, err, nil) + + // add needed KernelConfigOptions + for pos, option := range tt.givenOptions { + config.AddNeeded(option, tt.givenValues[pos]) + } + + // check amount of missing KernelConfigOptions first + missing := config.CheckMissing() + assert.Equal(test, len(tt.missingOptions), len(missing)) + + // check if missing KernelConfigOptions are the correct ones + assert.ElementsMatch(test, tt.missingOptions, missing) + } }) } } diff --git a/helpers/testdata/comments_config.gz b/helpers/testdata/comments_config.gz deleted file mode 100644 index 9633fe1f..00000000 Binary files a/helpers/testdata/comments_config.gz and /dev/null differ diff --git a/helpers/testdata/config_comments b/helpers/testdata/config_comments new file mode 100644 index 00000000..84c98923 --- /dev/null +++ b/helpers/testdata/config_comments @@ -0,0 +1,17 @@ +# this is a comment in the beginning +CONFIG_BPF=y +CONFIG_BPF_LSM=y +CONFIG_BPF_SYSCALL=y +# this is comment in the middle +CONFIG_ARCH_WANT_DEFAULT_BPF_JIT=y +CONFIG_BPF_JIT_ALWAYS_ON=y +CONFIG_BPF_JIT_DEFAULT_ON=y +CONFIG_BPF_PRELOAD=y +CONFIG_BPF_PRELOAD_UMD=m +# CONFIG_BLAH is not set +CONFIG_IPV6_SEG6_BPF=y +CONFIG_NETFILTER_XT_MATCH_BPF=m +CONFIG_HZ_250=y +CONFIG_HZ=250 +CONFIG_TEST_BPF=m +# a comment at the end diff --git a/helpers/testdata/config_comments.gz b/helpers/testdata/config_comments.gz new file mode 100644 index 00000000..737ddf17 Binary files /dev/null and b/helpers/testdata/config_comments.gz differ diff --git a/helpers/testdata/config_standard.gz b/helpers/testdata/config_standard.gz index 636ab23d..8f78d4c0 100644 Binary files a/helpers/testdata/config_standard.gz and b/helpers/testdata/config_standard.gz differ diff --git a/helpers/testdata/tarred_config.tar b/helpers/testdata/tarred_config.tar deleted file mode 100644 index 05f27ee0..00000000 Binary files a/helpers/testdata/tarred_config.tar and /dev/null differ