mirror of
https://github.com/garrytan/gstack.git
synced 2026-06-28 04:30:01 +02:00
harden(sync): close staging-guard TOCTOU + fail hard on marker write (#1802 C5)
checkOwnedStagingDir() now returns the realpath-resolved canonicalPath on a pass, and cleanupStagingDir() rmSync's that instead of the raw input — closing the gap where the input is a symlink swapped between the ownership check and the delete. makeStagingDir() tears down the partial dir and rethrows if the marker write fails, so a marker-less dir (which the guard would refuse forever) can never leak. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -287,6 +287,14 @@ describe("#1802 checkOwnedStagingDir — ownership matrix", () => {
|
||||
expect(checkOwnedStagingDir(mintStaging(), home).ok).toBe(true);
|
||||
});
|
||||
|
||||
test("#1802 C5: ok verdict carries the realpath-resolved canonicalPath", () => {
|
||||
const d = mintStaging();
|
||||
const v = checkOwnedStagingDir(d, home);
|
||||
expect(v.ok).toBe(true);
|
||||
// Callers must delete this (not the raw input) to close the symlink TOCTOU.
|
||||
expect(v.canonicalPath).toBe(fs.realpathSync(d));
|
||||
});
|
||||
|
||||
test("repo root (direct child, has .git, no marker) → refused", () => {
|
||||
const repo = path.join(home, "my-repo");
|
||||
fs.mkdirSync(path.join(repo, ".git"), { recursive: true });
|
||||
@@ -409,6 +417,27 @@ describe("#1802 C3 — import-timeout preserve (static invariant)", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ── #1802 C5: hardening (static invariant) ─────────────────────────────────
|
||||
describe("#1802 C5 — hardening (static invariant)", () => {
|
||||
const ingest = fs.readFileSync(
|
||||
path.join(ROOT, "bin", "gstack-memory-ingest.ts"),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
test("cleanupStagingDir deletes the canonical path, not the raw input", () => {
|
||||
expect(ingest).toMatch(/rmSync\(verdict\.canonicalPath \?\? dir/);
|
||||
});
|
||||
|
||||
test("makeStagingDir tears down + rethrows if the marker write fails", () => {
|
||||
const at = ingest.indexOf("function makeStagingDir");
|
||||
expect(at).toBeGreaterThan(-1);
|
||||
const slice = ingest.slice(at, at + 800);
|
||||
expect(slice).toMatch(/catch \(err\)/);
|
||||
expect(slice).toMatch(/rmSync\(dir, \{ recursive: true, force: true \}\)/);
|
||||
expect(slice).toMatch(/throw err/);
|
||||
});
|
||||
});
|
||||
|
||||
// ── #1802 C4: resume must not mark failed files as ingested ─────────────────
|
||||
// readNewFailures() maps gbrain's per-file failures (keyed by staging-relative
|
||||
// path) back to source paths so the caller can EXCLUDE them from state
|
||||
|
||||
Reference in New Issue
Block a user