Proof of concept mechanical port of ircnet/ircd to Rust as part of a bit about C being insecure for network services
0

Configure Feed

Select the types of activity you want to include in your feed.

feat(p6cc): port res.c resend_query request-retry dispatcher to Rust

The next bottom-up unit of the still-C res.c resolver proper, above the
P6aa query-dispatch layer (do_query_name/do_query_number) and below the
still-C get_res. Ported to ircd-common/res.rs via res_link.o
(-DPORT_RES_RESEND_P6cc).

resend_query is upstream file-static; the P6v RES_RESEND_LINKAGE seam is
promoted to the standard three-way shape (extern in res_link.o, GLOBAL in
the cref oracle, else static) so the cref oracle exports cref_resend_query
for L1. No-op when resend==0, else bumps reinfo.re_resends and re-dispatches
by record type to the Rust do_query_name/do_query_number. Removed the P6v
extern decl (the module now defines the symbol).

After this, only proc_answer/get_res/init_resolver remain C in res.c.

+152 -15
+101
docs/superpowers/plans/2026-06-05-p6cc-res-resend-query.md
··· 1 + # P6cc — port `res.c` `resend_query` to Rust 2 + 3 + ## Scope 4 + 5 + `resend_query` (`ircd/res.c:703`) — the DNS request-retry dispatcher. The next 6 + bottom-up unit of the still-C resolver proper, sitting directly **above** the 7 + P6aa query-dispatch layer (`do_query_name`/`do_query_number`) and **below** the 8 + still-C `get_res`. After this only the answer parser + socket I/O remain C 9 + (`proc_answer`/`get_res`/`init_resolver`). 10 + 11 + ```c 12 + RES_RESEND_LINKAGE void resend_query(ResRQ *rptr) { 13 + if (rptr->resend == 0) return; 14 + reinfo.re_resends++; 15 + switch (rptr->type) { 16 + case T_PTR: (void)do_query_number(NULL, &rptr->addr, rptr); break; 17 + case T_AAAA: 18 + case T_A: (void)do_query_name(NULL, rptr->name, rptr, rptr->type); break; 19 + default: break; 20 + } 21 + } 22 + ``` 23 + 24 + ### Classification 25 + 26 + **Utility / callee TU** — not in `msgtab` (file-static, not in `res_ext.h`). 27 + Callers: `timeout_query_list` (already Rust, P6v) and `get_res` (still C, 28 + `res.c:1149`). **L1 is the gate**; no L2 path (the resolver is dead code under 29 + the locked `NO_DNS_LOOKUP` config — same as every P6q–P6bb resolver bite; the 30 + golden binary never drives a live lookup). No S2S (no `IsServer`/remote-field). 31 + 32 + ### Callees — all already Rust 33 + 34 + - `do_query_name` (P6aa), `do_query_number` (P6aa) → `query_name` (P6z) → 35 + `send_res_msg` (P6y). So `resend_query` is the clean next leaf up the tree. 36 + - `reinfo.re_resends` — the res-private counter global, already exposed 37 + (de-static'd in res_link.o, GLOBAL in the cref oracle). 38 + 39 + ## Seam 40 + 41 + `resend_query` is upstream file-static. The `RES_RESEND_LINKAGE` macro already 42 + exists (P6v) but is only 2-way (`extern` under `PORT_RES_WALK_P6v`, else 43 + `static`). Promote it to the standard **3-way** shape so the cref oracle exports 44 + `cref_resend_query` for L1: 45 + 46 + ```c 47 + #if defined(PORT_RES_RESEND_P6cc) 48 + #define RES_RESEND_LINKAGE extern /* res_link.o: Rust defines it; still-C get_res + Rust timeout_query_list resolve at link */ 49 + #elif defined(PORT_RES_WALK_P6v) 50 + #define RES_RESEND_LINKAGE /* (legacy P6v path; superseded by P6cc when both set) */ 51 + #elif defined(RES_CACHE_EXPOSE) 52 + #define RES_RESEND_LINKAGE /* GLOBAL: cref_resend_query for L1 */ 53 + #else 54 + #define RES_RESEND_LINKAGE static 55 + #endif 56 + ``` 57 + 58 + res_link.o builds with **both** `PORT_RES_WALK_P6v` and the new 59 + `PORT_RES_RESEND_P6cc`; ordering `PORT_RES_RESEND_P6cc` first → `extern`. Wrap 60 + the body (`res.c:703-721`) in `#ifndef PORT_RES_RESEND_P6cc` so res_link.o drops 61 + it. `build.rs` gains `-DPORT_RES_RESEND_P6cc` on the res_link.o compile line. 62 + 63 + The cref oracle (`-DRES_CACHE_EXPOSE`, no `PORT_*`) compiles the body GLOBAL → 64 + `cref_resend_query`. 65 + 66 + ## Port 67 + 68 + In `ircd-common/src/res.rs`: 69 + - **Remove** the `fn resend_query(rptr: *mut reslist);` extern decl (lines 59-61) 70 + — the Rust module now *defines* it. 71 + - Add `#[no_mangle] pub unsafe extern "C" fn resend_query(rptr: *mut reslist)` 72 + faithful to the C: early-return on `resend == 0`, bump `reinfo.re_resends`, 73 + match `(*rptr).type_` on the `T_PTR` / `T_AAAA | T_A` / default arms, calling 74 + the Rust `do_query_number(null, addr_of_mut!((*rptr).addr), rptr)` / 75 + `do_query_name(null, (*rptr).name, rptr, type_ as c_int)`. 76 + 77 + ## L1 test — `ircd-testkit/tests/res_resend_query_diff.rs` 78 + 79 + Model on `res_do_query_diff.rs` (two isolated `World`s — LIVE `#[link_name]` vs 80 + `cref_*`; per-world loopback `::1` UDP recv/send sockets; interposed 81 + `gettimeofday` pinning the id). Add `resend_query` + `cref_resend_query` to the 82 + worlds. Snapshot `{ret(void), datagram bytes, node.{type,name,id,sends,sent}, 83 + re_resends delta, re_sent delta, h_errno}`. 84 + 85 + Branch matrix + inverses: 86 + 1. `resend == 0` → **no-op**: no datagram, `re_resends`/`re_sent` deltas == 0, 87 + node untouched (the inverse — the guard actually fires). 88 + 2. `resend == 1`, `type = T_A`, name set → dispatches `do_query_name`: datagram 89 + sent, `re_resends` +1, `re_sent` +1, `sends` bumped. 90 + 3. `resend == 1`, `type = T_PTR`, addr set → dispatches `do_query_number`: 91 + datagram sent (reverse name), `re_resends` +1. 92 + 4. `resend == 1`, `type = 0` (default arm) → `re_resends` +1 but **no** datagram 93 + (the inverse — counter bumps but nothing dispatched). 94 + 95 + ## Done gate 96 + 97 + - L1 zero-diff (`cargo test -p ircd-testkit --test res_resend_query_diff`). 98 + - `cargo build -p ircd-rs` 0 warnings; `nm target/debug/ircd | grep resend_query` 99 + shows it `T` (defined in the binary = from Rust). 100 + - PLAN.md P6 row + `PLAN-P6-progress.md` one-liner + `docs/progress-log/p6.md` 101 + detailed entry. Commit (conventional).
+29 -3
ircd-common/src/res.rs
··· 56 56 fn sendto_one(to: *mut aClient, fmt: *mut c_char, ...) -> c_int; 57 57 fn sendto_flag(chan: u32, pattern: *mut c_char, ...); 58 58 fn sendto_iauth(pattern: *mut c_char, ...) -> c_int; 59 - // res.c file-static resolver fn, exposed in res_link.o (P6v RES_RESEND_LINKAGE) 60 - // so the Rust timeout_query_list resolves to the still-C resend/socket path. 61 - fn resend_query(rptr: *mut reslist); 62 59 // Rust ports (resolved by the linker). 63 60 fn is_allowed(cptr: *mut aClient, function: c_long) -> c_int; 64 61 fn m_nopriv( ··· 497 494 (*rptr).he.h_length = IN_ADDR_SIZE as c_int; 498 495 } 499 496 query_name(ipbuf.as_mut_ptr() as *mut c_char, C_IN, T_PTR as c_int, rptr) 497 + } 498 + 499 + // ==================================================================== 500 + // P6cc: the request-retry dispatcher resend_query — the unit directly above the 501 + // P6aa query-dispatch layer (do_query_name/do_query_number) and below the still-C 502 + // get_res. Called by the Rust timeout_query_list (P6v) on the retry branch and by 503 + // the still-C get_res on a truncated/failed reply. 504 + // ==================================================================== 505 + 506 + /// res.c:703 `resend_query` — re-issue an outstanding DNS request that still has 507 + /// retries left. A no-op when `resend == 0`; otherwise bumps `reinfo.re_resends` 508 + /// and re-dispatches by record type: `T_PTR` → `do_query_number(NULL, &addr, rptr)`, 509 + /// `T_A`/`T_AAAA` → `do_query_name(NULL, name, rptr, type)`, any other type → a 510 + /// no-op (the counter still bumped, faithful to the C ordering). 511 + #[no_mangle] 512 + pub unsafe extern "C" fn resend_query(rptr: *mut reslist) { 513 + if (*rptr).resend == 0 { 514 + return; 515 + } 516 + reinfo.re_resends += 1; 517 + match (*rptr).type_ { 518 + T_PTR => { 519 + do_query_number(null_mut(), addr_of_mut!((*rptr).addr), rptr); 520 + } 521 + T_AAAA | T_A => { 522 + do_query_name(null_mut(), (*rptr).name, rptr, (*rptr).type_ as c_int); 523 + } 524 + _ => {} 525 + } 500 526 } 501 527 502 528 // ====================================================================
+5 -1
ircd-sys/build.rs
··· 273 273 // linkage seam needed, the cref oracle already has them) now live in 274 274 // ircd-common/res.rs; res_link.o drops their bodies so the still-C get_res/s_bsd 275 275 // callers resolve to the Rust defs. 276 + // P6cc adds -DPORT_RES_RESEND_P6cc: the request-retry dispatcher resend_query 277 + // (upstream file-static; RES_RESEND_LINKAGE promoted to the 3-way shape) now 278 + // lives in ircd-common/res.rs; res_link.o drops its body + declares it extern so 279 + // the still-C get_res + the Rust timeout_query_list resolve to the Rust def. 276 280 run( 277 281 Command::new("sh").current_dir(&cbuild).arg("-c").arg(format!( 278 - "{cc_override} {s_cflags} -DPORT_DNS_M_DNS -DPORT_RES_CACHE_P6q -DPORT_RES_CACHE_REM_P6r -DPORT_RES_CACHE_LOOKUP_P6s -DPORT_RES_CACHE_BUILD_P6t -DPORT_RES_REQ_P6u -DPORT_RES_WALK_P6v -DPORT_RES_HOSTNAME_P6w -DPORT_RES_CRES_MEM_P6x -DPORT_RES_SEND_P6y -DPORT_RES_QUERY_P6z -DPORT_RES_DOQUERY_P6aa -DPORT_RES_GETHOST_P6bb -c ../ircd/res.c -o res_link.o" 282 + "{cc_override} {s_cflags} -DPORT_DNS_M_DNS -DPORT_RES_CACHE_P6q -DPORT_RES_CACHE_REM_P6r -DPORT_RES_CACHE_LOOKUP_P6s -DPORT_RES_CACHE_BUILD_P6t -DPORT_RES_REQ_P6u -DPORT_RES_WALK_P6v -DPORT_RES_HOSTNAME_P6w -DPORT_RES_CRES_MEM_P6x -DPORT_RES_SEND_P6y -DPORT_RES_QUERY_P6z -DPORT_RES_DOQUERY_P6aa -DPORT_RES_GETHOST_P6bb -DPORT_RES_RESEND_P6cc -c ../ircd/res.c -o res_link.o" 279 283 )), 280 284 "compile res_link.o", 281 285 );
+17 -11
ircd/res.c
··· 88 88 #define RES_REQ_LINKAGE static 89 89 #endif 90 90 91 - /* P6v: the queue walkers del_queries/timeout_query_list are ported to Rust 92 - (ircd-common/res.rs) — the second bite of the resolver proper, built on the 93 - P6u queue core. They are already public (res_ext.h), so dropping their bodies 94 - in res_link.o (-DPORT_RES_WALK_P6v) lets the still-C callers (s_bsd.c, ircd.c) 95 - and the Rust list.rs resolve to the Rust defs at link. timeout_query_list's 96 - retry branch calls the still-C `resend_query`, which is file-static: expose its 97 - symbol (RES_RESEND_LINKAGE = extern) only when building res_link.o so the Rust 98 - port links against it. The cref oracle keeps resend_query static — its 99 - objcopy-renamed local cref_resend_query keeps cref_timeout_query_list 100 - self-contained inside libcref.a (resend_query has no out-of-TU caller). */ 101 - #if defined(PORT_RES_WALK_P6v) 91 + /* P6v/P6cc: the queue walkers del_queries/timeout_query_list are ported to Rust 92 + (ircd-common/res.rs, P6v); the request-retry dispatcher resend_query they call 93 + is now ported too (P6cc). resend_query is upstream file-static, so the same 94 + three-way linkage as RES_QUERY_LINKAGE: `extern` when building res_link.o 95 + (-DPORT_RES_RESEND_P6cc; the body is #ifndef'd out, Rust defines it, the still-C 96 + get_res + the Rust timeout_query_list resolve at link), GLOBAL in the cref 97 + oracle (cref_resend_query for L1), else static. The legacy -DPORT_RES_WALK_P6v 98 + arm (which made it `extern` to satisfy the Rust timeout_query_list) is kept for 99 + clarity but is superseded by P6cc whenever both are defined (res_link.o sets 100 + both). */ 101 + #if defined(PORT_RES_RESEND_P6cc) 102 + #define RES_RESEND_LINKAGE extern /* Rust defines it; C+Rust callers resolve at link */ 103 + #elif defined(PORT_RES_WALK_P6v) 102 104 #define RES_RESEND_LINKAGE /* extern: Rust timeout_query_list calls it */ 105 + #elif defined(RES_CACHE_EXPOSE) 106 + #define RES_RESEND_LINKAGE /* GLOBAL: cref_resend_query for L1 */ 103 107 #else 104 108 #define RES_RESEND_LINKAGE static 105 109 #endif ··· 700 704 } 701 705 #endif /* !PORT_RES_QUERY_P6z */ 702 706 707 + #ifndef PORT_RES_RESEND_P6cc /* P6cc: resend_query → ircd-common/res.rs */ 703 708 RES_RESEND_LINKAGE void resend_query(ResRQ *rptr) 704 709 { 705 710 if (rptr->resend == 0) ··· 719 724 } 720 725 return; 721 726 } 727 + #endif /* PORT_RES_RESEND_P6cc */ 722 728 723 729 /* 724 730 * process name server reply.