23 KiB
Resreq AST + CSE Implementation Plan
Generated from:
resreq-ast-cse-design.mdDate: 2026-04-28 Branch: merge_api_server
Summary Priority Order
P0 (blocking): makeString() TOCTOU fix, section.peg boundary bug ✓ DONE
Phase 1: TDD test skeletons (parallel with Phase 3) ✓ DONE
Phase 2: Header definitions + ABI audit ✓ DONE
Phase 3: PEG grammar (parallel with Phase 1) ✓ DONE 143 PASS / 3 FAIL (stubs)
Phase 4a: Core interning + sel_optimize_level ✓ DONE 145 PASS / 1 FAIL (Phase 4b stub)
Phase 4b: Type validation (gated on P0-1) ✓ DONE 203 PASS (2026-04-29)
Phase 4c: AND/OR associativity flattening ✓ DONE 157 PASS (2026-04-29)
Phase 4d: Const-fold transform ✓ DONE (2026-04-29)
Phase 4e: Memoized eval ✓ DONE 73 PASS (2026-04-29)
Phase 4f: GC + recompaction ✓ DONE 73 PASS (2026-04-29)
Phase 5: Build wiring ✓ DONE
Phase 6: Scheduler integration ✓ DONE 10 PASS (2026-04-30)
P0 — Critical Prerequisites (blocking gate)
Must complete before any Phase 4 work begins.
| # | Task | Section | Rationale |
|---|---|---|---|
| P0-1 | makeString() TOCTOU in src/lib/xstring.cmakeInstance + key_add, eliminating the TOCTOU window. key_add can no longer return -EEXIST on a miss path. |
§5.4 | Silent NULL return corrupts the interned AST in all §7 numeric-to-string coercions; blocks Phase 4b entirely |
| P0-2 | section.peg clause-boundary grammarselect_statement / usage_statement / span_statement, but PEG's e* always succeeds (matches empty), so alternatives 2+ were dead code (confirmed by packcc generating "defined but not used" for all pcc_action_select_statement_N functions). Fix: collapsed each statement rule into a single alternative with a compound negative lookahead covering all subsequent clause keywords. select_statement now stops at any of rusage[, span[, same[, affinity[, or ]; usage_statement stops at span[, same[, affinity[, or ]; span_statement stops at same[, affinity[, or ]. Both implicit (type==any span[ptile=1]) and explicit (select[type==any] span[ptile=1]) select forms verified by TDD: test_parse_section3 (implicit), test_parse_section_explicit_select_with_span, test_parse_section_explicit_select_rusage_span, test_parse_section_span_not_in_select — all PASS, no packcc dead-code warnings. |
§3.1 | Silent span/rusage content bleed into selectString in all implicit-select + multi-clause inputs |
Phase 1 — TDD Tests (write failing skeletons first) ✓ DONE
Priority: HIGH — must come before implementation (TDD contract)
Develop concurrently with Phase 3 (grammar is the test oracle).
| # | Task | Notes |
|---|---|---|
| 1-1 | lib/testsuite/test_select.c skeletons: parse valid expressions, unknown resource names (error), type mismatches (error) |
|
| 1-2 | SELECT_MAX_INPUT_LEN (8192) overflow, SELECT_MAX_DEPTH (64) nesting overflow |
|
| 1-3 | A&&B and B&&A produce same pointer; two makeSelectNode calls with same args return same pointer |
|
| 1-4 | ~~Write eval tests: numeric >/</==, string ==, bool standalone, !/&&/` |
|
| 1-5 | gc_sweep_select_nodes frees unreachable node; subsequent makeSelectNode re-interns fresh node |
|
| 1-6 | sched_session_active, sched_eval_in_progress, staging_merge_complete) using fake_scheduler_t scaffold in test_select.c |
Phase 6 wires real scheduler — signatures stable |
| 1-7 | src/lib/testsuite/test_resmgr_fixture.h + .c with make_test_resmgr()/free_test_resmgr() |
Phase 2 — Header Definitions ✓ DONE
Priority: HIGH — unblocks all implementation work
| # | Task | Notes |
|---|---|---|
| 2-1 | res_type_t, sel_node_type_t, xce_select_node_t typedef + struct to include/resreq.h |
|
| 2-2 | struct xce_resreq_select_ with xce_select_node_t* root |
|
| 2-3 | makeSelectNode, eval_select, eval_select_simple, select_eval_ctx_*, sel_node_table_*, gc_sweep_select_nodes, recompact_node_ids |
gc_mark_tree stays static in .c |
| 2-4 | var resmgr field to resreq_context_t (outer PEG auxil) |
|
| 2-5 | valtype_to_res_type helper; omit res_type_to_valtype (no current consumer — YAGNI) |
|
| 2-6 | xce_str_match as static inline wrapping strcmp |
|
| 2-7 | grep -rn 'parse_resreq|C\.parse_resreq' src/frontend/ |
|
| 2-8 | parse_resreq_primary and parse_resreq signatures to accept var resmgrschedobj.c:171, test_resreq.c:244/301/330 |
Phase 3 — PEG Grammar (lib/xce_select.peg) ✓ DONE
Priority: HIGH — develop concurrently with Phase 1
| # | Task | Notes |
|---|---|---|
| 3-1 | select_expr grammar (§3.1): ||, &&, !, comparisons, resname, literals, any |
|
| 3-2 | select_context_t auxil with resmgr, depth, errmsg[256], has_error |
|
| 3-3 | resname action: key_find, res_idx validity guard, valtype_to_res_type mapping |
|
| 3-4 | resname action: check against {select, rusage, span, same, affinity} |
|
| 3-5 | SELECT_MAX_INPUT_LEN (8192) and SELECT_MAX_DEPTH (64) guards |
|
| 3-6 | SEL_RESNAME vs SEL_STR; any excluded by grammar; true/false produce migration diagnostic |
|
| 3-R1 | SEL_STR from strdup/free to makeString/dereference in xce_select.peg and xce_select.ckill_select_node calls dereference() |
|
| 3-R2 | _Static_assert(sizeof(uintptr_t) >= sizeof(double)) before makeSelectNode in xce_select.cSEL_NUM bit-cast at compile time |
|
| 3-R3 | !("any" ![a-zA-Z_0-9]) negative lookahead to resname_node and resname_leaf in xce_select.pegany keyword from being tried as a resource name |
Phase 4a — Core Node Allocation and Interning ✓ DONE
Priority: HIGH — foundational; unblocks 4b–4f
| # | Task | Notes |
|---|---|---|
| 4a-1 | sel_optimize_level (static _Atomic int, sentinel -1) with sel_optimize_set() write-once CAS and sel_get_optimize_level() accessor |
|
| 4a-2 | sel_make_key() — zero padding bytes via struct literal init; SEL_RESNAME zeroes op2 for key purposes (res_type stored in node, not in key) |
|
| 4a-3 | sel_key_hash over the 3 key fields |
|
| 4a-4 | sel_node_table_init / sel_node_table_fini — makeInstance(&kvtab_type, ¶ms) with datakiller=NULL, zarray_init for linear nodes array |
|
| 4a-5 | makeSelectNode — CSE-disabled fast path; CSE-enabled path with commutativity sort, key lookup, TOCTOU double-check under lock, node_id assignment via atomic_fetch_add, zarray rollback via atomic_fetch_sub |
|
| 4a-6 | sel_node_to_key() for GC key reconstruction (must apply same commutativity sort as makeSelectNode) |
|
| 4a-7 | sel_optimize_test_reset() test-only helper (#ifdef UNIT_TEST): atomic_store_explicit reset to -1 plus sel_node_table_fini |
|
| 4a-R1 | node_id assignment inside lock after key_add succeeds so TOCTOU losers need no counter rollback |
Counter divergence under concurrent load |
| 4a-R2 | dereference((char*)op1) on first-lock cache hit for SEL_STR — caller transfers xstring ownership |
Ref leak on every cache hit for string nodes |
| 4a-R3 | SEL_MAX_INTERNED_NODES cap check in both lock sections (first miss + TOCTOU re-check) |
Unbounded table growth without cap |
| 4a-R4 | sel_node_table_fini to iterate and free all interned nodes before destroy(table) — NULL datakiller does not free them |
All nodes leaked at teardown; LSAN clean now |
| 4a-R5 | node->hashcode = sel_key_hash_fn(&key) in CSE path — was left 0, breaking Phase 4e memoization |
|
| 4a-R6 | dereference((char*)op1) on calloc failure in level==0 path |
xstring ref leak on OOM |
| 4a-R7 | -EINVAL guard on key_find — corrupt table must not silently proceed |
|
| 4a-R8 | -EEXIST recovery in key_add failure path — re-find and return the racing winner |
Rare concurrent insert race |
| 4a-R9 | _Static_assert(sizeof(uintptr_t) >= ...) to == per spec requirement |
Phase 4b — Parse-Time Type Validation ✓ DONE (2026-04-29)
Priority: HIGH — gate: P0-1 makeString() fix must be merged first
| # | Task | Notes |
|---|---|---|
| 4b-1 | parse_select() into parse_resreq_primary before kill_resreq_data |
Use-after-free if called after kill_resreq_data frees data.selectString |
| 4b-2 | snprintf "%.17g"), NULL-check makeString result, symmetric coercion |
Free orphaned SEL_NUM node in unoptimized mode before overwriting pointer |
| 4b-3 | >/</>=/<= type validation: reject non-numeric operands |
log_fatal (not assert) for belt-and-suspenders at eval time too |
| 4b-4 | res_idx validity guard and unknown-resource error; add migration diagnostic for true/false identifiers (parse as SEL_RESNAME with no match) |
|
| 4b-5 | kill_primary_resreq for conditional teardown: CSE-enabled → null root only; CSE-disabled → free_select_tree() then FREEUP |
NULL store before FREEUP to prevent GC mark dangling-pointer race |
| 4b-6 | parse_select with newlocale(LC_NUMERIC_MASK, "C") / uselocale / freelocale; log_fatal on failure in scheduler, log_error+return in API server |
_XOPEN_SOURCE 700 required before system headers |
| 4b-7 | strtod in pcc_action_value_node_1 (line 1593 of generated file), inside the uselocale scope — no locale vulnerability |
|
| 4b-R1 | lock(auxil->resmgr, RTTI_LOCK_READ) / unlock(auxil->resmgr) around key_find in resolve_resname() (xce_select.peg:38) — missing read lock caused data race in multi-threaded scheduler |
|
| 4b-R2 | [Review check — F2] STRING×NUM coercion error paths reviewed: makeSelectNode releases the xstring on OOM failure (level-0 path calls dereference(op1) before returning NULL); outer makeSelectNode(op_type) failure correctly calls SEL_DISCARD(l) + SEL_DISCARD(r) — no leak, false positive |
Phase 4c — Commutativity + Associativity Flattening ✓ DONE (2026-04-29)
| # | Task | Notes |
|---|---|---|
| 4c-1 | makeSelectNode step 1 covers AND/OR/EQ/NE; non-commutative GT/LT/GE/LE/NOT unchangedtest_cse_gt_not_commutative confirms GT is not commutative; AND/OR/EQ/NE sort verified. |
Already integrated in §5.4 design |
| 4c-2 | ~~Implement post-parse flatten pass for &&/` |
|
| 4c-3 | flatten_pass must free() each orphaned node — not its children (re-parented children must not be freed)free(new_right); free(node)) after re-parenting A/B/C; SEL_NOT branch frees node only. |
Children freed would cause double-free when free_select_tree walks from root |
Senior review fixes applied (2026-04-29):
- [MEDIUM — OOM safety] In AND/OR branch: update
node->u.binop.left/rightin-place (level==0) immediately after recursive calls, before anymakeSelectNode. This ensuresreturn nodeafter OOM returns a valid tree with live (not dangling) children. - [LOW — dead code] Removed dead
if (node->type == SEL_STR ...)/if (new_right->type == SEL_STR ...)guards in AND/OR and SEL_NOT branches — those nodes are never SEL_STR in those arms.
Phase 4d — Const-Fold Transform ✓ DONE (2026-04-29)
| # | Task | Notes |
|---|---|---|
| 4d-1 | resname == any → SEL_TRUE; resname != any → SEL_FALSE — check both child slots for SEL_ANY pointer (commutativity sort is ASLR-nondeterministic across runs) |
|
| 4d-2 | SEL_TRUE && X → X; SEL_FALSE || X → X |
|
| 4d-3 | SEL_FALSE && X → SEL_FALSE; SEL_TRUE || X → SEL_TRUE |
|
| 4d-4 | !SEL_TRUE → SEL_FALSE; !SEL_FALSE → SEL_TRUE |
Single-pass — const-fold shrinks trees, does not create new flattening opportunities |
Phase 4e — Evaluation with Memoization ✓ DONE (2026-04-29)
| # | Task | Notes |
|---|---|---|
| 4e-1 | eval_get_bool/eval_set_bool 2-bit bitmap accessors with node_id >= bool_bits_cap guards using log_fatal |
_Static_assert(EVAL_NONE == 0) for memset-reset correctness |
| 4e-2 | ~~Implement eval_select_inner — recursive eval with explicit error propagation (if (lv < 0) return -1), NOT C &&/` |
|
| 4e-3 | eval_select_inner: SEL_RESNAME writes to ctx->num_vals[node_id] or ctx->str_vals[node_id]; comparison ops call eval_select(child, ctx) then read cached arrays |
Critical: get_leaf_value() must NOT be called directly — bypasses cache writes, silently reads 0.0/NULL |
| 4e-4 | eval_select wrapper — bitmap cache check before calling eval_select_inner; cache EVAL_ERROR too (node-structural errors repeat per host) |
|
| 4e-5 | eval_select_simple for unoptimized path — plain recursive tree-walk, no select_eval_ctx_t, thread-safe (no shared state) |
|
| 4e-6 | eval_select guard: log_fatal if sel_get_optimize_level() <= 0 |
|
| 4e-7 | select_eval_ctx_init / select_eval_ctx_reset / select_eval_ctx_fini |
node_count snapshot at init; reset uses snapshot, never re-reads sel_node_next_id |
| 4e-8 | memset extended bool_bits bytes after realloc (realloc does not zero extension); partial-OOM contract — all three reallocs (bool_bits, num_vals, str_vals) must succeed atomically or log_fatal |
Mismatched capacities allow out-of-bounds write to num_vals with no inline bounds check |
Phase 4f — Mark-and-Sweep GC + Recompaction ✓ DONE (2026-04-29)
| # | Task | Notes |
|---|---|---|
| 4f-1 | gc_mark_tree — recursive DFS, early return on node == NULL || node->gc_mark; handles leaf / SEL_NOT / binary node types__attribute__((unused)) for Phase 6 wire-in |
Recursion bounded by SELECT_MAX_DEPTH (64); any increase requires stack analysis |
| 4f-2 | gc_sweep_select_nodes — Phase 6 guard placed before mark-clear (prevents two-call sweep-of-reachable-nodes bug); mark-clear iterates caps; sweep iterates caps, zarray_del before free |
GC does NOT acquire sel_node_table.lock (idle-phase invariant) |
| 4f-3 | Insert atomic_thread_fence(memory_order_seq_cst) after all primary->select->root = NULL unlinks and before gc_sweep_select_nodes call — Phase 6 call-site task |
Without fence: CPU/compiler may reorder NULL store after GC mark pass |
| 4f-4 | Runtime GC idle invariant guards at GC call site (sched_session_active, sched_eval_in_progress, staging_merge_complete) — log_fatal not assert — Phase 6 call-site task → see 6-3/6-4 |
assert compiled out by NDEBUG; use-after-free would result |
| 4f-5 | recompact_node_ids — iterate nodes zarray over caps slots (not count, gaps exist after zarray_del), assign sequential node_id, reset sel_node_next_id via atomic_store_explicit(..., memory_order_release) |
node_id and zarray slot index are decoupled — do not conflate |
| 4f-R1 | Two-call scenario: first call clears all marks and returns; second call sweeps everything | |
| 4f-R2 | free(node) / zarray_del order — zarray_del NULLs slot first, then free; eliminates latent use-after-free if killer is ever installed on nodes zarray |
|
| 4f-R3 | sel_node_table_fini iterated nodes.count instead of nodes.caps — after zarray_del creates gaps, new nodes slot into positions beyond count boundary and leaked |
Manifested as 32-byte ASAN leak after GC sweep + reintern in same lifetime |
Phase 5 — Build Wiring
| # | Task | Notes |
|---|---|---|
| 5-1 | custom_target for xce_select.peg in lib/meson.build matching section_parser/rusage_parser pattern |
Verify -o 'lib/xce_select' path format: grep -A8 'custom_target' src/lib/meson.build | grep '\-o' |
| 5-2 | xce_select_parser[0] (generated .c) to libxce_src |
|
| 5-3 | test_resmgr_fixture.c to testsuite executable() sources in lib/meson.build |
Phase 6 — Scheduler Integration
Status: ✓ DONE — 2026-04-30 — 10/10 scheduler tests pass
| # | Task | Notes |
|---|---|---|
| 6-1 | bb4a95c |
Single-threaded confirmed; stub at resource.c:127 |
| 6-2 | sched_meet_host_selection call sites accounted forbb4a95c |
No missed call sites |
| 6-3 | _Atomic int session_active, eval_in_progress, staging_merge_complete to xce_scheduler_t823a66e |
Use _Atomic int not volatile int — ARM atomicity guarantee |
| 6-4 | xce_scheduler_t fieldsacda9bb |
Signatures unchanged; only wrapper body changed |
| 6-5 | sched_meet_host_selection via eval_select_simple40261b7 |
Full implementation replacing stub that always returned 1 |
| 6-6 | sched_meet_host_selection returns int (0/1); preason on fail40261b7 |
int return kept; preason wired |
| 6-7 | build_host_eval_ctx907d8d8 |
Flattens worker->resvals zarray to numvals/strvals using Value_u_fvalue/svalue/bvalue |
| 6-8 | live_jobs flat array from sched->jobs kvmgr5267d36 |
Growth-loop heap alloc; OOM-safe (double-guarded: calloc fail + realloc null-sentinel 742d79e, fb5127a) |
| 6-9 | sel_optimize_set(1) at scheduler startupacda9bb |
Called in sched_init; no sel_optimize_set(0) needed (API server has separate process) |
| 6-10 | sel_alloc_thread_id; add thread-ID checks in eval_select and gc_sweep_select_nodesb722e5c 1eb4eb1 |
_Atomic int registered flag + pthread_equal guard; registered before while(1) loop |
| 6-11 | 5fcf85c |
Numeric (SEL_GE) and string (SEL_EQ) pass/fail cases |
| 6-12 | atomic_thread_fence(memory_order_seq_cst) before gc_sweep_select_nodes5267d36 |
After all gc_mark_select_tree calls in sched_end_session |
| R1 | staging_merge_complete bootstrap deadlock — GC never rand438742 |
sched_begin_session never set it to 1; guard in sched_end_session always fired |
| R2 | recompact_node_ids() missing from GC protocold438742 |
Added after gc_sweep_select_nodes in sched_end_session |
Key Implementation Traps
-
free_select_treevs GC mode confusion — callingfree_select_treein CSE-enabled mode or forgetting it in disabled mode both produce memory corruption. Thesel_get_optimize_level()guard insidefree_select_treecatches this withlog_fatal. -
get_leaf_value()bypass bug — callingget_leaf_value()directly in comparison branches bypasses thenum_vals/str_valscache write. Comparisons silently read 0.0/NULL (calloc default) instead of actual host values. Every comparison operator must calleval_select(child, ctx)first, then read the typed cache array. -
TOCTOU in
makeSelectNode— the double-check under lock (step 5) prevents node leaks when two threads race to intern the same node. The re-find must happen inside the lock or the winner could be freed before the loser reads it. -
atomic_thread_fencebefore GC — root-unlink writes (primary->select->root = NULL) must be visible to the GC mark pass. Without a seq_cst fence, CPU/compiler reordering causes GC to retain a node that should be swept (or vice versa). -
Partial-OOM in
select_eval_ctx_tresize — if one of the three reallocs (bool_bits, num_vals, str_vals) succeeds and another fails, the capacities diverge andeval_select_innercan write out-of-bounds tonum_vals(no inline bounds guard at write site). Treat all three as atomic —log_fatalon partial failure.