iscsi_crc32()/iscsi_crc32_ext() silently truncate size_t length to unsigned int β data-integrity contract violation for >=4GiB requests
Summary
The public API takes size_t but the length narrows to unsigned int at calculate_crc32c and again in multitable_crc32c: a caller requesting CRC coverage of 0x100000009 bytes gets the CRC of 9 bytes silently - the tail beyond 4GiB unchecked. The sibling crc32/crc32_ext honor size_t end-to-end, making the inconsistency an API trap. Bounds audit of all in-tree callers shows none can reach 2^32 today (dmsg aux clamped, swap <=MAXPHYS, hammer2 <=64KB radix, ext2fs block-sized) - correctness-contract hazard for hammer2/dmsg/swap integrity code. Demonstrated on the byte-identical source (truncation return observed); size_t-end-to-end fix validated (guard-page probe faults, truncation gone). Everything else machine-verified: 21/21 differential checks (all lengths x alignments x 4 engines vs bitwise refs, KATs), all 9 tables element-wise correct, zero guard-page faults over 256 boundary lengths, chaining/inversion protocol sound, pure functions with const tables (no init-once race).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5679 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| icrc32_diff.c | β | 10.9 KB | view raw | |
| fixdemo.c | β | 1.7 KB | view raw | |
| fix.diff | β | 1.7 KB | view raw | |
| build.sh | β | 187 B | view raw | |
| run.sh | β | 90 B | view raw | |
| build.log | β | 1.1 KB | view raw | |
| run.log | β | 680 B | view raw | |
| run.2.log | β | 286 B | view raw | |
| fix_build.log | β | 703 B | view raw | |
| fix_run.log | β | 239 B | view raw | |
| env.txt | β | 310 B | view raw | |
| README.md | β | 3.7 KB | β raw | |
| VERDICT.md | β | 7.0 KB | β raw | |
| manifest.json | β | 3.5 KB | view raw | |
| verdict.json | β | 4.7 KB | view raw |
DF-5679 β sys/libkern/icrc32.c pass-2 evidence pack
File: sys/libkern/icrc32.c (786 LOC) β classic CRC-32 table export
(crc32_tab, shared with sys/libkern/crc32.c) + CRC-32C single-table and
sliced-by-8 engines + calculate_crc32c / iscsi_crc32 / iscsi_crc32_ext
public API.
Consumers established (attack surface for length math):
hammer2 (hammer2_icrc32/icrc32c, sys/vfs/hammer2/hammer2.h:1457),
hammer1 vol_versionβ₯7 (sys/vfs/hammer/hammer_crc.h:73-77), swap-pager
write-verify (sys/vm/swap_pager.c:1876), dmsg wire protocol hdr/aux CRC
(sys/kern/kern_dmsg.c:2009-2012), ext2fs metadata CRC (seeded, incremental,
sys/vfs/ext2fs/ext2_csum.c), namei/vmount hashes, crc32()/crc32_ext()
consumed by disklabel64 and older HAMMER volumes.
This pack proves two things:
-
Negative result with weight β the primitives are bit-exact:
icrc32_diff(userland, compiles the pristine kernel sources) passes 21/21 checks: all 9 tables verified element-wise against bitwise- generated references (poly 0xEDB88320 / 0x82F63B78, sliced tables via the iterative slicing construction), KATs ("123456789"β0xCBF43926CRC-32,0xE3069283CRC-32C; empty β 0), functional equivalence of both engines for every length 0..600 Γ buffer alignment 0..3, incremental-chaining semantics (incl. by-1/by-3 chunk churn), a 1 MiB random multitable-vs-bitwise comparison, and a PROT_NONE guard-page probe (256 boundary lengths Γ 3 entry points) that faults on any read pastp+length. No OOB read, no wrong CRC, no chaining hazard exists in the arithmetic. -
The one real length-math defect (Info) β the public API takes
size_t sizebutcalculate_crc32c()takesunsigned int length(sys/libkern/icrc32.c:763), soiscsi_crc32(buf, 0x100000009ULL)silently computes the CRC of 9 bytes (T10 in the harness;fixdemo_baseline). Every in-tree caller is bounded (dmsg aux is clamped toDMSG_AUX_MAXatkern_dmsg.c:371-376, swap β€ MAXPHYS, hammer2 β€ 64 KB radix), so this is a correctness-contract hazard / defense-in-depth item, not a reachable bug today.
Reproduce
On the host (guest already up):
scp -F dfbsd-qemu/config -r findings/poc/DF-5679 dfbsd-maxx:/tmp/
dfbsd-qemu/vm.sh run_user 'sh /tmp/DF-5679/build.sh' # build.log
dfbsd-qemu/vm.sh run_user 'sh /tmp/DF-5679/run.sh' # run.log
build.sh copies /usr/src/sys/libkern/{icrc32,crc32}.c from the guest
tree (md5 29e2ead0a146caec695a6804944e3cde /
7bb201c97ceb81fec180aaf9e9a9e445 β identical to the audited tree), builds
icrc32_diff, fixdemo_baseline, and fixdemo_patched (the latter from a
copy with fix.diff applied). run.sh runs all three.
Expected output
RESULT: ALL-PASS (21 checks, 0 failures)β primitives bit-exact, no OOB.T10 ... TRUNCATED (equal)β the filed defect, demonstrated.FIX-BAD ... truncation still present(baseline, exit 1) thenFIX-OK: length honored -> child faulted on guard page (sig 11)(patched, exit 0) β fix validated.
Artifacts
| file | what |
|---|---|
icrc32_diff.c |
differential/KAT/guard/truncation harness (code under test included verbatim) |
fixdemo.c |
fix-validation demo (baseline vs patched, guard-page fork test) |
fix.diff |
size_t end-to-end fix (icrc32.c + systm.h prototype), git-apply-able, never applied to sys/ |
build.sh / run.sh |
exact runnable commands |
build.log / run.log / run.2.log |
full untrimmed compiler + runtime output |
fix_build.log / fix_run.log |
fix-validation build + run |
env.txt |
guest uname, compiler, md5 of code under test |
VERDICT.md |
full narrative |
manifest.json / verdict.json |
machine-readable catalog / verdict |
DF-5679 β VERDICT (pass 2, GLM 5.3)
Target: sys/libkern/icrc32.c β CRC-32/CRC-32C data-integrity primitives.
Mode: audit pass 2 with self-verification. Guest: DragonFly 6.5-DEVELOPMENT
(#0 Thu Jul 2 2026, X86_64_GENERIC); /usr/src/sys/libkern/icrc32.c md5-identical
to the audited tree (29e2ead0a146caec695a6804944e3cde). All evidence in this
pack was produced on that guest with its own cc 8.3.
Bottom line
The file's arithmetic is bit-exact and memory-safe; the only defect found is a
size_tβunsigned int length truncation in the public API
(sys/libkern/icrc32.c:776-786 wrappers β :760-770 implementation), demonstrated
at runtime (T10 / fixdemo_baseline), unreachable by every in-tree caller
today (bounds audit below), and fixed and fix-validated (fix.diff /
fixdemo_patched). Severity: Info (hardening / correctness-contract). Everything
else the checklist asked for was hunted and killed with machine-checked evidence.
What was verified and how
(a) Tables / polynomial constants β VERIFIED ELEMENT-WISE
All 256 entries of crc32_tab (poly 0xEDB88320, icrc32.c:58-102) and of
crc32Table (poly 0x82F63B78 = reflected 0x1EDC6F41, icrc32.c:144-209) match
tables generated bitwise in the harness. sctp_crc_tableil8_o32 ==
crc32Table, and the seven higher sliced tables (o40..o88,
icrc32.c:306-676) each match the canonical iterative slicing construction
T_{k+1}[i] = (T_k[i] >> 8) ^ T_0[T_k[i] & 0xff]. There is no runtime
table initialization at all (compile-time const in .rodata), so the
"init once protocol" race class is structurally impossible. Check: T1..T4
PASS.
(b) Update/final arithmetic β DIFFERENTIAL-TESTED
For every length 0..600 Γ every buffer alignment 0..3 (random data, fixed
seed): iscsi_crc32 == bitwise reference (init/xorout ~0), calculate_crc32c
== bitwise raw, singletable_crc32c == bitwise raw (single-table engine tested
at all lengths, not just the length < 4 gate at icrc32.c:765), and
crc32/crc32_ext (from sys/libkern/crc32.c, which links against
crc32_tab from this file) == bitwise reference. Known-answer vectors:
"123456789" β 0xCBF43926 (CRC-32) / 0xE3069283 (CRC-32C); empty β 0.
A 1 MiB random buffer exercises the full sliced-by-8 loop. Checks T5,T6,T8
PASS β no silent-corruption divergence exists between the single-table,
multi-table, and bitwise implementations.
Boundary/OOB analysis (killed): multitable_crc32c (icrc32.c:750)
computes init_bytes = 4 - (buf & 3) β {1,2,3,4}; it is only reachable with
length β₯ 4 (icrc32.c:765-769), so length - init_bytes never underflows
and init + running + end == length exactly. After the init loop the pointer
is 4-byte aligned, so the *(const uint32_t *)p_buf loads (icrc32.c:707,723,726)
are aligned (and benign anyway on x86). A PROT_NONE guard-page probe with data
ending exactly at an unmapped page for 256 boundary lengths (covering every
L mod 8 Γ effective alignment) across iscsi_crc32, calculate_crc32c, and
crc32 produced zero faults (T9 PASS). The big-endian byte-reconstruction
branch (icrc32.c:701-705, 717-721) was verified to be the LE-word equivalent
(moot: DragonFly is x86-only).
(c) Consumers (who feeds lengths) β ALL BOUNDED
sys/kern/kern_dmsg.c:2009-2012βaux_crc/hdr_crc; receive side clampsaux_size > DMSG_AUX_MAXβEINVAL(kern_dmsg.c:371-376);hdr_sizeis protocol-bounded.sys/vm/swap_pager.c:1876βbp->b_bcountβ€ MAXPHYS.sys/vfs/hammer2/*βchain->bytesis radix-derived, β€ 64 KB (hammer2_chain.c:5391,5416,5538,5584,5597); volume-header CRCs are over fixed constants (hammer2_ondisk.c:530-550); filename CRCs bounded by PSTR_LEN (hammer2_subr.c:197-216).sys/vfs/hammer/hammer_crc.h:73-77β buffer sizes β€ buffer I/O limits.sys/vfs/ext2fs/ext2_csum.cβ block/group-descriptor-sized, seeded incremental raw chaining consistent with the non-inverting contract.sys/kern/vfs_cache.c:3881-3883,sys/vm/vm_page.c:1581,sys/kern/vfs_mount.c:457βsizeof/hash inputs.
None can reach 2^32 bytes, so the truncation below cannot be triggered
in-tree today.
(d) State reuse β NONE POSSIBLE
All functions are pure: no mutable globals, no context struct, no lazy-init
flags; the only writable is the local crc. Reentrant/SMP-safe by
construction. Inversion protocol verified: iscsi_crc32_ext(b,l,0) ==
iscsi_crc32(b,l), chained ext(ext(0,a),b) == one-shot over a||b, and
same for crc32_ext and raw calculate_crc32c chaining, including by-1 and
by-3 chunk churn across the length<4 engine boundary (T7 PASS β after I
fixed a double-processing bug in my own harness's by-3 loop, not in the
kernel code).
The defect that was filed (DF-5679, Info)
iscsi_crc32/iscsi_crc32_ext accept size_t size
(sys/sys/systm.h:205-206) but the length narrows to unsigned int at
calculate_crc32c (icrc32.c:763) and again inside
crc32c_sb8_64_bit/multitable_crc32c (icrc32.c:686,743). A request for
0x100000009 bytes is silently serviced as 9 β an integrity contract
violation (data past the first 4 GiB would be unchecked), not an overflow:
the code under-reads. Contrast crc32()/crc32_ext() in
sys/libkern/crc32.c:114-142 which honor size_t end-to-end.
Runtime proof (reproduced, deterministic, guest):
iscsi_crc32(buf, 0x100000009ULL) == iscsi_crc32(buf, 9) == 4520c30c
(T10; fixdemo_baseline prints the silent return). Impact: none reachable
today (bounds audit above) β Info, certain (the truncation is type-system
provable and machine-demonstrated).
Fix validation: fix.diff propagates size_t through
calculate_crc32c/multitable_crc32c/crc32c_sb8_64_bit and the two
prototype sites (icrc32.c:15-16 userland, sys/sys/systm.h:207). Applied
to a copy and rebuilt: baseline fixdemo exits 1 with the silent truncation
(fix_run.log), patched fixdemo child faults on the guard page
(SIGSEGV) because the full 0x100000009 length is now honored
(FIX-OK, exit 0). Bad behavior gone β fix_status: fixed. A kernel
rebuild validation was not performed (userland rig; no memory-corruption
finding, so the mandatory kernel-rebuild rule does not apply).
Honest accounting
Three rig iterations were needed (attempts=3): (1) first build exposed a
missing hunk in my fix.diff (userland prototype at icrc32.c:15-16) and a
-Wparentheses precedence bug in one harness CHECK; (2) first run's single
T7 failure was traced to my harness double-processing the last 3-byte chunk
(kernel exonerated); (3) clean ALL-PASS. Guest left up, /tmp-only
artifacts, no panic, guest_dirty=0.
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 (size_t end-to-end, 4+1 hunks) applied to a copy of the pristine source and rebuilt on the guest: baseline fixdemo silently returns the 9-byte CRC (truncation present, exit 1); patched fixdemo honors the full 0x100000009 length and the child faults on the PROT_NONE guard page (SIGSEGV, FIX-OK, exit 0). The silently-truncated behavior is gone. Kernel-rebuild validation not performed: userland rig, non-memory-corruption Info finding (mandatory kernel rebuild rule applies to memory-corruption findings only).
fix_run.log (baseline FIX-BAD/exit 1 vs patched FIX-OK sig 11/exit 0); fix_build.log (patch -p1 hunks 1-4 + systm.h hunk applied, both builds clean); fix.diff (sha256 fed7a77746635dc8bf11fd3a3301d51fee995ff6db31537dd7ad68796a029027).
Confirmed kernel references
Detail
Evidence (decisive lines)
run.log: 'T10 truncation: iscsi_crc32(buf, 0x100000009)=4520c30c iscsi_crc32(buf, 9)=4520c30c TRUNCATED (equal)' and 'RESULT: ALL-PASS (21 checks, 0 failures)'; fix_run.log: baseline 'FIX-BAD ... truncation still present' (exit 1) vs patched 'FIX-OK: length honored -> child faulted on guard page (sig 11)' (exit 0); VERDICT.md for the full class-by-class kill list with path:line.
PoC changes
No seed existed (orchestrator reserved IDs only); rig authored from scratch. Three iterations: (1) fix.diff was missing the #ifndef _KERNEL prototype hunk at icrc32.c:15-16 (patched-copy build failed with conflicting types) and one harness CHECK had a ^/== precedence bug caught by -Wparentheses; (2) first run's single T7 failure was my harness double-processing the final 3-byte chunk (228=76*3 leaves no remainder) - kernel exonerated; (3) clean ALL-PASS run + fix validation.
Verified recommended fix
Propagate size_t end-to-end: calculate_crc32c/multitable_crc32c/crc32c_sb8_64_bit length params and the icrc32.c:15 + sys/sys/systm.h:207 prototypes take size_t (see fix.diff); keeps parity with crc32()/crc32_ext() which already honor size_t.
Verdict
iscsi_crc32()/iscsi_crc32_ext() accept size_t size but calculate_crc32c() narrows it to unsigned int (sys/libkern/icrc32.c:763, called at :779/:785), so a 0x100000009-byte request is silently serviced as 9 bytes: demonstrated on the guest (T10 and fixdemo_baseline: identical CRCs 4520c30c, silent return). This is an under-read (unchecked tail), not an overflow. Bounds audit of every in-tree caller (kern_dmsg DMSG_AUX_MAX clamp at kern_dmsg.c:371-376, swap_pager MAXPHYS, hammer2 64KB radix cap, ext2fs block sizes, sizeof-based hashes) shows no caller can reach 2^32, so impact is a correctness-contract / defense-in-depth hazard only. The rest of the file is proven bit-exact and memory-safe by the same rig: 21/21 differential checks (all 9 tables element-wise vs generated references, KATs 0xCBF43926/0xE3069283, every length 0..600 x alignment 0..3 for both engines, chaining semantics incl. by-1/by-3 chunk churn, 1MiB random, PROT_NONE guard-page OOB probe with zero faults across 256 boundary lengths).
No comments yet.