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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5679 Β· 15 files
FileTypeDescriptionSize
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:

  1. 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" β†’ 0xCBF43926 CRC-32, 0xE3069283 CRC-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 past p+length. No OOB read, no wrong CRC, no chaining hazard exists in the arithmetic.

  2. The one real length-math defect (Info) β€” the public API takes size_t size but calculate_crc32c() takes unsigned int length (sys/libkern/icrc32.c:763), so iscsi_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 to DMSG_AUX_MAX at kern_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) then FIX-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
VERDICT.md
↓ download raw

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

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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.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).
↓ fix.diffper-fix-DF-5679

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).