DF-3481 / verdict.json
{ "finding_id": "DF-3481", "status": "not_reproduced", "reproduced": 0, "impact": "none", "confidence": "likely", "verdict": "v1 ng_ppp (LIVE stack, LEGACY bucket) guards NONE of its private state (no lock/token/BGL anywhere in ng_ppp.c): the frags TAILQ, qlen, mseq/xseq, activeLinks and conf are mutated by ng_ppp_mp_input/frag_process/get_packet/frag_trim/frag_checkstale/frag_reset/rmnode without any serialization, and v1 (unlike netgraph7s cpu0 item funnel, DF-0330) has structurally concurrent execution contexts: cpu0-netisr control plane (ngc_send, uipc_socket.c:259 port), cpuN-netisr ksocket upcalls (inline in sowakeup, uipc_socket2.c:604), the BGL-held 0.5s fragTimer (plain callout_init => kern_timeout.c:580 get_mplock), and process-thread ng_iface output (no BGL, no netisr). Extensive adversarial attempts on the stock INVARIANTS guest did NOT reproduce corruption: 4000+2000 SET_CONFIG-toggle frag_reset races at ~700k deliveries/s across CPU-hashed pump flows, 700 shutdown-vs-inflight races, and 1600 dup-hammer timer-stale cycles over 8 staggered bundles all ran clean. The negative result is explained by undocumented funnels: connected-socket ksocket feeds forward delivery through the sockets cpu0 protocol port (and the soclose token handshake serializes shutdown), so every harness-reachable inbound pair executes serialized on stock topologies -- mirroring netgraph7s DF-0330 latent verdict. The defect remains certain by inspection and is one refactor (MPSAFE soreceive, port selection, any non-socket feeder such as ng_ether-fed PPPoE bundles with multi-CPU input hashing) away from racing; primitive if raced: double TAILQ_REMOVE/kfree of struct ng_ppp_frag + double m_freem (reclaim-exploitable heap corruption). Fix authored and regression-tested (per-node lwkt_token), not race-validated because no collision was achievable.", "exploit_chain": "", "evidence": [ "findings/poc/DF-3481/VERDICT.md \u2014 full containment analysis and attempt log", "findings/poc/DF-3481/pppfragrace.c \u2014 8-bundle sticker/park/stale/dup-hammer timer race harness", "findings/poc/DF-3481/pppteardown.c \u2014 shutdown (mode 0) and SET_CONFIG-toggle (mode 1) race harness", "findings/poc/DF-3481/run.log \u2014 timer-race cycles completing without panic", "findings/poc/DF-3481/run.teardown.log \u2014 teardown attempts completing without panic", "findings/poc/DF-3481/fix_regression.log \u2014 patched ng_ppp.ko (per-node token) survives toggle storm + frag cycles", "findings/poc/DF-3481/fix.diff \u2014 per-node lwkt_token serialization (compiles -Werror)" ], "kernel_refs": [ "sys/netgraph/ppp/ng_ppp.c:1144", "sys/netgraph/ppp/ng_ppp.c:1210", "sys/netgraph/ppp/ng_ppp.c:1290", "sys/netgraph/ppp/ng_ppp.c:1364", "sys/netgraph/ppp/ng_ppp.c:1447", "sys/netgraph/ppp/ng_ppp.c:1876", "sys/netgraph/ppp/ng_ppp.c:791", "sys/netgraph/netgraph/ng_base.c:2034", "sys/kern/kern_timeout.c:580", "sys/kern/uipc_socket.c:259", "sys/kern/uipc_socket2.c:604", "sys/netgraph/ksocket/ng_ksocket.c:1097" ], "poc_changes": "Initial design parked stale packets behind a first-only blocker with sequence gaps; that is invisible to ng_ppp_frag_checkstale because its running-seq scanner desyncs at any gap (the DF-0386 defect) -- final design pins mseq with a middle-frag sticker on a second link and uses strict head-contiguity. Dup-seq hammer fragments walk the full queue (TAILQ_FOREACH_REVERSE in ng_ppp_mp_input) without inserting, so only the timer delivers stale batches. AF_NETGRAPH sockaddr paths longer than ~14 chars are truncated by the socket layer (sendto returns ENOENT); all node/hook names were shortened accordingly. mkpeer must target the OWNING node (f1:), not the peer path. Leftover graphs hold UDP ports forever, so ports are allocated dynamically each run.", "attempts": 9, "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64", "runtime_sec": 5400, "guest_dirty": 0, "build_cmd": "cc -O2 -w -pthread -o pppfragrace pppfragrace.c; cc -O2 -w -pthread -o pppteardown pppteardown.c; kldload ng_socket ng_ppp ng_ksocket", "run_cmd": "./pppfragrace 200; ./pppteardown 4000 1", "code_hash": "0507441deb7ab23e7fc8445cbcb13c6359c52f47b978848956e54c504b23303f", "notes": "Side discovery for the orchestrator (out of this files scope): ng_iface and ng_eiface node creation panics the stock kernel deterministically (panic: trying to free NULL pointer in ng_iface_constructor+0x3a2 / ng_eiface_constructor+0xf6 via plain root ngctl mkpeer . iface i0 inet on a fresh boot) -- logs preserved in findings/poc/DF-3482/. Both node types are unusable on this kernel, which also blocked the ng_iface-output race topologies. Guest was reset with-src after fix validation.", "recommended_fix": "Add a per-node lwkt_token to ng_ppp_private acquired at every entry point (rcvmsg/newhook/rcvdata/rmnode/disconnect/frag_timeout) -- see fix.diff; porting netgraph7s per-node writer token is the long-term fix.", "fix_status": "not_testable", "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT #0: (ng_ppp.ko rebuilt in-guest with per-node token; netgraph.ko rebuilt with DF-3483 fix)", "fix_baseline_reproduced": 0, "fix_patched_reproduced": 0, "fix_verdict": "Patched module compiles (-Werror) and survives 1000 config-toggle storms + 160 frag-race cycles with the data path functional (fix_regression.log). Race-validated fix is not possible because the baseline race never collided on stock topologies (socket-port funnel).", "fix_evidence": "findings/poc/DF-3481/fix_regression.log" } |