Skip to content

Do not crash on a null in the configuration - #2234

Merged
giuseppe merged 3 commits into
containers:mainfrom
kolyshkin:fix-null-in-spec-arrays
Sep 9, 2026
Merged

Do not crash on a null in the configuration#2234
giuseppe merged 3 commits into
containers:mainfrom
kolyshkin:fix-null-in-spec-arrays

Conversation

@kolyshkin

Copy link
Copy Markdown
Collaborator

A JSON null where the configuration expects a string is parsed into a NULL
pointer, which is how libocispec represents a field that is not present.
The code walking these values took a C string for granted, so a document
that is merely invalid, rather than malicious, killed the runtime:

$ jq '.mounts[1].options' config.json
[
  "nosuid",
  null
]
$ crun run nullopt
Segmentation fault (core dumped)

The first commit is an unrelated bug found on the way: the array of
arguments built for crun exec was one element too long, and its length
counted the crun arguments along with the command.

The second commit checks for a NULL where such a value is used, and what
it does depends on what the value means. An option of a mount is
free-form, so a null one is skipped, as an option that is not there; a
value that selects a behavior -- a seccomp flag or architecture, a
scheduler flag, an environment variable, a sysctl -- is rejected with
EINVAL, the way an unknown value in the same place already is; an
annotation whose value is null is left out of the state, which cannot hold
a null. Five of the loops over the options of a mount go through
get_mount_flags_or_option(), so the check for those lives there.

The third commit is a test that builds a configuration touching as many
string-valued fields as possible and puts a null in each of its strings in
turn -- 87 of them as it stands -- expecting the runtime to report what is
wrong instead of dying. On the tree before the fix it names 37 offending
fields.

A container process that dies on a NULL leaves the runtime with a broken
channel and no error of its own, which is what the test looks for besides
a signal. Built with the sanitizers, the same test also reports the
undefined behavior of handing a NULL to a function declared not to take
one, which is how two of the fields (process.scheduler.flags and
linux.sysctl) turned up.

The first case came from the fuzzer; a two hour run of all ten targets on
this branch, with ASan and UBSan, found nothing.

The array of arguments built for "crun exec" was one element too long,
and its length field was set to the whole argc, not to the number of
arguments actually copied.  The loop ran once past the last argument,
reading argv[argc], which is the terminating NULL of argv, so the extra
element ended up as a NULL in the middle of an array of strings, and
args_len counted the crun arguments (the global options, the "exec"
itself and the container id) along with the command.

Nothing dereferenced the extra element so far, since the array is passed
to execv(), which stops at the NULL, but the length is wrong for anything
that walks the array.

Copy exactly the arguments that follow the container id.

Signed-off-by: Kir Kolyshkin <kolyshkin@gmail.com>
A JSON null where the configuration expects a string is parsed into a
NULL pointer, which is right: a NULL is how a field that is not present
is represented everywhere else in the parsed document.  The code walking
these values, though, took a C string for granted.  A config.json with

    "mounts": [{"destination": "/dev", "options": ["nosuid", null]}]

made get_idmapped_option() call strlen() on the NULL and die:

    $ crun run nullopt
    Segmentation fault (core dumped)

Check for a NULL where such a value is used.  An option of a mount, which
is free-form, is skipped, as an option that is not there; a value that
selects a behavior -- a seccomp flag or architecture, a scheduler flag, an
environment variable, a sysctl -- is rejected with EINVAL, the way an
unknown value in the same place already is.  An annotation whose value is
null is left out of the state, which cannot hold a null.

Five of the loops over the options of a mount call
get_mount_flags_or_option(), so the check for those lives there.

Found by fuzzing, then by putting a null in each string of a
configuration in turn.

Signed-off-by: Kir Kolyshkin <kolyshkin@gmail.com>
Rather than a case per field, build a configuration that touches as many
string-valued fields as possible, then put a null in each of its strings
in turn -- 87 of them as it stands -- and run a container with it.  The
runtime has to report what is wrong instead of dying.

A container process that dies on a NULL leaves the runtime with a broken
channel and no error of its own, which is what the test looks for besides
a signal; built with the sanitizers, the same test also reports the
undefined behavior of handing a NULL to a function declared not to take
one.

On the tree before the previous commit the test names 37 offending fields.

Signed-off-by: Kir Kolyshkin <kolyshkin@gmail.com>

@giuseppe giuseppe left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@giuseppe
giuseppe merged commit 322729a into containers:main Sep 9, 2026
59 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants