β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-2785

Reachable panic("semop - can't undo undos") in sys_semop SEM_UNDO rollback via same-process thread race β€” unprivileged local kernel DoS

Field Value
ID DF-2785
Status new
Severity Medium
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H
CWE CWE-362 Race Condition β†’ unhandled error (kernel panic)
File sys/kern/sysv_sem.c
Lines 998-1006 (rollback), 237-268 (adjust)
Area kern
Confidence certain
Discovered 2026-08-31
Pass 2 (GLM 5.3 second pass)
Bucket base:kern
Reported pending
Known CVE none
CVE match novel

Summary

sys_semop's SEM_UNDO error path rolls undo adjustments back in reverse order and panic()s if any rollback adjust fails, on the assumption that reverse order "guarantees we won't run out of space". That guarantee is single-threaded only: undo entries are keyed (semid, semnum) in the process-shared p_sem_undo, and semundo_adjust() serializes only per call β€” nothing protects one thread's undo vector against siblings. A victim thread whose vector self-cancels an entry reaches the rollback with that entry absent by construction; if sibling churn threads keep the undo table full (un_cnt == semume == 25) in the gap, the rollback adjust takes not-found+full β†’ EINVAL β†’ panic. Reproduced on the stock INVARIANTS guest: unprivileged user panicked the kernel in ~3 seconds. Full-system DoS (reboot); negative control (lockstep churn, table never full) 0 errors in 21M iterations, proving the full-table precondition.

Proof of contest

VERIFIED (findings/poc/DF-2785/undo_race_panic.c, unpriv): 25 filler undo entries + two churn threads oscillating un_cnt 24↔25 + victim looping the 4-op self-cancelling vector β†’ panic: semop - can't undo undos, sys_semop+0x65d, guest wedged in DDB ~3s after launch. Fixed kernel (panic β†’ kprintf + break) survived 300s+: 306M victim iterations, 21247 failure-path errors, 170 formerly-fatal race events, guest up.

Minimal: replace the panic with kprintf + break (validated). Full fix: serialize the per-process undo vector (mutex held across sys_semop's adjust/rollback phase and semexit's apply loop). fix.diff in the pack.

Timeline

  • 2026-08-31 Discovered during pass-2 audit of sysv_sem.c (GLM 5.3); unpriv panic reproduced ~3s + fix validated same run.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2785 Β· 10 files
FileTypeDescriptionSize
undo_race_panic.c β€” 3.7 KB view raw
build.sh β€” 76 B view raw
run.sh β€” 195 B view raw
build.log β€” 88 B view raw
run.log β€” 822 B view raw
panic.txt β€” 372 B view raw
env.txt β€” 305 B view raw
fix.diff β€” 656 B view raw
VERDICT.md β€” 2.7 KB ↓ raw
verdict.json β€” 3.0 KB view raw
VERDICT.md
↓ download raw

DF-2785 VERDICT

Status: reproduced (unprivileged kernel panic β†’ local DoS, certain). Fix validated (fixed).

Baseline (stock kernel #0, INVARIANTS): panic in ~3 seconds

First attempt design churned without filling the undo table (0 errors in 21M iterations β€” churn threads in lockstep kept only ~2 entries alive; kept as run.log commentary on iteration counts). Corrected churn (persistent 25 filler entries + dip/re-fill cycles, staggered threads) panicked the guest within ~3s of launch as an unprivileged user:

panic: semop - can't undo undos
cpuid = 3
Trace beginning at frame 0xfffff8011833b888
sys_semop() at sys_semop+0x65d
syscall2() at syscall2+0x11e
Debugger("panic")

(serial console; full capture in panic.txt; guest wedged in DDB β†’ down.)

Root cause (sys/kern/sysv_sem.c:998-1006): the SEM_UNDO rollback loop panic()s if a reverse-order rollback adjust fails. The "reverse order guarantees we won't run out of space" argument is valid only single-threaded: semundo_adjust() (:219-269) keys entries by (semid, semnum) in the process-shared p->p_sem_undo and takes only p_token, per call β€” nothing serializes one thread's undo vector against sibling threads of the same process. Victim vector {w,+1,U}{w,-1,U}{x,+1,U}{x,-1,U}: adjust#1 deletes the entry adjust#0 created (net-zero), so if adjust#2 fails on a full table (un_cnt == semume == 25), the rollback of sops[1] looks up an entry that is absent by construction while the table is full β†’ EINVAL β†’ panic. Churn threads only need to re-fill the table inside the sub-microsecond gap between the victim's adjust#1 and adjust#2.

Fix validation (patched kernel #1: panic β†’ kprintf + break)

Race re-run for 300+ seconds on the patched kernel:

victim : 306184192 iters, 21247 errors        (failure path heavily exercised)
semop: SEM_UNDO rollback lost a race, pid 860 (dmesg)
… 170 occurrences of "lost a race" …          (the exact formerly-panicking path)
guest: up                                      (baseline: dead in ~3s)

Every one of the 170 race events would have been the panic on the stock kernel. Bad behavior gone: no panic; the condition is logged.

Impact ceiling

Medium β€” unprivileged, reliable (seconds) local kernel panic = local DoS. panic() is an intentional kernel halt, so no memory corruption beyond the DoS itself; a fuller fix would serialize undo vectors per-process (mutex across the adjust/rollback phase) rather than only removing the crash.

Artifacts

panic.txt (serial console), run.log (baseline narrative + fixed-run progress), run.fixed.log (patched-kernel run + dmesg count), env.txt, fix.diff (validated in-guest: build RC=0, installkernel RC=0).

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Patched kernel (fix.diff, nativekernel RC=0, installkernel RC=0) survived 300s+ of the identical race that killed the stock kernel in 3s; 170 formerly-fatal events logged, guest stayed up.

["run.fixed.log: 'victim : 306184192 iters, 21247 errors' + dmesg count 170 + guest 'up'", 'fix.diff']
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #1: Tue Sep 1 06:03:53 UTC 2026 root:dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Evidence (decisive lines)

["panic.txt: 'panic: semop - can't undo undos', sys_semop+0x65d via syscall2", "run.fixed.log: guest up, 21247 failure-path errors, dmesg x170 'semop: SEM_UNDO rollback lost a race, pid 860'", 'VERDICT.md: construction, timeline, baseline vs patched']

PoC changes

Two design iterations: (1) naive +1/-1 churn left 0 persistent entries (0 errors in 21M iters - kept as negative evidence); (2) persistent 25-filler setup + dip/re-fill churn with staggered threads panicked in ~3s.

Verified recommended fix

Replace the rollback panic with a kprintf+break (validated in-guest: no panic across 170 race events); longer term, hold a per-process undo mutex across the adjust/rollback phase.

Verdict

sys_semop()'s SEM_UNDO rollback panic("semop - can't undo undos") (sysv_sem.c:1004-1006) is reachable by an unprivileged user with two sibling threads: the reverse-order rollback 'cannot run out of space' guarantee is single-threaded only, because undo entries (keyed semid/semnum in the process-shared p_sem_undo) are mutated by sibling threads between the victim's per-call p_token sections. Victim vector {w,+1,U}{w,-1,U}{x,+1,U}{x,-1,U} with churn keeping un_cnt==semume==25 makes rollback of sops[1] find an absent entry in a full table -> EINVAL -> panic. Stock INVARIANTS guest panicked in ~3s (panic.txt). Patched kernel (panic->kprintf+break) survived 300s+ with 170 logged race events and 306M victim iterations.