[Bug 299207] libcasper cap_clone(): NULL pointer write after failed dup() in the no-casper fallback branch (libcasper.h:203)
- Go to: [ bottom of page ] [ top of archives ] [ this month ]
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.