From bd058ff2e52f145c0fdd596e6d3318aa4bad70b5 Mon Sep 17 00:00:00 2001 From: Kir Kolyshkin Date: Thu, 8 Oct 2026 10:55:31 -0700 Subject: [PATCH] tests/int: check config.json keys in update_config Go's encoding/json (v1) matches object keys case-insensitively, and silently ignores unknown keys. As a result, runc happily accepts a config.json with e.g. "CPU" instead of "cpu", or "pagesize" instead of "pageSize", while other runtimes (which run runc integration tests, e.g. crun) may not. In addition, runc will likely switch to encoding/json/v2 at some point, which matches keys case-sensitively, so such keys will be silently ignored by runc, too. Add a simple check-config helper, which checks that all keys in config.json are known to runtime-spec (and spelled exactly as in the spec), and call it from update_config, so tests using wrong keys fail right away. Signed-off-by: Kir Kolyshkin --- Makefile | 2 +- tests/cmd/check-config/check-config.go | 110 +++++++++++++++++++++++++ tests/integration/helpers.bash | 2 + 3 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 tests/cmd/check-config/check-config.go diff --git a/Makefile b/Makefile index 08de31efe..8fdd6138b 100644 --- a/Makefile +++ b/Makefile @@ -90,7 +90,7 @@ TESTBINDIR := tests/cmd/_bin $(TESTBINDIR): mkdir $(TESTBINDIR) -TESTBINS := recvtty sd-helper seccompagent fs-idmap pidfd-kill remap-rootfs key_label +TESTBINS := recvtty sd-helper seccompagent fs-idmap pidfd-kill remap-rootfs key_label check-config .PHONY: test-binaries $(TESTBINS) test-binaries: $(TESTBINS) $(TESTBINS): $(TESTBINDIR) diff --git a/tests/cmd/check-config/check-config.go b/tests/cmd/check-config/check-config.go new file mode 100644 index 000000000..541c51a8b --- /dev/null +++ b/tests/cmd/check-config/check-config.go @@ -0,0 +1,110 @@ +// check-config checks that config.json only has the keys known to +// runtime-spec, spelled exactly as in the spec. +// +// This is needed because Go's encoding/json (v1) matches keys +// case-insensitively, and silently ignores unknown keys, so a wrong key +// in a test goes unnoticed by runc (but not by other runtimes). +package main + +import ( + "encoding/json" + "fmt" + "maps" + "os" + "reflect" + "slices" + "strconv" + "strings" + + "github.com/opencontainers/runtime-spec/specs-go" +) + +func main() { + if len(os.Args) != 2 { + fmt.Fprintf(os.Stderr, "usage: %s config.json\n", os.Args[0]) + os.Exit(2) + } + data, err := os.ReadFile(os.Args[1]) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + var v any + if err := json.Unmarshal(data, &v); err != nil { + fmt.Fprintf(os.Stderr, "%s: %v\n", os.Args[1], err) + os.Exit(1) + } + errs := check(v, reflect.TypeFor[specs.Spec](), "") + for _, e := range errs { + fmt.Fprintf(os.Stderr, "%s: %s\n", os.Args[1], e) + } + if len(errs) > 0 { + os.Exit(1) + } +} + +func check(v any, t reflect.Type, path string) (errs []string) { + for t.Kind() == reflect.Pointer { + t = t.Elem() + } + switch t.Kind() { + case reflect.Struct: + m, ok := v.(map[string]any) + if !ok { + return nil + } + fields := jsonFields(t) + for _, k := range slices.Sorted(maps.Keys(m)) { + ft, ok := fields[k] + if !ok { + errs = append(errs, unknownKey(path, k, fields)) + continue + } + errs = append(errs, check(m[k], ft, path+"/"+k)...) + } + case reflect.Slice, reflect.Array: + s, _ := v.([]any) + for i, e := range s { + errs = append(errs, check(e, t.Elem(), path+"/"+strconv.Itoa(i))...) + } + case reflect.Map: + m, _ := v.(map[string]any) + for _, k := range slices.Sorted(maps.Keys(m)) { + errs = append(errs, check(m[k], t.Elem(), path+"/"+k)...) + } + } + return errs +} + +// jsonFields returns a map of JSON keys to field types for a struct type. +func jsonFields(t reflect.Type) map[string]reflect.Type { + fields := make(map[string]reflect.Type) + for f := range t.Fields() { + name, _, _ := strings.Cut(f.Tag.Get("json"), ",") + if name == "-" { + continue + } + // Fields of an embedded struct with no tag are promoted. + if f.Anonymous && name == "" && f.Type.Kind() == reflect.Struct { + maps.Copy(fields, jsonFields(f.Type)) + continue + } + if !f.IsExported() { + continue + } + if name == "" { + name = f.Name + } + fields[name] = f.Type + } + return fields +} + +func unknownKey(path, key string, fields map[string]reflect.Type) string { + for name := range fields { + if strings.EqualFold(name, key) { + return fmt.Sprintf("%s/%s: wrong case, should be %q", path, key, name) + } + } + return fmt.Sprintf("%s/%s: unknown key", path, key) +} diff --git a/tests/integration/helpers.bash b/tests/integration/helpers.bash index 2534868b7..fccc95c03 100755 --- a/tests/integration/helpers.bash +++ b/tests/integration/helpers.bash @@ -82,6 +82,8 @@ function update_config() { local config config=$(jq "$@" "./config.json") || return echo "$config" >"./config.json" + # Make sure the test does not use wrong or unknown keys. + "$TESTBINDIR/check-config" "./config.json" } # Shortcut to add additional uids and gids, based on the values set as part of