[Bug 299207] libcasper cap_clone(): NULL pointer write after failed dup() in the no-casper fallback branch (libcasper.h:203)

From: <bugzilla-noreply_at_freebsd.org>
Date: Wed, 07 Oct 2026 16:02:05 UTC
https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=299207

            Bug ID: 299207
           Summary: libcasper cap_clone(): NULL pointer write after failed
                    dup() in the no-casper fallback branch
                    (libcasper.h:203)
           Product: Base System
           Version: 16.0-CURRENT
          Hardware: amd64
                OS: Any
            Status: New
          Severity: Affects Some People
          Priority: ---
         Component: bin
          Assignee: bugs@FreeBSD.org
          Reporter: zkd18cjb@mail.ustc.edu.cn

In the `#else /* !WITH_CASPER */` static-inline fallback branch of
`lib/libcasper/libcasper/libcasper.h` (lines 186-206), `cap_clone()` handles a
failed `dup(chan->cch_fd)` incorrectly:

```c
newchan = cap_init();
...
Verbatim from `lib/libcasper/libcasper/libcasper.h` (the `#else /* !WITH_CASPER
*/`
static-inline branch, current main):

```c
        newchan = cap_init();
        if (newchan == NULL) {
                return (NULL);
        }

        if (chan->cch_fd == -1) {
                newchan->cch_fd = -1;
        } else {
                newchan->cch_fd = dup(chan->cch_fd);
                if (newchan->cch_fd < 0) {
                        free(newchan);
                        newchan = NULL;          /* error handled here... */
                }
        }
        newchan->cch_flags = chan->cch_flags;   /* line 203: ...but executed
anyway
                                                -> NULL +
offset-of(cch_flags)=4 write */

        return (newchan);
}
```

The error path frees the channel and NULLs the pointer but **falls through**
to the `cch_flags` assignment instead of returning NULL.

**Trigger:** file-descriptor exhaustion (e.g. `RLIMIT_NOFILE` reached) at the
moment `cap_clone()` is called by a program built without `WITH_CASPER`
(linking the fallback branch). Reproducer: set
`setrlimit(RLIMIT_NOFILE, 16)`, exhaust fds, then call the public
`cap_clone()`.

**Reproducer** (self-contained, ~90 lines; uses only the public header). Build:

```sh
cc -O2 -o cap_clone_repro cap_clone_repro.c        # release
cc -O1 -g -fsanitize=address,undefined -o cap_clone_repro_asan
cap_clone_repro.c
```

Full driver:

```c
/* freebsd-src libcasper cap_clone() NULL-deref reproducer
 *
 * lib/libcasper/libcasper/libcasper.h — the !WITH_CASPER static inline
 * cap_clone():
 *
 *     newchan = cap_init();
 *     if (newchan == NULL) return (NULL);
 *     if (chan->cch_fd == -1) { newchan->cch_fd = -1; }
 *     else {
 *             newchan->cch_fd = dup(chan->cch_fd);
 *             if (newchan->cch_fd < 0) { free(newchan); newchan = NULL; }
 *     }
 *     newchan->cch_flags = chan->cch_flags;      <-- newchan may be NULL here
 *
 * When dup(2) fails (fd table full), newchan is freed and set to NULL but the
 * function falls through to the NULL-pointer write instead of returning NULL.
 *
 * Driver uses ONLY public headers (libcasper.h in no-casper fallback mode:
 * struct cap_channel { int cch_fd; int cch_flags; } is public there).
 *
 * fd-exhaustion group: RLIMIT_NOFILE exhausted -> dup() fails -> SEGV (write
to
 *                NULL+4).  Observed under ASan as SEGV on unknown address.
 * Control group: fds available -> clone succeeds and flag round-trips.
 *
 * Exit: 0 = both groups behaved as documented; 1 = deviation.
 */
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/resource.h>
#include <unistd.h>

#include "libcasper.h"

static int failures = 0;

int
main(int argc, char **argv)
{
        struct rlimit rl;
        int devnull;
        cap_channel_t *chan, *clone;

        devnull = open("/dev/null", O_RDONLY);
        if (devnull < 0) {
                perror("open /dev/null");
                return (1);
        }

        /* ---------- control group: fds available, clone must succeed
---------- */
        chan = cap_init();
        if (chan == NULL) {
                printf("[DEV] control cap_init failed
");
                return (1);
        }
        chan->cch_fd = dup(devnull);
        chan->cch_flags = CASPER_NO_UNIQ;
        clone = cap_clone(chan);
        if (clone != NULL && clone->cch_flags == CASPER_NO_UNIQ) {
                printf("[OK ] control: cap_clone succeeded, flags round-trip "
                    "(fd=%d)
", clone->cch_fd);
                close(clone->cch_fd);
                free(clone);
        } else {
                printf("[DEV] control: cap_clone returned %p (expected "
                    "success)
", (void *)clone);
                failures++;
        }
        close(chan->cch_fd);
        free(chan);

        /* ---------- fd-exhaustion group: dup() must fail ---------- */
        if (argc > 1 && strcmp(argv[1], "exhaust") == 0) {
                rl.rlim_cur = 16;
                rl.rlim_max = 16;
                if (setrlimit(RLIMIT_NOFILE, &rl) != 0) {
                        perror("setrlimit");
                        return (1);
                }
                /* consume all remaining fds; devnull stays open (setrlimit
                 * does not close existing descriptors) */
                for (int i = 0; i < 32; i++)
                        open("/dev/null", O_RDONLY);
                chan = cap_init();
                if (chan == NULL) {
                        printf("[DEV] exhaust: cap_init failed
");
                        return (1);
                }
                /* sanity: dup must fail now */
                if (dup(devnull) != -1) {
                        printf("[DEV] exhaust setup: dup unexpectedly succeeded
");
                        return (1);
                }
                chan->cch_fd = devnull; /* valid, open */
                chan->cch_flags = 0;
                printf("[EXH] calling cap_clone with fd table exhausted "
                    "(dup must fail)...
");
                fflush(stdout);
                clone = cap_clone(chan);
                /* NOT REACHED on vulnerable builds: SEGV inside cap_clone.
                 * On a fixed build we would get here with clone == NULL. */
                if (clone == NULL) {
                        printf("[DEV] exhaust: fixed build (clone returned
NULL, "
                            "no crash)
");
                        failures++;     /* deviation vs vulnerable baseline */
                } else {
                        printf("[DEV] exhaust: clone succeeded unexpectedly
");
                        failures++;
                }
                return (failures == 0 ? 0 : 1);
        }

        printf("run with 'exhaust' argument for the fd-exhaustion group
");
        return (failures == 0 ? 0 : 1);
}

```

**Run:**

```sh
./cap_clone_repro            # control group: fds available, clone succeeds
./cap_clone_repro exhaust    # fd-exhaustion group: dup() fails, NULL write
```

Observed:

- release build (cc -O2): Segmentation fault (exit 139) inside `cap_clone()`
  on the fd-exhaustion run:

```
[OK ] control: cap_clone succeeded, flags round-trip (fd=5)
[EXH] calling cap_clone with fd table exhausted (dup must fail)...
```

- ASan/UBSan build: `libcasper.h:203:21: runtime error: member access within
  null pointer of type 'struct cap_channel_t'` then SEGV on 0x4 (WRITE):

```
[OK ] control: cap_clone succeeded, flags round-trip (fd=5)
[EXH] calling cap_clone with fd table exhausted (dup must fail)...
../lib/libcasper/libcasper/libcasper.h:203:21: runtime error: member access
within null pointer of type 'struct cap_channel_t'
AddressSanitizer:DEADLYSIGNAL
=================================================================
==488075==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000004 (pc
0x5acb439395cc bp 0x000000000000 sp 0x7fffc4529000 T0)
==488075==The signal is caused by a WRITE memory access.
==488075==Hint: address points to the zero page.
    #0 0x5acb439395cc  (cap_clone_repro_asan+0x25cc)
    #1 0x5acb43939d83  (cap_clone_repro_asan+0x2d83)
    #2 0x786d392f7d8f  (/lib/x86_64-linux-gnu/libc.so.6+0x29d8f)
    #3 0x786d392f7e3f  (/lib/x86_64-linux-gnu/libc.so.6+0x29e3f)
    #4 0x5acb43939384  (cap_clone_repro_asan+0x2384)

AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV (cap_clone_repro_asan+0x25cc)
==488075==ABORTING
```

- Control (run without the `exhaust` argument): `[OK ] control: cap_clone
succeeded, flags
  round-trip` — clean; the fd-exhaustion is the sole variable.

**Suggested fix:** add the missing early return after the `free(newchan);
newchan = NULL;`
failure handling (or restructure so the failure path returns immediately).

-- 
You are receiving this mail because:
You are the assignee for the bug.