Skip to content

FABLE_CLEANUP: fix(core): 20-day audit fixes for the core engine - #1012

Merged
aidenybai merged 3 commits into
mainfrom
devin/1751430000-fable-core
Jul 2, 2026
Merged

FABLE_CLEANUP: fix(core): 20-day audit fixes for the core engine#1012
aidenybai merged 3 commits into
mainfrom
devin/1751430000-fable-core

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Summary

Core-engine slice of the 20-day deep-audit fixes from #999 (split into 4 PRs).

  • Binary-split retry budget is per-top-level-batch and anchored lazily (spawn-batches.ts): the 180s split budget was anchored at pass start (healthy lint time consumed it, so a batch failing after minute 3 was dropped whole with zero split attempts) and was pass-wide (one bad batch starved later batches). Drop reasons now name the actual limit + depth that fired.
  • REACT_DOCTOR_SUPPLY_CHAIN_TIMEOUT_MS now actually raises the budget: the configured value is threaded into checkSupplyChain's inner cap, which stayed pinned at the 90s constant (the env var could previously only lower the effective budget).
  • fixGroupId no longer rides the Score API payload (contract drift vs. "request shape identical").
  • file-lint-cache persist() fails open on a corrupt sibling ruleset bucket ({"rulesets":{"x":null}} previously crashed every warm scan until the file was hand-deleted).
  • Ruleset hash records the oxlint child's Node version — the nvm fallback can run a different Node than the parent, so engine upgrades of the binary that produced the cached diagnostics didn't bust the cache.
  • /tmp fallback cache dir is uid-scoped — the predictable world-writable path let another local user pre-create it and plant cached diagnostics.
  • NUL bytes de-binarified in compute-ruleset-hash.ts and run-oxlint.ts (literal U+0000 made git/ripgrep treat the files as binary) → \u0000 escape sequences. The cache keys are byte-identical; three of the four ruleset-hash separators are too, but the fourth was a stray literal space and is now NUL like its siblings — so the ruleset hash changes and the per-file lint cache takes a one-time miss on upgrade (fail-open: one slower scan, then warm again).
  • Auto-detected default branch validated with isSafeGitRevision before reaching git merge-base argv (repo-controlled origin/HEAD symref).
  • Security-scan fork folds fs errors to a skip (Effect.tryPromise + orElseSucceed) instead of defecting through the unconditional Fiber.join and sinking the scan.
  • POSIX argv cap raised to 1.5 MB — the 24k Windows-sized guard silently degraded --scope lines to file scope on large PRs on Linux/macOS.
  • Socket alert type/severity lookups guard Object.prototype members ("constructor" off the wire read a Function and bypassed sanitizeTerminalText).
  • Near-miss hint says "add the qualified id" instead of "change it"; dead CheckDeadCodeOptions.userConfig removed.

Follow-up commit (c22fb9ba, from adversarial review of this PR):

  • Darwin-sized argv cap (800k) — macOS ARG_MAX is 1 MiB including envp, below the new 1.5 MB POSIX cap; an argv that passes the guard but crosses the real OS limit makes spawn throw E2BIG synchronously (outside Node's async-error whitelist), escaping Effect uncatchably — the exact Unguarded subprocess spawn on the scan path aborts the run (ENAMETOOLONG / ENOTDIR / spawn UNKNOWN; regression of #46) #924 crash class the guard exists to prevent. The guard test now also overflows every platform's cap.
  • securityScan.failed telemetry — the security-scan fail-open above previously made a failed pass indistinguishable from a clean one; it's now recorded on InspectOutput.securityScanFailed and threaded through the cached scan payload to the Sentry wide event, mirroring supplyChainOverlapTimedOut (failed passes are never cached). Pinned by a regression test.
  • uid-scope test pinning — the tmp-cache-dir test's startsWith assertion passed with or without the uid suffix; it now asserts the react-doctor-cache-<scope> basename.
  • Deslop from review: NODE_VERSION_PROBE_TIMEOUT_MS moved to constants.ts, drop-reason nested ternary flattened, persist()'s kept-hashes loop collapsed, comment tightening; LINT_PHASE_TIMEOUT_MS's sizing note updated for per-batch budgets. Plus a changeset (react-doctor / @react-doctor/core patch).

New regression tests: per-batch split budget, supply-chain timeout threading, score-payload field stripping, corrupt cache bucket, security-scan fail-open, all-platform argv-cap overflow.

Part of splitting #999. Siblings: rules, CLI, and action/CI PRs.

Link to Devin session: https://app.devin.ai/sessions/023f6cac087a40558f73fca03773c180
Requested by: @aidenybai


Note

Medium Risk
Touches scan orchestration, subprocess spawning, on-disk caches, and security-sensitive supply-chain parsing; behavior is mostly fail-open with regression tests, but argv caps and one-time lint cache invalidation can change large-PR diff scope and first-run lint performance.

Overview
Core-engine reliability and security from the 20-day audit: scan orchestration, lint spawning, caches, git argv, and telemetry.

Lint binary-split recovery is scoped per top-level batch with the split deadline anchored at the first splittable failure (not pass start), so healthy lint time no longer eats the budget and one bad batch cannot starve later batches. Drop reasons now name whether the depth cap or time budget fired.

Spawn argv guards are platform-sized (Windows 24k, macOS 800k, POSIX 1.5M) so large --scope lines git diffs are not silently downgraded on Linux/macOS while avoiding synchronous E2BIG on Darwin.

Security scan I/O errors fail open (skip the pass) instead of defecting through Fiber.join and failing the whole scan; securityScan.failed is threaded through inspect output, scan cache eligibility, and Sentry wide events (mirroring supply-chain overlap timeout).

Per-file lint cache: corrupt sibling ruleset buckets no longer crash persist(); ruleset hash uses the oxlint child Node version (nvm fallback); cache keys use \u0000 separators (one-time cache miss on upgrade). /tmp fallback cache paths are uid-scoped against cross-user poisoning.

Supply-chain: REACT_DOCTOR_SUPPLY_CHAIN_TIMEOUT_MS is threaded into the check’s inner wall-clock cap (can raise, not only lower). Socket alert type/severity lookups use Object.hasOwn so wire values like "constructor" cannot hit Object.prototype.

Score API payloads strip fixGroupId with other local-only fields. Auto-detected default branch is validated before git argv. Dead-code no longer forwards unused userConfig to deslop. Foreign-disable near-miss hints say add the qualified rule id rather than replace tokens.

Reviewed by Cursor Bugbot for commit d614cb3. Bugbot is set up for automated code reviews on this repo. Configure here.

…ache hardening, NUL de-binarification

Co-Authored-By: Aiden Bai <aiden.bai05@gmail.com>
@aidenybai aidenybai self-assigned this Jul 2, 2026
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@pkg-pr-new

pkg-pr-new Bot commented Jul 2, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/eslint-plugin-react-doctor@1012
npm i https://pkg.pr.new/oxlint-plugin-react-doctor@1012
npm i https://pkg.pr.new/react-doctor@1012

commit: d614cb3

rayhanadev and others added 2 commits July 1, 2026 22:50
…ixes

- cap darwin spawn argv at 800k: macOS ARG_MAX is 1 MiB (argv + envp
  together) and an argv that passes the guard but crosses the OS limit
  makes spawn throw E2BIG synchronously, escaping Effect — the #924
  crash class the guard exists to prevent; the guard regression test
  now also overflows every platform's cap
- record the security scan's fail-open on `securityScanFailed` and
  thread it InspectOutput -> CachedScanPayload -> wide event
  (`securityScan.failed`), mirroring `supplyChainOverlapTimedOut`;
  failed passes are never cached, and the fail-open path is now pinned
  by a regression test
- pin the uid suffix in the tmp cache-dir test (the prefix match passed
  with or without the fix)
- deslop: NODE_VERSION_PROBE_TIMEOUT_MS moves to constants.ts, flatten
  the drop-reason nested ternary, collapse persist()'s keptHashes loop,
  inline the tmp cache dir name, tighten the hasOwn comment; annotate
  LINT_PHASE_TIMEOUT_MS's now-per-batch sizing assumption
- changeset for the audit-fix release notes

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e-core

# Conflicts:
#	packages/react-doctor/src/cli/utils/scan-result-cache.ts
#	packages/react-doctor/src/inspect.ts
@aidenybai
aidenybai merged commit 80e3093 into main Jul 2, 2026
25 checks passed
@aidenybai
aidenybai deleted the devin/1751430000-fable-core branch July 2, 2026 07:55
rayhanadev added a commit that referenced this pull request Jul 2, 2026
…-duration

Resolved spawn-batches.ts: main reworked the split-retry budget to lazy
per-batch anchoring (splitBudget: { deadlineMs } param, better drop-reason
messages); this branch added the --max-duration isPastDeadline() skip and the
scannedFileCount subtraction. Combined both — my deadline check sits atop
main's new signature, and the call site passes { deadlineMs: null } while still
subtracting deadline-skipped files from the scanned count.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

2 participants