fix(daemon): raw IPC memory setters reject out-of-range input (lr-553d27) - #417
fix(daemon): raw IPC memory setters reject out-of-range input (lr-553d27)#417clagentic-builder[bot] wants to merge 5 commits into
Conversation
…ess check (lr-71f0c3) fix(scripts): stop leaking raw 'unknown command' on stale-daemon process check (lr-71f0c3)
…9e6569) fix(server-settings): add Save button to Advanced > Memory panel (lr-9e6569)
Pure validateMemAvailableThresholdMB / validateTokensPerMbHeadroom functions, extracted so the web/WS setter path and the raw IPC socket path validate against one contract instead of two independently-maintained copies.
|
PEACHES — clean (0 findings) Verified SHA 3cbf485 extracts shared memory-setting validators for parity between raw IPC and WS/web setter paths. Checked:
|
|
BOBBIE -- clean Audited PR #417 (lr-553d27) at head 3cbf485 against base 35da090. Diff: lib/daemon.js, lib/memory-setting-validate.js, test/memory-setting-validate-lr-553d27.test.js, test/daemon-ipc-memory-setter-parity-lr-553d27.test.js. Integrity gap (the finding I raised on PR #416, originally cited at lib/daemon.js:1732-1738 pre-fix) is closed on both control paths. The raw IPC switch cases set_mem_available_threshold and set_tokens_per_mb_headroom (lib/daemon.js ~1738-1760 post-fix) now call the SAME validateMemAvailableThresholdMB / validateTokensPerMbHeadroom functions (lib/memory-setting-validate.js:16-30) that the WS/web handlers onSetMemAvailableThreshold / onSetTokensPerMbHeadroom already used (lib/daemon.js ~1232-1259). Both branches now return ok:false with a band-naming error and leave config unmutated on an out-of-range value; both persist and echo the actual accepted value (not a substituted default) on ok:true. No residual clamp-then-ok:true path found in this diff. Coercion review (raw IPC is the less-trusted local socket surface): parseInt-based validation unchanged in semantics from the pre-existing web path -- null/undefined/Infinity/object inputs all coerce to NaN and are rejected; numeric-prefix strings (e.g. 300abc) still truncate-parse, but that is pre-existing parseInt behavior already shipped on the web path, not a new exposure introduced by this PR. No new sink, no new confirm-without-landing variant (tome #845 checked specifically for this) -- this PR is a pure extraction into a shared module, not a behavior change beyond parity. Test files (in scope, reviewed): both new test files exercise real socket reachability over lib/ipc.js, not just unit-level validator plumbing, plus a source-parity check that daemon.js actual case bodies call the shared validator. Matches the reachability-over-plumbing discipline tome #845 calls for. Scanners: gitleaks (scoped 35da090..3cbf485, 1 commit) -- 0 leaks. trufflehog (same range) -- 0 verified/unverified secrets. semgrep p/security-audit on both changed lib files -- 0 findings. No package.json/package-lock.json changes in this diff -- osv-scanner not applicable. scanners_run: gitleaks(ok), trufflehog(ok), semgrep(ok), osv-scanner(not-applicable, no dep changes) Findings: none. |
test/verify-installed-build-stale-process-lr-71f0c3.test.js:172 assigned an unused 'matches' var whose result was never read, from a regex whose escaping did not match its adjacent comment's description. The comment describes an assertion (regex-based detection confined to resolveProcessBuildStatus, not duplicated in main()) that the following two real assertions (fnStart/fnEnd boundary slicing) already correctly implement -- no assertion was missing, so the dead line is deleted rather than replaced. Confirmed pure no-op for test outcomes: total test count unchanged at 1600 before and after across multiple full-suite runs.
…alidators (lr-553d27) BOBBIE's coercion review on PR #417 found parseInt() alone truncate-parses a garbage-suffixed string ("300abc" -> 300) instead of rejecting it: a raw IPC caller sending {"value":"300abc"} received ok:true with 300 silently persisted -- a different value than it sent, the same reports-success-while-substituting-a-different-value contract violation this task exists to close, one level below the out-of-range case. Added coerceCleanInteger() to lib/memory-setting-validate.js: accepts a real integer number, or a numeric string that is a clean whole number (optionally surrounded by whitespace) with no trailing/leading non-digit characters; rejects everything else, including a numeric prefix followed by garbage and a decimal fraction. Applied to both validators, so both the WS/web and raw IPC paths get the fix automatically via the shared contract. CONSTRAINT preserved: in-range callers are unaffected, and a clean numeric STRING (e.g. "1000") -- a plausible caller shape over a JSON socket -- remains accepted; only malformed input is newly rejected. Extended both test files: unit coverage for coerceCleanInteger's accept/reject boundaries (garbage suffix, decimal fraction, whitespace-wrapped garbage vs. whitespace-padded clean value), and real-socket reachability coverage asserting a raw IPC caller sending "300abc"/"128xyz" receives ok:false with the band named and the persisted value unchanged, while a clean numeric string still round-trips. Demonstrated the new tests fail against plain parseInt (temporarily reverted, confirmed the intended failures and no others, restored) and pass with the fix; full suite 1610/1610 clean on a subsequent run (2 residual failures on the run immediately prior are the pre-existing lr-eb0d5e-class daemon-spawn flake, unrelated to this change).
3cbf485 to
0d74b15
Compare
Brings the raw IPC socket setters for set_mem_available_threshold and set_tokens_per_mb_headroom in lib/daemon.js to parity with the WS/web handlers onSetMemAvailableThreshold and onSetTokensPerMbHeadroom: an out of range value is now rejected with ok:false and an error naming the valid band, instead of being silently clamped to the default while still reporting ok:true.
Extracted a single shared validator module, lib/memory-setting-validate.js, exporting validateMemAvailableThresholdMB and validateTokensPerMbHeadroom, and pointed both the web/WS path and the raw IPC path at it. The divergence between two independently-maintained copies of the same range check was the actual root cause named in the task, not just the individual clamp.
Sweep of the rest of the raw IPC switch: checked every other case (add_project, remove_project, get_status, get_activity_diagnostics, get_build_status, set_pin, set_project_title, set_os_users, set_auto_continue, set_keep_awake, enable_recovery, disable_recovery, shutdown, restart, update). None of the others share the clamp-and-report-success shape. Only the two memory-guard setters had the defect; both are fixed here.
TASK: lr-553d27
UPDATE 1 -- CI red root-caused and NOT from this diff. The 'test' job failed once at head 3cbf485 on test/project-asset-auth-fallback-lr-e33776.test.js ("daemon did not respond within 20000 ms"), a pre-existing daemon-spawn/port-poll timeout test unrelated to this PR's surface (it spawns lib/daemon.js as a real subprocess; my new tests only drive lib/ipc.js's createIPCServer with an injected handler, no daemon spawn at all). Node's --test runner isolates each test file into its own process, ruling out cross-file handle leakage as the mechanism. Reproduced 1 failure in 3 local full-suite runs (same command, same code); the other two were clean at 1600/1600 -- nondeterministic, matching the exact daemon-spawn-then-poll-port flake class already tracked in lr-eb0d5e (shell-asset-public-before-auth-lr-2895ea, ws-subprotocol-echo-cookie-auth-lr-4c58ae, boot-smoke-lr-1a5f). Filed a comment on lr-eb0d5e adding this as a fourth tracked file rather than fixing lib/server.js or that test file here -- out of this PR's scope per that task's own investigation boundary.
UPDATE 2 -- folded in lr-d5c542 (authorized fold-in, same PR). test/verify-installed-build-stale-process-lr-71f0c3.test.js:172 had a dead 'var matches = src.match(...)' assignment whose result was never read, with escaping that did not match its adjacent comment's description. DECISION: deleted the line rather than writing a replacement assertion -- the comment describes an assertion (regex-based detection confined to resolveProcessBuildStatus, not duplicated in main()) that the following two real assertions (fnStart/fnEnd boundary slicing) already correctly implement; nothing was missing. Confirmed pure no-op for test outcomes: total test count unchanged (1600 before, 1600 after) across multiple full-suite runs.
UPDATE 3 -- folded in a coercion gap BOBBIE found in this PR's own new code (authorized fold-in, same PR). parseInt() alone truncate-parses a garbage-suffixed string ("300abc" -> 300) instead of rejecting it -- a raw IPC caller sending {"value":"300abc"} would have received ok:true with 300 silently persisted, the exact reports-success-while-substituting-a-different-value defect this task exists to close, one level below the out-of-range case. DECISION: added coerceCleanInteger() to lib/memory-setting-validate.js -- accepts a real integer, or a numeric string that is a clean whole number (whitespace-trimmed) with no trailing/leading garbage; rejects a non-numeric prefix match, a decimal fraction, or anything non-numeric. Applied to both validators so both paths get the fix via the shared contract. CONSTRAINT preserved: a clean numeric string (e.g. "1000") -- a plausible caller shape over a JSON socket -- remains accepted; only malformed input is newly rejected. Extended both test files with unit and real-socket reachability coverage; demonstrated the new tests fail against plain parseInt (temporarily reverted, confirmed, restored) and pass with the fix.
Final state: npm test 1610/1610 passing on a clean run (residual failures on other runs are the pre-existing lr-eb0d5e-class flake, confirmed unrelated to this diff by file isolation and reproduction pattern).
TASK: lr-553d27, lr-d5c542