⬢ DragonFlyBSD Kernel Audit
DF-3360 / disassembly-analysis.txt
← back to finding ↓ download raw
Frame layout of pf_normalize_tcpopt in the SHIPPED /boot/kernel/pf.ko
(DragonFly 6.5-DEVELOPMENT #0, INVARIANTS, gcc80 -O2):

  push rbp; mov rsp,rbp; push r15..rbx; sub $0x58,rsp
  opts[]            = rbp-0x58 .. rbp-0x30   (40 bytes, filled by pf_pull_hdr
                                              via `lea -0x58(%rbp),%rdx`)
  rbp-0x30..rbp-0x28 = 8-byte ALIGNMENT PADDING (no instruction in the
                       function references -0x30(%rbp))
  rbp-0x28..rbp      = saved rbx, r12, r13, r14, r15

The unbounded access is compiled in with no guard:
  270a5:  cmp $0x2,%edx            ; opt == TCPOPT_MAXSEG
  2708a:  movzbl 0x1(%r15),%r11d   ; optlen = optp[1]
  2709c:  cmp %ebx,%r11d ; jg break  ; optlen > cnt -> break (2 <= 2 passes)
  270aa:  movzwl 0x2(%r15),%esi    ; *mss   <-- OOB READ  when r15 = opts+38
  ...
  270f2:  mov %ax,0x2(%r15)        ; *mss = htons(max_mss)  <-- OOB WRITE

With optp == opts+38 (38 bytes of filler + MSS option with declared
length 2), mss == opts+40 == rbp-0x30: the 2-byte read/write lands in
the padding slot. On this exact binary the OOB is therefore inert
(reads stale stack padding; conditional write hits padding only).
Any compiler/optimization change that places a live local or spill at
rbp-0x30 (or opts at a different offset) turns this into a genuine
2-byte stack overwrite adjacent to saved registers.

Source-level reachability: cnt == 40 requires th_off == 15; the tile
[38 bytes filler][0x02 0x02] passes every check in the walk
(optlen=2 is >= 2 and <= cnt=2), so the case body executes with
mss one-past-the-end. Verified at runtime: 300k crafted packets through
`scrub in on lo0 all max-mss {64,1}` (rule evaluations climbed to
416k+, guest stable) -- the path runs on every packet; only the
frame layout prevents observable corruption in this build.