Skip to content

ci: run the test suite under the address and undefined behaviour sanitizers - #2231

Open
kolyshkin wants to merge 6 commits into
containers:mainfrom
kolyshkin:asan-check-job
Open

kolyshkin wants to merge 6 commits into
containers:mainfrom
kolyshkin:asan-check-job

Conversation

@kolyshkin

@kolyshkin kolyshkin commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Run the test suite under the address and undefined behaviour sanitizers.

Nothing did so far: the check jobs already pass ASAN_OPTIONS to every
make check, but nothing was ever built with a sanitizer, so the variable --
and the libasan6 the workflow installs -- did nothing. The only sanitizer
build is the fuzzing one, which covers what the fuzzing targets reach and
nothing else.

Four commits, the first two being what turning it on immediately found:

  1. exec: the capabilities structure was allocated with xmalloc and every
    field filled in except the _residual json_object pointer, which the
    generated free function hands to json_object_put. glibc happens to return
    zeroed memory there; with the sanitizer's allocator
    crun exec --cap CAP_KILL segfaults on exit, after the exec succeeded.
  2. cgroup: -Werror fails the sanitizer build on a maybe-uninitialized
    that gcc only sees once the sanitizers change what it inlines. The warning
    is wrong -- period_len is only read when period_str is set, and they are
    set together -- but the build has to pass.
  3. build: add --enable-sanitizers. The flags go into their own substituted
    variables appended to the per-target flags, the way --enable-coverage
    does, rather than into CFLAGS. In CFLAGS they would reach tests/init,
    which is linked statically, and they would make the configure check for a
    working -static fail, which silently disables the entire test suite.
    Note -fno-sanitize-recover: without it UBSan only prints and the suite
    stays green regardless.
  4. ci: add a check-sanitizers job, root and rootless -- the two reach
    different code, cgroup setup and the mount and user namespace paths above
    all.

The new job passes. The testing-farm failures are unrelated; they fail on
every PR right now.

@kolyshkin
kolyshkin force-pushed the asan-check-job branch 3 times, most recently from badf6fc to 19e5674 Compare September 3, 2026 07:27
@kolyshkin kolyshkin changed the title ci: run the test suite under the address sanitizer ci: run the test suite under the address and undefined behaviour sanitizers Sep 3, 2026
@kolyshkin
kolyshkin force-pushed the asan-check-job branch 2 times, most recently from b503de0 to 89735f3 Compare September 3, 2026 18:27
@kolyshkin
kolyshkin marked this pull request as ready for review September 3, 2026 18:47
The capabilities structure passed to the exec code was allocated with
xmalloc and every field was filled in afterwards -- every field except the
_residual json_object pointer, which the generated free function passes to
json_object_put. It was thus called on whatever the allocator happened to
leave there.

In practice glibc hands out zeroed memory here, so the pointer was NULL and
json_object_put did nothing, but an allocator that fills fresh allocations
with a pattern, such as the address sanitizer, makes "crun exec --cap" crash
on exit:

  #0  json_object_put () from /lib64/libjson-c.so.5
  #1  free_runtime_spec_schema_config_schema_process_capabilities ()
  #2  free_runtime_spec_schema_config_schema_process ()
  containers#3  cleanup_process_schemap () at src/exec.c:236

Use xmalloc0, as is already done a few lines above for the user structure.

Signed-off-by: Kir Kolyshkin <[email protected]>
period_len is only read when period_str is not NULL, and the two are set
together, but gcc does not make that connection once the sanitizers change
what it inlines, and --enable-werror then fails the build:

  src/libcrun/cgroup-resources.c:1106:21: error: 'period_len' may be used
  uninitialized [-Werror=maybe-uninitialized]

Initialize it.

Signed-off-by: Kir Kolyshkin <[email protected]>
Add a configure option that builds libcrun, the crun binary and the testing
library with the address and undefined behaviour sanitizers, so that the test
suite can be run under them.

The flags are kept in their own substituted variables and appended to the
per-target flags, following what --enable-coverage already does, rather than
added to CFLAGS. Putting them in CFLAGS would reach tests/init as well, which
is linked statically and cannot be built with the sanitizers; it would also
make the configure check for a working -static flag fail, which in turn
disables the whole test suite.

The undefined behaviour sanitizer needs -fno-sanitize-recover: without it it
only prints what it finds and lets the program carry on, so a test suite run
would stay green no matter what it reported.

Signed-off-by: Kir Kolyshkin <[email protected]>
Run the test suite against a build with the address and undefined behaviour
sanitizers. Nothing did so far: the check jobs pass ASAN_OPTIONS to make
check, but nothing was ever built with a sanitizer, so the variable had no
effect. The only sanitizer build is the fuzzing one, which covers the code the
fuzzing targets reach and nothing else.

Both as root and rootless: the two reach different code -- cgroup setup, the
mount handling and the user namespace paths above all -- so both are worth
looking at under the sanitizers.

Signed-off-by: Kir Kolyshkin <[email protected]>
The fuzzer harness is a subreaper, so every process a container leaves
behind is reparented to it.  They were collected from a SIGCHLD handler,
which has two problems.

The handler consumes the status of the processes libcrun_container_run()
is itself waiting for, so the non-detached target never saw its container
exit.  And because the reaping was asynchronous, a container from one
iteration could outlive it and die during the next one, which under
honggfuzz -- it traces the whole process tree -- attributes the death to
an input that had nothing to do with it.

Drop the handler and drain the children at the end of every container
run instead, with a bound, since a container process is under no
obligation to ever exit.

Signed-off-by: Kir Kolyshkin <[email protected]>
Modes 0 and 1 fork and run a real container, and honggfuzz's crash
counter cannot be used to judge them.  honggfuzz attaches with
PTRACE_O_TRACEFORK, so every process the container spawns is traced as
well, and any of them dying from a signal is recorded as a crash of the
target -- including the ones that are meant to die: killed by the fuzzed
seccomp profile, killed by the kernel because a fuzzed rlimit made
execve() fail past the point of no return, or killed by the runtime
itself while it cleans the container up.

None of honggfuzz's own filters can be used to sort those out: the crash
counter is incremented before the stackhash blocklist is consulted, and
--verifier gives up on a crash it could not unwind, which is exactly what
these are.

So drop --exit_upon_crash for those two, let the run finish, and decide
afterwards by replaying every artifact in a fresh process with the
sanitizers told to report what they catch.  A memory error in the runtime
is reported by ASan or UBSan, which are still live in the process that
hits it; a container process killed after execve() has left the sanitizer
runtime behind with the rest of its address space and produces nothing.
Only the former fails the job.

The other modes do not fork and keep asserting crashes_count:0.

Signed-off-by: Kir Kolyshkin <[email protected]>
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.

1 participant