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

Missing SEMVMX upper-bound in semop/semexit allows semval overflow, wrap, spurious wakeups, and rollback corruption

Field Value
ID DF-0046
Status new
Severity Low
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:L
CWE CWE-190 Integer Overflow or Wraparound; CWE-682 Incorrect Calculation
File sys/kern/sysv_sem.c
Lines 848-854 (semop positive), 530 (SETVAL), 1139 (semexit)
Area kern
Confidence certain
Discovered 2026-06-29
Reported pending

Summary

SEMVMX (32767) is exported to userland via seminfo, and POSIX/SVID requires semop to fail with ERANGE when an operation would make semval exceed SEMVMX, but the kernel never enforces the upper bound. The positive-sem_op branch (:848-854) does semptr->semval += sopptr->sem_op with no upper- bound check (the negative branch at :827 has the lower-bound analogue), and semexit's positive-adjval path (:1139) and SETVAL (:530, int→u_short truncation) are likewise unchecked. The in-tree comment at :160 ("SEMVMX unused - user param") documents that SEMVMX is advisory only. Consequences: semval (a u_short) wraps past 65535; the wrap-to-0 case spuriously satisfies the semzcnt wakeup (:834) waking waiters that believe the semaphore drained (broken mutual exclusion); and once wrapped, the rollback paths (:886, :1013) operate on the wrapped u_short, leaving semval in a state unrelated to its pre-call value. No kernel memory corruption was found (the wrap stays inside the self-contained u_short field) — impact is IPC-state integrity / local DoS via broken synchronization, not privilege escalation.

Root cause

sys/kern/sysv_sem.c:848-854:

} else {                                  /* positive sem_op branch */
    semptr->semval += sopptr->sem_op;     /* :849  no SEMVMX upper-bound check */
    if (sopptr->sem_flg & SEM_UNDO)
        do_undos = 1;
    if (semptr->semncnt > 0)
        wakeup(semptr);
}

Compare the negative branch (:827) which has if (semptr->semval + sopptr->sem_op < 0). semval is u_short (:40). The same omission is at SETVAL (:530, semptr->semval = real_arg.val) and semexit (:1139, semptr->semval += adjval).

Threat model & preconditions

  • Attacker position: any local user with SEM_A on a semaphore set.
  • Privileges gained or impact: corruption of the set's semval, including semaphores shared with other principals (other UIDs in the same group, or root-owned setuid helpers sharing the set). The corruption is silent and persistent, and lets a waiter enter a critical section believing semval==0 when its logical value is much larger β€” a cross-user synchronization/DoS primitive. The rollback wrap can leave a victim's semaphore in an arbitrary state. No path to kernel memory corruption (the wrap stays inside the self-contained u_short field).
  • Required config or capabilities: SEM_A on the set; default kernel.
  • Reachability: semop(2) with large positive ops; semctl(SETVAL).

Proof of concept

PoC source: findings/poc/DF-0046/sem_wrap.c

Build & run (unprivileged)

cc -o sem_wrap findings/poc/DF-0046/sem_wrap.c
./sem_wrap

Expected output

A wrapped semval after four +32767 ops (no ERANGE), and a non-zero semval after a failed-and-rolled-back op (should be 0).

Impact

IPC-state integrity / POSIX non-compliance / local DoS via broken SysV semaphore synchronization. No kernel memory corruption. Rated Low.

Enforce SEMVMX on the positive-op, SETVAL, and semexit paths (the negative branch already shows the pattern):

--- a/sys/kern/sysv_sem.c
+++ b/sys/kern/sysv_sem.c
@@ -848 +848,5 @@
            } else {
+               if (semptr->semval + sopptr->sem_op > seminfo.semvmx) {
+                   eval = ERANGE;
+                   lwkt_relpooltoken(semptr);
+                   goto done;
+               }
                semptr->semval += sopptr->sem_op;

(and the analogous check at SETVAL :530 and semexit :1139).

References

Timeline

  • 2026-06-29 Discovered during automated file-by-file audit of sys/kern/sysv_sem.c.
  • pending Reported to DragonFlyBSD security contact.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0046 Β· 13 files
FileTypeDescriptionSize
sem_wrap.c trigger-source positive-op wrap PoC + wrap-to-0 spurious wait-for-zero release 4.1 KB view raw
fix_test.c trigger-source supplementary test exercising positive-op and SETVAL SEMVMX bounds 1.6 KB view raw
build.sh build-script cc -o sem_wrap sem_wrap.c; cc -o fix_test fix_test.c 201 B view raw
run.sh run-script ./sem_wrap (unprivileged) 367 B view raw
run.log run-log baseline run on unpatched #0 kernel (bug confirmed) 342 B view raw
fix_run.log run-log patched #1 kernel runs (ERANGE at op2 + fix_test all-pass) 888 B view raw
fix_build.log build-log nativekernel build of fix.diff, full output, rc=0 5.6 MB ↓ download
fix.diff suggested-fix SEMVMX bound at semop(+op) :849, SETVAL :530, semexit :1139 1.0 KB view raw
env.txt environment uname, cc version, semvmx/semaem sysctls 234 B view raw
VERDICT.md verdict full narrative: mechanism, impact, before/after fix validation 5.9 KB ↓ raw
README.md readme how to build/run, expected output on buggy vs fixed kernel 2.7 KB ↓ raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
README.md readme how to build/run, expected output on buggy vs fixed kernel
↓ download raw

DF-0046 β€” PoC

sem_wrap.c β€” semval (u_short) overflow/wrap via the missing SEMVMX upper-bound in semop/SETVAL/semexit, with wrap-to-0 breaking wait-for-zero (semop(0)) synchronization semantics.

fix_test.c β€” supplementary test exercising the positive-op and SETVAL paths (used to confirm the fix on the patched kernel).

The bug

SEMVMX (32767) is exported to userland via seminfo and POSIX/SVID requires semop/SETVAL to fail with ERANGE when an operation would make semval exceed SEMVMX. The DragonFly kernel never enforces the upper bound at any of the three write sites:

Path Citation Code
semop (+op) sys/kern/sysv_sem.c:848-854 semptr->semval += sopptr->sem_op;
SETVAL sys/kern/sysv_sem.c:530 semptr->semval = real_arg.val;
semexit sys/kern/sysv_sem.c:1139 semptr->semval += adjval;

The negative-sem_op branch (:827) has the lower-bound analogue (if (semptr->semval + sopptr->sem_op < 0)), and the in-tree comment at :160 admits "SEMVMX unused - user param". semval is u_short (:40), so large positive ops wrap past 65535.

Consequences (demonstrated by sem_wrap.c):

  1. POSIX violation β€” semval exceeds SEMVMX with no ERANGE returned (four +32767 ops land semval at 131068 mod 65536 = 65532).
  2. Wrap-to-0 breaks mutual exclusion β€” +32767 + +32767 + +2 wraps semval to exactly 0 (logical value 65536). A process blocked in semop(semnum=0, op=0) (wait-for-zero, semzcnt) is spuriously released even though the logical semaphore value is large and positive β€” broken SysV semaphore synchronization.

No kernel memory corruption (semval is a self-contained u_short field); impact is IPC-state integrity / POSIX non-compliance / local DoS via broken synchronization. No escalation path (no memory write primitive).

Build & run (unprivileged)

cc -o sem_wrap sem_wrap.c
cc -o fix_test fix_test.c    # supplementary
./sem_wrap

Expected output

BUG present (unpatched #0 kernel)

[1] semval after four +32767 ops = 65532  (SEMVMX=32767, POSIX would have ERANGE'd at op2)
    -> BUG CONFIRMED: semval 65532 > SEMVMX 32767 (no ERANGE)
[2] wait-for-zero child RELEASED after wrap-to-0 (semval logical=65536, u_short=0) -> BUG: spurious release, mutual exclusion broken

FIXED kernel (#1, with fix.diff applied)

sem_wrap: op2 (POSIX says ERANGE; DragonFly allows): Result too large

(fix_test additionally confirms SETVAL also returns ERANGE for val > SEMVMX, and normal small ops still work β€” no regression.)

VERDICT.md verdict full narrative: mechanism, impact, before/after fix validation
↓ download raw

DF-0046 β€” Verdict

Verdict: REPRODUCED (logic bug / POSIX non-compliance / local DoS). Fix VALIDATED.

Summary

The DragonFlyBSD SysV semaphore implementation never enforces the SEMVMX (32767) upper bound that POSIX/SVID requires. semval is a u_short (sys/kern/sysv_sem.c:40); three write sites mutate it without any check against SEMVMX:

Path Citation Defect
semop +op sys/kern/sysv_sem.c:849 semptr->semval += sopptr->sem_op; β€” no bound
SETVAL sys/kern/sysv_sem.c:530 semptr->semval = real_arg.val; — int→u_short trunc, no bound
semexit sys/kern/sysv_sem.c:1139 semptr->semval += adjval; β€” no bound

The negative-sem_op branch (sys/kern/sysv_sem.c:827) does have the lower-bound analogue (if (semptr->semval + sopptr->sem_op < 0)), confirming the upper-bound omission is the bug, not an intentional design. The in-tree comment at sys/kern/sysv_sem.c:160 itself admits "SEMVMX unused - user param".

Mechanism (trigger β†’ primitive β†’ effect)

  1. Trigger β€” unprivileged user calls semget(IPC_PRIVATE, ...) then semop with large positive sem_op values (each ≀ 32767, the short range). No privilege check beyond the set's IPC mode bits.
  2. Primitive β€” the positive-op branch at :849 does semptr->semval += sopptr->sem_op with no SEMVMX check. Four +32767 ops accumulate 0 + 4Γ—32767 = 131068, which wraps the u_short to 131068 mod 65536 = 65532. The wrap is silent (no ERANGE).
  3. Effect (demonstrated two ways): - POSIX violation: semval = 65532 > SEMVMX (32767) with no ERANGE. A POSIX-compliant kernel would have returned ERANGE at the second op. - Wrap-to-0 breaks mutual exclusion: +32767 + +32767 + +2 = 65536 wraps semval to exactly 0 (logical value 65536). A process blocked in semop(0) (wait-for-zero, semzcnt waiter) is spuriously released β€” the PoC's forked child exits with code 42 (released) where it should have blocked indefinitely. This breaks SysV semaphore mutual exclusion for any consumer that relies on the zero state.

Impact

IPC-state integrity / POSIX non-compliance / local DoS via broken SysV semaphore synchronization. No kernel memory corruption β€” the wrap stays inside the self-contained u_short field; there is no write primitive, no adjacent-object corruption, no escalation path. Rated Low (matches the finding's severity). The wrap-to-0 spurious-release is the most security-relevant consequence (broken cross-process mutual exclusion), but it requires a victim process sharing the semaphore set and using wait-for-zero semantics.

Exploit chain

none β€” this is a pure logic/integer bug with no memory-corruption primitive. semval is a self-contained u_short; the wrap does not corrupt adjacent kernel memory. There is no write primitive to convert to control flow, no slab grooming, no function pointer to hijack. The realistic impact ceiling is the wrap-to-0 spurious wakeup demonstrated in observation [2], which is a synchronization-integrity / local-DoS issue.

PoC changes

The original sem_wrap.c made a "rollback leaves non-zero" claim that does not hold (u_short arithmetic is closed under mod 65536, so symmetric add/subtract in the rollback returns to the original value β€” the rollback correctly lands at 0). I replaced that false claim with a sharper, more security-relevant demonstration: the wrap-to-0 spurious release of a wait-for-zero child, which is the actual broken-mutual-exclusion primitive. Also added a supplementary fix_test.c that exercises both the positive-op and SETVAL paths for use on the patched kernel.

Fix validation (Phase 8)

Authored fix.diff adding SEMVMX enforcement at all three write sites:

  1. positive-op (:849): if ((int)semptr->semval + sopptr->sem_op > seminfo.semvmx) { eval = ERANGE; lwkt_relpooltoken(semptr); goto done; } β€” matches the finding's proposed pattern and the existing negative-branch style.
  2. SETVAL (:530): if (real_arg.val < 0 || real_arg.val > seminfo.semvmx) { eval = ERANGE; break; } — also catches the int→u_short truncation case (e.g. val = 70000 would have stored 70000 & 0xffff = 4464).
  3. semexit (:1139): clamp to SEMVMX (semexit cannot fail; the process is exiting). Defense-in-depth β€” once semop and SETVAL are bounded, semexit's input state is always ≀ SEMVMX, so the clamp is a backstop.

Before/after (clean comparison on the same guest):

Kernel sem_wrap observation [1] (4Γ—+32767) sem_wrap observation [2] (wrap-to-0) fix_test SETVAL =SEMVMX+1
#0 unpatched baseline semval = 65532, no ERANGE (BUG) child RELEASED (broken mutex) n/a (would store 4464)
#1 patched (fix.diff) op2 returns ERANGE (FIXED) unreachable (op2 fails first) returns ERANGE (FIXED)

Patched kernel: DragonFly 6.5-DEVELOPMENT #1: Sun Jul 12 23:47:04 UTC 2026, sha256(/boot/kernel/kernel) = 425bca4973a730ceafb4cda4a71b64cd26e10c45d41e26eb55e26e171243619e. Smoke test (normal -1 op) still works (semval=32766), no regression.

fix.diff supersedes the finding markdown's proposal (which only patched the positive-op branch). The verified fix covers all three write sites (positive-op, SETVAL, semexit) for a complete bound enforcement. The finding markdown's ## Recommended fix only showed the positive-op hunk and noted "and the analogous check at SETVAL :530 and semexit :1139" β€” fix.diff implements exactly that, with semexit using a clamp (not ERANGE) since it cannot fail.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: baseline semval=65532 no ERANGE + wait-for-zero release; patched ERANGE at op2 + SETVAL; smoke -1 OK.

BEFORE #0: semval=65532 (>SEMVMX), no ERANGE, broken mutex. AFTER #1: ERANGE at op2, SETVAL ERANGE, -1 OK (no regression).
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Sun Jul 12 23:47:04 UTC 2026 (sha256 425bca4973a730ceafb4cda4a71b64cd26e10c45d41e26eb55e26e171243619e)

Confirmed kernel references

Detail

Exploit chain

none -- pure logic/integer bug. semval is self-contained u_short; no write primitive, no adjacent memory corruption, no slab grooming. IPC-state integrity / local-DoS.

Evidence (decisive lines)

baseline #0: semval=65532 (>SEMVMX=32767, no ERANGE); wait-for-zero child released after wrap-to-0 (broken mutex). patched #1: ERANGE at op2; SETVAL=SEMVMX+1 -> ERANGE; smoke -1 OK (semval=32766, no regression).

PoC changes

Rewrote sem_wrap.c: replaced flawed rollback claim with wrap-to-0 spurious wait-for-zero release demo. Added fix_test.c (positive-op + SETVAL SEMVMX bounds). Added build.sh, run.sh, VERDICT.md, manifest.json, fix.diff, full logs.

Verified recommended fix

fix.diff enforces SEMVMX at all three write sites: (1) semop positive-op at :849 (ERANGE if >SEMVMX); (2) SETVAL at :530 (ERANGE if >SEMVMX, also catches int->u_short trunc); (3) semexit at :1139 (clamp to SEMVMX). Supersedes finding markdown (adds SETVAL + semexit). Full git-apply-able diff in findings/poc/DF-0046/fix.diff.

Verdict

REPRODUCED. sysv_sem.c never enforces SEMVMX (32767) upper bound. Positive-op at :849 does semval += sem_op with no check. Four +32767 ops land at 65532 with no ERANGE. Security-relevant consequence: +32767 +32767 +2 wraps semval to 0, spuriously releasing a wait-for-zero child (broken mutual exclusion). No memory corruption.