TBRIDGE_GETRESULT without a prior TBRIDGE_LOADTEST leaves tbridge_testcase NULL and panics the kernel in tbridge_test_done's KKASSERT on any INVARIANTS build (stock X86_64_GENERIC ships INVARIANTS; all dfregress kernels build tbridge in)
Summary
Missing LOADTEST-state precondition on the GETRESULT ioctl: the mandatory tbridge_test_done() KKASSERTs tbridge_testcase != NULL - deterministic single-ioctl panic on INVARIANTS kernels (GENERIC ships them; dfregress builds tbridge in). Distinct from DF-2048 (curtest NULL one line earlier). Root-only 0600 debug driver. REPRODUCED on custom INVARIANTS kernel (panic.txt, guest wedged in DDB); kmod variant degrades to ENOTSUP; EINVAL-guard fix validated (patched kernel survives).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5019 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.6 KB | β raw | |
| VERDICT.md | β | 3.5 KB | β raw | |
| poc.c | β | 1.2 KB | view raw | |
| mytcase.c | β | 961 B | view raw | |
| build.sh | β | 260 B | view raw | |
| run.sh | β | 118 B | view raw | |
| build.log | β | 5.5 MB | β download | |
| build.fix.log | β | 5.5 MB | β download | |
| run.log | β | 1.5 KB | view raw | |
| run.kmod.log | β | 84 B | view raw | |
| panic.txt | β | 427 B | view raw | |
| env.txt | β | 491 B | view raw | |
| fix.diff | β | 372 B | view raw | |
| verdict.json | β | 3.5 KB | view raw | |
| manifest.json | β | 1.2 KB | view raw |
DF-5019 β tbridge GETRESULT without LOADTEST: NULL tbridge_testcase β deterministic KKASSERT kernel panic (INVARIANTS builds)
Files
sys/dev/misc/tbridge/tbridge.cβ the defectmytcase.cβ minimal out-of-tree testcase kmod (registerstbridge_curtest)poc.cβ userland trigger (one ioctl, run as root)build.shβ exact guest build commandsrun.shβ exact guest run commands
Build & run (on the DragonFly guest, as root)
See build.sh / run.sh. Two build modes matter:
- tbridge as a kldload'able kmod (
/usr/src/sys/dev/misc/tbridge/Makefile): builds fine,/dev/tbridgeappears (0600 root:wheel). On the stock GENERIC this is the only way to get the driver β GENERIC does not containdevice tbridge(it isoptional tbridgeinsys/conf/files:2122, onlyLINT64has it). Result: no panic β out-of-tree kmods compile withoutINVARIANTS(noopt_global.h), so theKKASSERTintbridge_test_doneis a no-op and the whole NULL walk degrades tocopyoutreturningENOTSUP(errno 45). Seerun.kmod.log. - tbridge compiled into the kernel (
device tbridge+options INVARIANTS, i.e. the dfregress regression-test configuration; stockX86_64_GENERICalso shipsoptions INVARIANTSat sys/config/X86_64_GENERIC:56): deterministic kernel panic on the firstTBRIDGE_GETRESULTioctl. Seerun.log/panic.txt.
Expected output (kernel build)
GETRESULT returned -1, errno=45 (kernel survived: finding NOT reproduced) <- kmod build Fatal trap 9: ... assertion "tbridge_testcase != NULL" failed in tbridge_test_done at /usr/src/sys/dev/misc/tbridge/tbridge.c:196 <- in-kernel build (panic)
Root cause
tbridge_dev_ioctl() (tbridge.c:240) handles TBRIDGE_GETRESULT
(tbridge.c:265) without checking that a TBRIDGE_LOADTEST ever ran.
With a testcase registered (tbridge_curtest != NULL, requires a testcase
kmod) but no LOADTEST, tbridge_testcase (the proplib dict) is still NULL:
- tbridge.c:266
kthread_create(tbridge_curtest->tb_run, ...)β runs the testcase, which callstbridge_test_done()(any real testcase must), - tbridge.c:196
KKASSERT(tbridge_testcase != NULL)β panics on any INVARIANTS kernel, - without INVARIANTS the NULL dict then flows through
testcase_get_timeout(NULL)(tbridge.c:274),testcase_set_result(NULL,..)(tbridge.c:201/289) andprop_dictionary_copyout_ioctl(NULL)β all NULL-tolerant in libprop β ending inENOTSUP.
Distinct from known DF-2048 (tbridge_curtest NULL because no testcase
module is loaded β one line earlier, same ioctl).
DF-5019 β VERDICT
Status: reproduced (panic), fixed (validated). Impact: local DoS β deterministic kernel panic; root-gated. Confidence: certain.
What was claimed
TBRIDGE_GETRESULT (tbridge.c:265-297) performs no check that a
TBRIDGE_LOADTEST preceded it. With a testcase module registered
(tbridge_curtest != NULL) but no LOADTEST issued, tbridge_testcase
(the proplib dict) is still NULL, and the testcase kthread's mandatory
tbridge_test_done() hits KKASSERT(tbridge_testcase != NULL)
(tbridge.c:196) β a deterministic kernel panic on any INVARIANTS kernel.
Without INVARIANTS the NULL dict walks through proplib NULL-tolerantly and
ends at prop_dictionary_copyout_ioctl(NULL) β ENOTSUP.
Distinct from known DF-2048 (tbridge_curtest NULL because no testcase
module is loaded β dereferenced one line earlier at tbridge.c:266).
How it was proven (DragonFly 6.5-DEVELOPMENT guest, KVM)
- Stock kernel + kldload'able tbridge.ko: module built from in-tree
sys/dev/misc/tbridge/Makefile, loaded, testcase kmodmytcase.ko(registers tb_run βtbridge_test_done(RESULT_PASS)) loaded, PoC ranioctl(TBRIDGE_GETRESULT)with no LOADTEST β no panic (run.kmod.log): out-of-tree kmods compile withoutINVARIANTS(no-include opt_global.h), so the KKASSERT is compiled out; the ioctl returned errno 45 (ENOTSUP) from the NULL-dict copyout. This bounds the exposure of the kmod path to a benign error. - tbridge compiled into the kernel (
device tbridgeappended to a copy of X86_64_GENERIC β KERNCONF DF5019,make nativekernel, INVARIANTS active β stock X86_64_GENERIC shipsoptions INVARIANTS, sys/config/X86_64_GENERIC:56): same PoC βpanic: assertion "tbridge_testcase != NULL" failed in tbridge_test_done at /usr/src/sys/dev/misc/tbridge/tbridge.c:196(panic.txt,run.log), guest wedged in DDB, ssh dead. Deterministic, single-shot, no race.
Fix validation
fix.diff (guard tbridge_testcase == NULL β EINVAL at the top of
TBRIDGE_GETRESULT) applied inside the guest's /usr/src copy, kernel
rebuilt (build.fix.log), installed, rebooted, identical PoC rerun:
GETRESULT returned -1, errno=22 (EINVAL) β no panic, guest stayed up.
Exploit chain / uid=0?
None. The device node is 0600 root:wheel (verified: crw------- root:wheel)
and the driver is optional tbridge (sys/conf/files:2122) β absent from
X86_64_GENERIC (only LINT64 lists it), so reaching it at all requires the
admin to build it in or kldload it, and then only root can open the node.
Impact ceiling: root-triggerable kernel panic of a debug/test driver
(realistically hit by dfregress runs: any GETRESULT after a failed/absent
LOADTEST β e.g. LOADTEST failed because no testcase was registered yet β
panics the regression kernel). The DF-2049 concurrency family similarly
stays root-gated.
Notes
sys/sys/tbridge.his not self-contained: a testcase module that includes only it fails to compile (SYSINIT lives in<sys/kernel.h>); worked around in mytcase.c. Build-trap only.tbridge_printfunderflow (DF-2047),hz * timeoutoverflow (DF-2050), concurrent-LOADTEST UAF (DF-2049) remain as known.
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
fixedfix.diff applied to guest /usr/src, kernel rebuilt (nativekernel) and rebooted; identical PoC returns EINVAL (errno 22) with the kernel alive β panic gone. fix_patched_reproduced=false is the desired outcome (bug behavior does NOT reproduce on the patched kernel).
VERDICT.md (Fix validation section), build.fix.log, panic.txt (baseline) vs run.log narrative (patched run: 'GETRESULT returned -1, errno=22')
Confirmed kernel references
- sys/dev/misc/tbridge/tbridge.c:265
- sys/dev/misc/tbridge/tbridge.c:196
- sys/dev/misc/tbridge/tbridge.c:202
- sys/sys/tbridge.h:33
- sys/conf/files:2122
- sys/config/X86_64_GENERIC:56
Detail
Exploit chain
kldload/built-in tbridge + testcase kmod registering tb_run -> open /dev/tbridge (root only) -> ioctl(TBRIDGE_GETRESULT) with no LOADTEST -> kthread runs tb_run -> tbridge_test_done -> KKASSERT(tbridge_testcase != NULL) fails -> panic. No uid=0 path: 0600 device, opt-in driver.
Evidence (decisive lines)
panic.txt (exact assertion + DDB), run.log (baseline narrative), run.kmod.log (kmod path -> ENOTSUP), build.log/build.fix.log (kernel builds), env.txt (uname, /dev/tbridge perms, INVARIANTS), VERDICT.md, poc.c, mytcase.c, build.sh, run.sh, fix.diff
PoC changes
Seed had no runnable PoC. Authored: mytcase.c (out-of-tree testcase kmod; needed #include
Verified recommended fix
Reject TBRIDGE_GETRESULT with EINVAL when tbridge_testcase == NULL (no LOADTEST issued); see fix.diff.
Verdict
Deterministic kernel panic reproduced: ioctl(TBRIDGE_GETRESULT) with no prior TBRIDGE_LOADTEST drives the test kthread into tbridge_test_done() where KKASSERT(tbridge_testcase != NULL) (sys/dev/misc/tbridge/tbridge.c:196) fails on any INVARIANTS kernel β stock X86_64_GENERIC ships INVARIANTS and dfregress kernels build tbridge in; verified on a DragonFly 6.5-DEVELOPMENT guest with 'device tbridge' compiled in (panic + DDB wedge, ssh dead). On the stock kernel with tbridge kldload'ed as a module there is no panic: out-of-tree kmods compile without INVARIANTS, the KKASSERT is a no-op and the walk degrades to ENOTSUP (errno 45). Root-gated throughout (/dev/tbridge is 0600 root:wheel and the driver is opt-in), so impact is a root-triggerable panic of a debug driver, not privilege escalation. fix.diff (EINVAL guard at the top of TBRIDGE_GETRESULT) validated on the guest: baseline panics, patched kernel returns EINVAL and survives.
No comments yet.