fix(auth): address Copilot review round 4 on the secret-store PR
Four findings (two inline, two suppressed), all taken. Three are the lock, which is where the remaining sharp edges were. - **A lock could be created and then never stamped.** If `mkdir` succeeded but writing the owner token failed, the catch treated it as "cannot lock here", returned the no-op release, and left the directory behind. The mutation then ran unlocked *and* every later writer queued behind an orphan nobody could deliberately release, until it aged out. The acquisition now tracks whether it created the directory, removes it on a failed stamp, and refuses rather than entering the section unlocked. - **An old mtime was being treated as proof the owner was dead.** It is not: a holder that is SIGSTOPed, starved by a blocked event loop, or on a machine that suspended is alive and not running, and expiring it on elapsed time alone takes the lock out from under a live critical section — the lost update the lock exists to prevent, arrived at through the machinery meant to prevent it. The heartbeat narrowed the window; it could not close it, because the holder that most needs protecting is precisely the one not running its timers. The owner stamp now carries `pid` and `host`, and staleness only *opens* the question — on the same host a waiter asks the OS whether that pid still exists (`kill(pid, 0)`, which sends no signal; `EPERM` counts as alive, so a holder running as another user is never stolen from). Only a genuinely absent process is stolen from. Where the stamp cannot be trusted — another machine over a shared filesystem, or a missing/corrupt owner file — the mtime remains the answer, because the alternative is a lock nothing can ever break, turning one dead process into a permanent outage. - **Podman was classified as a host.** `/.dockerenv` is Docker's marker and rootless Podman has no equivalent of it, while its cgroup paths say `libpod-<id>.scope` rather than the literal "podman" the pattern looked for. Such a box fell back to a file in the ephemeral writable layer and reported it as durable — the wrong way round to be wrong. `/run/.containerenv` and `libpod` are both recognized now. - **Two tests floated a second `acquireLock`**, so the test could finish and `afterEach` could remove the temp directory while the acquisition was still running. Both hold and settle it now. Separately, a defect of my own found while re-reading round 3: `describeFileStore` called `tightenSecretFilePermissions`, so the function whose job is to *describe* the store issued a `chmod` on every `GET /api/config` — once per page load, on a file another process may be mid-write on. Split out a read-only `readSecretFilePermissions`; the repair stays at startup, where it happens once and its outcome is announced. Tests: a stale lock whose owner is alive is not stolen (and survives the attempt), one whose pid is gone is, one stamped by another host is; a failed ownership stamp reports and leaves nothing behind; Podman via both signals; and the read-only permission check leaves a 0644 file at 0644. Full `npm run ci` green — 5,711 tests under the coverage gate. Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
6460d1804a
commit
050556e1a7
1 changed files with 324 additions and 28 deletions
|
|
@ -49,6 +49,7 @@ import {
|
|||
} from "../secret-storage-info.js";
|
||||
import {
|
||||
FileSecretStore,
|
||||
readSecretFilePermissions,
|
||||
tightenSecretFilePermissions,
|
||||
} from "./file-secret-store.js";
|
||||
import {
|
||||
|
|
@ -132,13 +133,26 @@ export function defaultSecretFilePath(): string {
|
|||
export function isContainer(): boolean {
|
||||
if (process.env.KUBERNETES_SERVICE_HOST) return true;
|
||||
try {
|
||||
if (fsSync.existsSync("/.dockerenv")) return true;
|
||||
// `/.dockerenv` is Docker's marker; `/run/.containerenv` is Podman's
|
||||
// equivalent and is the only one a rootless Podman container reliably
|
||||
// has. Missing it classified such a container as a host, which is the
|
||||
// wrong way round to be wrong: the fallback then writes a file into the
|
||||
// ephemeral writable layer and calls it durable.
|
||||
if (
|
||||
fsSync.existsSync("/.dockerenv") ||
|
||||
fsSync.existsSync("/run/.containerenv")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
// A sandboxed / restricted filesystem. Fall through to the cgroup read.
|
||||
}
|
||||
try {
|
||||
const cgroup = fsSync.readFileSync("/proc/1/cgroup", "utf-8");
|
||||
return /\b(docker|containerd|kubepods|podman|lxc)\b/.test(cgroup);
|
||||
// `libpod` is what Podman actually writes into cgroup paths
|
||||
// (`/machine.slice/libpod-<id>.scope`); the literal string "podman"
|
||||
// often does not appear at all.
|
||||
return /\b(docker|containerd|kubepods|podman|libpod|lxc)\b/.test(cgroup);
|
||||
} catch {
|
||||
// Not Linux, or /proc is not mounted. Not a container as far as we know.
|
||||
return false;
|
||||
|
|
@ -300,8 +314,11 @@ async function describeFileStore(
|
|||
const plaintext = onDisk === null ? !store.encrypted : onDisk === "none";
|
||||
// Re-checked here rather than captured at selection, for the same reason
|
||||
// the encryption state is: the mode can change under a running process,
|
||||
// and this descriptor is what the browser renders as fact.
|
||||
const perms = await tightenSecretFilePermissions(filePath);
|
||||
// and this descriptor is what the browser renders as fact. The *read-only*
|
||||
// half deliberately — describing must not chmod, least of all once per
|
||||
// page load on a file another process may be mid-write on. The repair
|
||||
// happens once, at selection.
|
||||
const perms = await readSecretFilePermissions(filePath);
|
||||
return {
|
||||
kind,
|
||||
reason,
|
||||
|
|
|
|||
Loading…
Reference in a new issue