DF-0281 / idiv_comparison.txt
DF-0281 โ OBJECT-CODE-LEVEL PROOF: unguarded `idiv` (unpatched) vs guarded (patched)
====================================================================================
Both snippets are from `ng_btsocket_rfcomm_send_credits()` inlined into
`ng_btsocket_rfcomm_sessions_task` in the ACTUAL compiled `ng_btsocket.ko`
module built from /usr/src/sys/netgraph7/bluetooth/socket/ against the
audit-source kernel (DragonFly 6.5-DEVELOPMENT #0, Thu Jul 2 06:02:54 UTC 2026).
The divisor is `pcb->mtu` (loaded from offset 0x24 of the PCB, a `u_int16_t`).
In the source this is line 3283:
credits = ssb_space(&pcb->so->so_rcv) / pcb->mtu;
When `pcb->mtu == 0` (set by a peer PN MCC frame, line 3019, no validation),
the `idiv` raises #DE -> kernel panic.
UNPATCHED (vulnerable) โ `idiv %r8` at offset 0xb418, NO guard before it:
------------------------------------------------------------------------
b3df: movzwl 0x24(%r13),%edi ; edi = pcb->mtu (the divisor)
b3e4: test %edx,%edx ; (unrelated CFC flag check)
b3e6: jne b6bc
b3ec: mov 0xf0(%rcx),%rsi ; \ ssb_space(&pcb->so->so_rcv)
b3f3: mov 0xf8(%rcx),%rax ; > (the dividend)
b3fa: movzwl %di,%r8d ; / r8 = pcb->mtu (ZERO-EXTENDED = divisor)
b3fe: sub 0x90(%rcx),%rax
b405: sub 0x88(%rcx),%rsi
b40c: cmp %rax,%rsi
b40f: cmovg %rax,%rsi
b413: mov %rsi,%rax ; rax = dividend
b416: cqto ; sign-extend rax into rdx:rax
b418: idiv %r8 ; *** DIVIDE BY pcb->mtu โ NO GUARD ***
; *** if r8==0 -> #DE -> kernel panic ***
b41b: mov %eax,%r9d ; r9d = credits
NOTE: there is NO `test %r8d,%r8d; je` or any zero-check on the divisor
between loading pcb->mtu (b3df/b3fa) and the `idiv` (b418). The divide is
unconditional. This is the compiled form of the unguarded line 3283.
PATCHED (fix.diff applied) โ `idiv %rdi` at offset 0xb4b2, GUARDED by `test; je`:
--------------------------------------------------------------------------------
b46b: movzwl 0x24(%r13),%ecx ; ecx = pcb->mtu (the divisor)
b470: test %cx,%cx ; *** THE GUARD: is pcb->mtu == 0? ***
b473: je b7d3 ; *** YES -> jump to clean return (b7d3),
; SKIPPING the divide entirely ***
b479: mov 0x0(%r13),%rsi ; only reached when pcb->mtu != 0
b47d: testb $0x4,0xe1(%rsi)
b484: jne b5b4
b48a: mov 0xf0(%rsi),%rax ; \ ssb_space(...) dividend
b491: mov 0xf8(%rsi),%rdx ; >
b498: movzwl %cx,%edi ; / edi = pcb->mtu (KNOWN NON-ZERO here)
b49b: sub 0x90(%rsi),%rdx
b4a2: sub 0x88(%rsi),%rax
b4a9: cmp %rdx,%rax
b4ac: cmovg %rdx,%rax
b4b0: cqto
b4b2: idiv %rdi ; *** DIVIDE โ only reached if pcb->mtu != 0 ***
b4b5: mov %eax,%r9d ; r9d = credits
...
b7d3: mov 0xb8(%rbx),%eax ; <-- je target: clean return path
b7d9: jmpq ab8f ; (function epilogue / next iteration)
The `test %cx,%cx; je b7d3` at b470/b473 is exactly the compiler's emission of
the fix's `if (pcb->mtu == 0) return (EINVAL);` guard. When pcb->mtu == 0,
execution jumps past the `idiv` to b7d3 (clean return). The divide-by-zero is
now impossible.
CONCLUSION: fix.diff closes the bug at the object-code level. The unguarded
`idiv %r8` (unpatched) becomes a guarded `idiv %rdi` preceded by
`test %cx,%cx; je <return>` (patched).