Skip to content

feat(kernel): validate DHCP messages arriving on the client port - #185

Merged
kernalix7 merged 1 commit into
mainfrom
feat/dhcp-validate-wire
Aug 15, 2026
Merged

feat(kernel): validate DHCP messages arriving on the client port#185
kernalix7 merged 1 commit into
mainfrom
feat/dhcp-validate-wire

Conversation

@kernalix7

Copy link
Copy Markdown
Owner

Audit of crates/kernel/src/dhcp.rs plus wiring into the UDP receive path, per the crates/kernel/AGENTS.md "audit then wire one protocol per PR" rule.

Audit result

No overflow/underflow/out-of-bounds/unbounded-loop defect. The options TLV walker and the RFC 2132 §9.3 overload (file/sname) regions are all saturating-arithmetic and length-bounded, with END/PAD handled and non-recursive overload. Two validation-policy gaps remained, and the client was unreachable from the receive path.

Fix 1 — htype/hlen validation in process_response

The BOOTP htype/hlen fields declare the link-layer address model that determines how chaddr is read. RFC 2131 §2 fixes them at 1/6 for Ethernet; any other value means the hardware address is not the 6-byte MAC this client binds the lease to. They were accepted unchecked. TDD: rejects_non_ethernet_htype / rejects_wrong_hlen failed before, pass after; accepts_valid_ethernet_offer guards the legit path.

Fix 2 — dhcp::validate_message framing gate

New framing-only validator: fixed-header length + magic cookie, real op (either direction), Ethernet htype/hlen, cookie, and an options field that is bounded and terminated by END (RFC 2131 §4.1). The shared walker now reports whether it saw END; the lenient public parse_dhcp_options keeps tolerating a missing END while the strict gate requires it.

Wiring

handle_udp_packet validates the payload after checksum for src_port == 67 && dst_port == 68. DHCP is directional, so the exact pair (not || as with shared DNS port 53) is the correct client-inbound predicate. Malformed frames drop via the existing Ok(0) convention. No DhcpClient state / lease / xid-correlation / query TX — that needs a CSPRNG (random.rs is explicitly not one) and an interface/route model, so it is a separate PR. Note: broadcast offers are still blocked by handle_ipv4's dst == local_ip; this PR is parser wiring, not a live exchange.

Tests

  • RED-first htype/hlen: 2 fail before, pass after.
  • validate_message: discover/offer accepted; truncated header, bad cookie, non-Ethernet htype, missing END rejected.
  • Net-layer: valid OFFER on 67→68 consumed; bad cookie dropped; DHCP-shaped bytes on wrong ports bypass the gate (guards &&).
  • Pre-existing test_process_response_nak had an incomplete fixture (htype/hlen unset); corrected, not deleted.

Verification

  • cargo fmt --all -- --check · cargo clippy --workspace -- -D warnings · cargo build --workspace
  • cargo build -p oncrix-kernel --bin oncrix-kernel --target aarch64-unknown-none / riscv64gc-unknown-none-elf
  • bash scripts/run-tests.sh4619 passed, 0 skipped (kernel 367 → 385)
  • bash scripts/run-qemu-test.sh — boots clean on all three architectures

Audit of `dhcp.rs` ahead of wiring it into the UDP receive path, plus the
wiring, in one PR per the "one protocol per PR" convention.

Audit result: no overflow/underflow/out-of-bounds/unbounded-loop defect. The
options TLV walker and the overload (`file`/`sname`) regions are all
saturating-arithmetic and length-bounded. Two validation-policy gaps remained,
and the client was not reachable from the receive path at all.

Fixes:
- `DhcpClient::process_response` now validates the BOOTP `htype`/`hlen` fields.
  Those fields declare the link-layer address model that determines how
  `chaddr` is read; RFC 2131 §2 fixes them at 1/6 for Ethernet, and any other
  value means the hardware address is not the 6-byte MAC this client binds the
  lease to. Previously they were accepted unchecked.
- New `dhcp::validate_message`, a framing-only admission gate: fixed-header
  length plus magic cookie, a real op (either direction), the Ethernet
  htype/hlen invariant, the cookie, and an options field that is both bounded
  and terminated by `END` (RFC 2131 §4.1 requires END as the last option). The
  shared walker now reports whether it saw `END`; the lenient public
  `parse_dhcp_options` keeps tolerating a missing END, while the strict
  receive-path gate requires it.

Wiring: `handle_udp_packet` keeps the UDP header and, for server-to-client
traffic (`src_port == 67 && dst_port == 68`), validates the payload after the
checksum. DHCP is directional, so the exact port pair — not `||` as with the
shared DNS port — is the correct client-inbound predicate. Malformed frames
drop silently via the existing `Ok(0)` convention. No `DhcpClient` state,
lease, transaction-ID correlation, or query transmission is introduced; that
stateful integration needs a CSPRNG (which `random.rs` is explicitly not) and
an interface/route model, so it is a separate PR.

Tests: RED-first — `rejects_non_ethernet_htype` and `rejects_wrong_hlen`
failed before the fix and pass after; `accepts_valid_ethernet_offer` guards the
legitimate path. `validate_message` accepts a real discover/offer and rejects
truncated header, bad cookie, non-Ethernet htype, and a missing END. Net-layer:
a valid OFFER on 67→68 is consumed, a bad-cookie datagram is dropped, and
DHCP-shaped bytes on non-DHCP ports bypass the gate entirely (guards the `&&`
predicate). The pre-existing `test_process_response_nak` used an incomplete
fixture (htype/hlen unset); the fixture is corrected to a well-formed frame,
not deleted.

Suite: 4619 passed, 0 skipped. QEMU boot passes on x86_64, aarch64, riscv64.
@kernalix7
kernalix7 merged commit 728213e into main Aug 15, 2026
3 checks passed
@kernalix7
kernalix7 deleted the feat/dhcp-validate-wire branch August 15, 2026 03:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant