Skip to content

Bug: Masked configuration/decoding errors cause silent fallback to runc and confusing failures #876

Description

@Nachiket-Roy

Summary

When urunc loads a unikernel configuration via GetUnikernelConfig(), any parsing, validation, or base64 decoding error is collapsed into ErrNotUnikernel.

As a result, urunc assumes the workload is a normal OCI container and silently falls back to runc. Since the rootfs only contains a unikernel binary, runc eventually fails with an unrelated error (missing executable, missing filesystem, rootless configuration, etc.), hiding the actual configuration problem.

Code references

Error masking (pkg/unikontainers/unikontainers.go)

config, err := GetUnikernelConfig(bundlePath, spec)
if err != nil {
    return nil, ErrNotUnikernel
}

GetUnikernelConfig() already distinguishes between several failure modes:

  • missing configuration
  • malformed JSON
  • invalid base64
  • validation failures
  • filesystem errors

However, all of these are discarded here.

Silent fallback (cmd/urunc/create.go)

unikontainer, err := unikontainers.New(...)
if err != nil {
    if errors.Is(err, unikontainers.ErrQueueProxy) ||
        errors.Is(err, unikontainers.ErrNotUnikernel) {

        return runcExec()
    }
    return err
}

Since every configuration error becomes ErrNotUnikernel, the runtime silently executes runc instead.

Reproduction 1: invalid base64 annotations

Example urunc.json:

{
  "com.urunc.unikernel.unikernelType": "not-base64-!!!",
  "com.urunc.unikernel.hypervisor": "not-base64-!!!",
  "com.urunc.unikernel.binary": "not-base64-!!!"
}
./dist/urunc_static_amd64 create --bundle ./repro_bundle test-container

Observed output:

INFO failed to fetch urunc annotations from spec, fallback to urunc.json
ERRO Failed to decode string: not-base64-!!!
ERRO Failed to decode string: not-base64-!!!
ERRO Failed to decode string: not-base64-!!!
ERRO runc create failed: rootless container requires user namespaces

The important observation is not the final runc error itself (the exact message depends on the environment), but that runcExec() is reached at all after a configuration decoding failure.

Note on the decode logs

The three Failed to decode string messages come from tryDecode(), a helper used only for preview logging.

They are not the decode operation that determines container creation.

The real decoding happens later inside UnikernelConfig.decode(), which returns an error such as:

failed to decode Hypervisor:
illegal base64 data at input byte 3

That error is immediately discarded by New() and never reaches the user.

Reproduction 2: missing mandatory field

urunc.json:

{
  "com.urunc.unikernel.unikernelType": "dW5pa3JhZnQ=",
  "com.urunc.unikernel.binary": "L2tlcm5lbA=="
}

Observed output:

INFO failed to fetch urunc annotations from spec, fallback to urunc.json
ERRO runc create failed: rootless container requires user namespaces

Here no base64 decoding fails.

Instead, jsonConf.validate() returns:

unikernel configuration is missing mandatory field:
com.urunc.unikernel.hypervisor

That validation error is also discarded and replaced by the unrelated runc failure.

This demonstrates that the problem is not specific to base64 decoding it affects all configuration errors.

Impact

Malformed unikernel configurations are treated as ordinary OCI containers.

Instead of reporting the actual configuration error, urunc falls back to runc, producing misleading runtime failures that make debugging unnecessarily difficult.

Proposed solution

Only fall back to runc when no unikernel configuration exists.

If configuration is present but invalid, propagate the original error.

config, err := GetUnikernelConfig(bundlePath, spec)
if err != nil {
    if errors.Is(err, os.ErrNotExist) {
        return nil, ErrNotUnikernel
    }

    return nil, err
}

This preserves the existing behavior for ordinary OCI containers while allowing malformed unikernel configurations to fail with descriptive errors.

Behavior after the change

Scenario Current Proposed
No annotations + no urunc.json Fall back to runc Fall back to runc
Invalid JSON Fall back to runc Return JSON parse error
Invalid base64 Fall back to runc Return decode error
Missing mandatory field Fall back to runc Return validation error
urunc.json is a directory Fall back to runc Return descriptive error

Known limitation

A partial annotation set (for example, only unikernelType with no urunc.json) still eventually results in os.ErrNotExist and therefore falls back to runc.

Whether partial annotations should be considered sufficient evidence of unikernel intent is a separate design question and intentionally left out of this change to keep the fix focused.

LLM Disclosure

Used GPT to draft and refine the issue report, and Gemini to help verify the issue and reproduction.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    devInvolves developmentenhancementNew feature or request

    Type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions