DragonFly's Linux-compat seqlock shim returns the seqcount to its pre-write value β drm_vblank_count_and_time() accepts torn {count,time} vblank snapshots (protocol violation)
Summary
SYSTEMIC: the DF seqlock shim implements write_sequnlock as sequence-- (0->1->0) instead of Linux's monotonic advance, and read_seqbegin does not wait for even. A reader that begins before a writer and re-checks after the writer COMPLETES sees sequence == start and accepts a torn snapshot - exactly the race seqcounts exist to detect. The vblank reader reads the u64 count and ktime between begin/retry while store_vblank writes both; torn pairs flow to userspace via the unauthenticated vblank UAPI (wrong sequence/tval, mismatched flip timestamps). i915 engine stats share the broken shim. Guest harness with the shim's algorithm verbatim: deterministic torn-pair-accepted + 4057/4189 torn snapshots under 2-thread stress; fixed semantics -> 0. Restore-Linux-semantics fix in the shim (safe global correction).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4197 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.4 KB | β raw | |
| VERDICT.md | β | 4.8 KB | β raw | |
| seqlock_aba.c | β | 6.4 KB | view raw | |
| build.sh | β | 105 B | view raw | |
| run.sh | β | 178 B | view raw | |
| build.log | β | 399 B | view raw | |
| run.log | β | 1.9 KB | view raw | |
| env.txt | β | 402 B | view raw | |
| fix.diff | β | 901 B | view raw | |
| manifest.json | β | 1.0 KB | view raw | |
| verdict.json | β | 3.9 KB | view raw |
DF-4197 β DragonFly's Linux-compat seqlock shim violates the seqcount protocol β torn vblank {count,time} snapshots accepted (drm_vblank_count_and_time)
File under audit: sys/dev/drm/drm_vblank.c (pass 2)
What
sys/dev/drm/include/linux/seqlock.h implements the Linux seqlock API with
broken seqcount semantics:
write_seqlock() { spin_lock; sl->sequence++; sfence; } /* 0 -> 1 */
write_sequnlock() { sl->sequence--; spin_unlock; sfence; }/* 1 -> 0 β DECREMENT */
read_seqbegin() { return READ_ONCE(sl->sequence); } /* no wait-even */
read_seqretry() { lfence; return sl->sequence != start; }
A Linux seqcount advances monotonically (each write section bumps the
count twice: 0β1β2β4ββ¦), so any writer that started after a reader's
read_seqcount_begin() changes the count and forces a retry. The DF shim
returns the count to its original value (0β1β0). A reader that starts
before a writer and re-checks after that writer completes observes
sequence == start and accepts a torn snapshot β precisely the race the
seqcount protocol exists to detect.
The in-kernel consumer under audit is the vblank counter/timestamp pair:
- writer:
store_vblank()β drm_vblank.c:92-106 (write_seqlock; vblank->time = t; vblank->count += inc; write_sequnlock), - reader:
drm_vblank_count_and_time()β drm_vblank.c:784-788 (do { seq = read_seqbegin; count = vblank->count; time = vblank->time; } while (read_seqretry)).
A torn {count,time} pair is handed to userspace by
drm_wait_vblank_ioctl (reply.sequence/tval), drm_crtc_get_sequence_ioctl
(:1801-1802) and drm_crtc_queue_sequence_ioctl (:1861, event target
sequence), and used internally by drm_crtc_send_vblank_event and page-flip
completion timestamps. Result: wrong vblank sequence numbers/timestamps
(event delivered against a mismatched counter, waits satisfied early/never)
β logic corruption of the UAPI vblank contract, no memory unsafety. On i386
builds the unlocked u64 counter reads (drm_vblank_count, :277-285) can
tear independently of the seqlock.
Second in-tree consumer of the same shim (same loss of guarantee):
i915 engine stats (intel_engine_cs.c:1623 write_seqlock cycles).
How it is proven here
seqlock_aba.c (guest, DragonFly cc 8.3, -O2):
demoβ deterministic single-thread interleave of the exact shim functions (verbatim semantics from seqlock.h:69-99): reader begins (seq=0), loads OLD count, writer runs a COMPLETE write section (0β1β0), reader loads NEW time,read_seqretryreturns 0 β TORN PAIR {count=100(old), time=200000(new)} ACCEPTED.demo_fixedβ identical interleave with corrected (Linux monotonic) semantics: sequence ends at 2, retry fires, torn pair rejected.stressβ real 2-thread contention, 4M write cycles: DF semantics: torn_accepted=4057/4189 (two runs); fixed semantics: torn_accepted=0.
Reproduce
cc -O2 -Wall -pthread -o seqlock_aba seqlock_aba.c ./seqlock_aba demo # TORN PAIR ACCEPTED, rc=1 ./seqlock_aba demo_fixed # torn rejected, rc=0 ./seqlock_aba stress # torn_accepted=4057, rc=1 ./seqlock_aba stress_fixed # torn_accepted=0, rc=0
Scope
Kernel-level manifestation needs a DRM device (guest has none β hardware
gated), but the shim defect itself is proven exactly: the harness copies the
shim's algorithm 1:1. The fix (fix.diff) restores Linux seqcount semantics
in sys/dev/drm/include/linux/seqlock.h.
DF-4197 VERDICT
Status: inconclusive (kernel manifestation hardware-gated: no DRM device in guest) β the shim protocol violation itself is proven exactly and deterministically on the DragonFly guest, including under 2-thread stress (4057 torn snapshots accepted under DF semantics, 0 under fixed semantics). Impact: corrupted vblank counter/timestamp pairs delivered through the DRM UAPI (logic corruption; no memory unsafety, no info leak of kernel memory β both torn values are legitimate vblank values). uid=0 chain N/A.
Root cause (path:line)
sys/dev/drm/include/linux/seqlock.h:77-83βwrite_sequnlock()doessl->sequence--, returning the seqcount to its pre-write value (0β1β0 per write section). Linux'swrite_seqcount_end()increments (0β1β2), making the count change visible forever after.sys/dev/drm/include/linux/seqlock.h:90-94βread_seqbegin()is a plain load; it neither waits for the count to become even nor otherwise prevents beginning a read against a count that a completed writer has "restored".
Consequence for the audited file:
sys/dev/drm/drm_vblank.c:784-788β reader of {count,time}; a writer (store_vblank, drm_vblank.c:102-105) completing entirely inside the reader's two field loads leavessequence == startβ no retry β torn pair accepted.- Consumers: drm_vblank.c:1518 (
drm_wait_vblank_reply), :1801-1802 (drm_crtc_get_sequence_ioctl), :1861/:1899 (queue_sequence), drm_crtc_send_vblank_event (:913), drm_crtc_arm_vblank_event (:888). - On i386 the u64
countitself can tear (two 32-bit halves); on amd64 each field is atomic but the pair is not.
Also affected (same shim, out-of-file consumer, mentioned for blast radius): i915 engine stats write_seqlock (sys/dev/drm/i915/intel_engine_cs.c:1623).
Proof
Deterministic demo (verbatim shim algorithm; writer completes between the reader's two loads β the canonical seqcount race):
$ ./seqlock_aba demo
[demo] semantics=DF-shim
[demo] read_seqbegin -> 0; reader loaded count=100 (preempted here...)
[demo] writer ran a FULL cycle: count=101 time=200000, sequence=0
[demo] reader resumed: loaded time=200000, read_seqretry=0
[demo] RESULT: TORN PAIR ACCEPTED {count=100(old), time=200000(new)} β reader believes it is consistent
rc=1
$ ./seqlock_aba demo_fixed
[demo] writer ran a FULL cycle: count=101 time=200000, sequence=2
[demo] RESULT: retry=1 β torn pair REJECTED, reader retries
rc=0
Stress (2 threads, 4M writer cycles, invariant time == count*1000):
[stress] semantics=DF-shim consistent=8910351 torn_accepted=4189 [stress] semantics=FIXED(Linux) consistent=6953440 torn_accepted=0
(earlier independent run: 7767673/4057 and 6980918/0 β see run.log)
Both semantics variants live in the same binary; *_fixed compiles the
corrected write_sequnlock (sequence++) and read_seqbegin (wait-even), i.e.
exactly what fix.diff applies to the shim header.
Why Low severity
The torn values are still real vblank counts/timestamps (no kernel-memory disclosure), and the practical effect is limited to wrong sequence numbers / timestamps / event-target computations on the vblank UAPI β visible as compositor glitches or early/missed vblank waits. No memory-safety consequence found on any consumer of count/time.
Fix validation
fix.diff (sys/dev/drm/include/linux/seqlock.h): write_sequnlock advances
the count (sequence++) and read_seqbegin spins until the count is even β
restoring Linux read_seqcount_begin/write_seqcount_end semantics.
Harness-level validation: demo_fixed/stress_fixed reject every torn
snapshot (0 accepted over ~7M reads). A full DRM kernel build/boot is not
possible on this guest (no GPU), so kernel-level validation is
fix_status: inconclusive. All in-tree users (drm_vblank store/read,
i915 engine stats) use the API in the Linux-expected way, so the semantic
restoration is safe for them.
Killed negatives (adjacent code audited)
store_vblankwriter discipline: writers are serialized by the embedded real spinlock AND by vblank_time_lock (assert_spin_locked, :98) β writer vs writer is fine; only the reader protocol is broken.- seqcount_t section of the same header (__read_seqcount_begin waits for even) is correct β only the seqlock_t half is broken.
- drm_vblank_count (:277-285) unlocked read: upstream-identical; on amd64 practically atomic; noted here as an i386-only aggravator of this same finding rather than a separate one.
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
inconclusivefix.diff restores monotonic seqcount semantics (write_sequnlock increments; read_seqbegin waits for even β identical to the *_fixed harness variant). Harness A/B: demo_fixed rejects the torn pair (retry=1) and stress_fixed accepts 0 torn pairs over ~7M reads vs 4057/4189 baseline. Kernel-level validation impossible on this guest (no DRM device); harness-level validation passed cleanly.
run.log stress lines 'DF-shim ... torn_accepted=4057/4189' vs 'FIXED(Linux) ... torn_accepted=0'; fix.diff
Confirmed kernel references
Detail
Evidence (decisive lines)
run.log: demo 'TORN PAIR ACCEPTED' rc=1 / demo_fixed 'REJECTED' rc=0; stress torn_accepted=4057 and 4189 (two runs) vs stress_fixed torn_accepted=0; VERDICT.md verbatim shim-vs-Linux semantics comparison
PoC changes
harness written fresh (no seed): verbatim shim algorithm from seqlock.h:69-99 (pthread_mutex stands in for struct spinlock; sfence/lfence kept), verbatim store_vblank/drm_vblank_count_and_time bodies, plus a fixed-semantics variant compiled into the same binary for A/B contrast
Verified recommended fix
Restore Linux seqcount semantics in the shim: write_sequnlock -> sl->sequence++ and read_seqbegin -> spin until even (see fix.diff)
Verdict
Kernel manifestation is hardware-gated on this guest (no DRM device), but the defect itself is the shim, and the shim's exact algorithm is reproduced deterministically and under 2-thread stress on the DragonFly guest: sys/dev/drm/include/linux/seqlock.h write_sequnlock() decrements the sequence count back to its pre-write value (0->1->0) instead of advancing it monotonically (Linux: 0->1->2) and read_seqbegin() does not wait for an even count, so drm_vblank_count_and_time (drm_vblank.c:784-788) accepts a torn {count,time} pair whenever exactly one store_vblank (drm_vblank.c:92-106) completes inside its two field loads. Demo: TORN PAIR ACCEPTED {count=100(old), time=200000(new)} with read_seqretry==0 under DF semantics vs retry==1 under corrected semantics; stress: torn_accepted=4057/4189 across two runs under DF semantics vs 0 under fixed semantics (~8M reads each). Torn values are legitimate vblank counters (no kernel-memory disclosure) but corrupt the vblank UAPI: wrong reply.sequence/tval (drm_wait_vblank_reply:1518), wrong DRM_IOCTL_CRTC_GET_SEQUENCE output (:1801), wrong event target sequence in DRM_IOCTL_CRTC_QUEUE_SEQUENCE (:1861), mismatched flip-completion timestamps. i915 engine stats (intel_engine_cs.c:1623) share the broken shim.
No comments yet.