Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions pkg/unikontainers/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,19 @@ func GetUnikernelConfig(bundleDir string, spec *specs.Spec) (*UnikernelConfig, e
return nil, fmt.Errorf("config not found in spec annotations or in %s: %w", uruncJSONFilename, err)
}

// Decode before validating. The values in urunc.json are base64, and
// base64 ignores CR and LF, so a field holding only newlines is
// non-empty while encoded but decodes to the empty string. Validating
// the encoded form would let such a config through with mandatory
// fields that downstream code reads as empty.
if err := jsonConf.decode(); err != nil {
return nil, err
}

if err := jsonConf.validate(); err != nil {
return nil, fmt.Errorf("invalid unikernel config from %s: %w", uruncJSONFilename, err)
}

if err := jsonConf.decode(); err != nil {
return nil, err
}
return jsonConf, nil
}

Expand Down
156 changes: 156 additions & 0 deletions pkg/unikontainers/config_fuzz_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
// Copyright (c) 2023-2026, Nubificus LTD
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package unikontainers

import (
"encoding/base64"
"encoding/json"
"io"
"os"
"path/filepath"
"testing"

specs "github.com/opencontainers/runtime-spec/specs-go"
"github.com/sirupsen/logrus"
)

// b64 is a helper for building seed corpus entries that are valid base64.
func b64(s string) string {
return base64.StdEncoding.EncodeToString([]byte(s))
}

// fuzzBundle creates a bundle directory once per fuzz worker process and
// returns the bundle dir, the path of the urunc.json inside its rootfs, and a
// spec pointing at that rootfs.
//
// This is deliberately hoisted out of the fuzz function. Creating the bundle
// per iteration with t.TempDir() costs a mkdir plus a recursive cleanup on
// every execution and drops throughput by roughly two orders of magnitude,
// which is enough to stop the fuzzer finding shallow bugs in a CI-length run.
// Reusing one directory and overwriting the file is safe here because each
// worker process gets its own f.TempDir() and executes iterations serially.
func fuzzBundle(f *testing.F) (string, string, *specs.Spec) {
f.Helper()

// Workload-controlled input makes getConfigFromJSON emit an Error-level
// log line per field that fails to base64-decode. Left enabled, that
// serializes every iteration on stderr and costs about two orders of
// magnitude of throughput.
logrus.SetOutput(io.Discard)

dir := f.TempDir()
rootfs := filepath.Join(dir, "rootfs")
if err := os.MkdirAll(rootfs, 0o750); err != nil {
f.Fatal(err)
}

return dir, filepath.Join(rootfs, uruncJSONFilename), &specs.Spec{
Root: &specs.Root{Path: rootfs},
Annotations: map[string]string{},
}
}

// FuzzConfigMandatoryFields asserts the contract that validate() exists to
// enforce: any config GetUnikernelConfig hands back must carry non-empty
// unikernelType, hypervisor and binary. Sixteen references in
// unikontainers.go read those three straight out of the state annotations
// without rechecking them, and Map() silently drops empty values, so an
// accepted config with an empty mandatory field corrupts container state
// rather than failing loudly.
//
// Input is the four field values, so the fuzzer mutates the base64 payloads
// directly instead of having to synthesize the surrounding JSON. That makes
// it far more effective than FuzzGetUnikernelConfigJSON at reaching the
// decoder, at the cost of not exercising JSON parsing itself.
func FuzzConfigMandatoryFields(f *testing.F) {
f.Add(b64("unikraft"), b64("qemu"), b64("/unikernel/app"), b64("nginx -c /nginx.conf"))
f.Add(b64("rumprun"), b64("hvt"), b64("/unikernel/app.hvt"), b64(""))
f.Add("", "", "", "")
f.Add("not-base64!", "qemu", "bin", "")

dir, jsonPath, spec := fuzzBundle(f)

f.Fuzz(func(t *testing.T, unikernelType, hypervisor, binary, cmdline string) {
data, err := json.Marshal(map[string]string{
annotType: unikernelType,
annotHypervisor: hypervisor,
annotBinary: binary,
annotCmdLine: cmdline,
})
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(jsonPath, data, 0o600); err != nil {
t.Fatal(err)
}

conf, err := GetUnikernelConfig(dir, spec)
if err != nil {
return // rejected, a valid outcome for arbitrary input
}
if conf == nil {
t.Fatal("GetUnikernelConfig returned a nil config and a nil error")
}

if conf.UnikernelType == "" || conf.Hypervisor == "" || conf.UnikernelBinary == "" {
t.Fatalf("accepted config is missing a mandatory field\n"+
"encoded: type=%q hypervisor=%q binary=%q\n"+
"decoded: type=%q hypervisor=%q binary=%q",
unikernelType, hypervisor, binary,
conf.UnikernelType, conf.Hypervisor, conf.UnikernelBinary)
}
})
}

// FuzzGetUnikernelConfigJSON drives arbitrary urunc.json bytes through the
// public entry point. It asserts that a config urunc accepts always carries
// the mandatory fields that downstream code reads out of the state
// annotations, and that no input panics the parser.
func FuzzGetUnikernelConfigJSON(f *testing.F) {
seed, err := json.Marshal(map[string]string{
annotType: b64("unikraft"),
annotHypervisor: b64("qemu"),
annotBinary: b64("/unikernel/app"),
annotCmdLine: b64("nginx"),
})
if err != nil {
f.Fatal(err)
}
f.Add(seed)
f.Add([]byte(`{}`))
f.Add([]byte(`not json`))

dir, jsonPath, spec := fuzzBundle(f)

f.Fuzz(func(t *testing.T, data []byte) {
if err := os.WriteFile(jsonPath, data, 0o600); err != nil {
t.Fatal(err)
}

conf, err := GetUnikernelConfig(dir, spec)
if err != nil {
return // rejected, which is a valid outcome for arbitrary input
}
if conf == nil {
t.Fatal("GetUnikernelConfig returned a nil config and a nil error")
}

if conf.UnikernelType == "" || conf.Hypervisor == "" || conf.UnikernelBinary == "" {
t.Fatalf("accepted config is missing a mandatory field: "+
"type=%q hypervisor=%q binary=%q\ninput: %q",
conf.UnikernelType, conf.Hypervisor, conf.UnikernelBinary, data)
}
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
go test fuzz v1
string("\n")
string("\n")
string("\n")
string("")