Skip to content

An interrupted vendor eject (Ctrl-C, SIGTERM or SIGKILL) can leave the npm lock back on the registry, unpatched, with no journal, and a re-run then says "No manifest found, nothing to vendor" (exit 0) #1782

Open

Description

[agent] Found by the scheduled npm bug-hunt routine (ledger #302).

Summary

The hosted → vendored eject (plain socket-patch vendor over hosted pins) writes the upstream restore straight to disk. It rewrites package-lock.json back to the registry entry and deletes a hosted-created .npmrc (or leaves the allow-remote=all line in a pre-existing one). Only afterwards does it run the vendored apply, whose group commit writes .socket/vendor/.commit-journal.json. The only safety net in between is the in-memory EjectSnapshot.

If the process dies in that window, nothing is left on disk to recover from. That covers SIGKILL, Ctrl-C (SIGINT), a CI job cancel or timeout (SIGTERM), an OOM kill and a power loss. The project ends up neither hosted nor vendored:

  • the lock points at the unpatched registry tarball;
  • .npmrc is gone;
  • .socket/vendor/ holds an orphaned, partly written uuid dir;
  • there is no journal for the next locked command to replay.

The next npm ci installs the vulnerable bytes. Every tool says nothing is wrong:

  • re-running vendor prints No manifest found, nothing to vendor. (--json: status: "noManifest"), exit 0;
  • vendor --check prints nothing, exit 0;
  • vex says there is nothing to attest.

The scan --mode vendored takeover of the same hosted pin is crash-safe. Killed at each of its renames, it stays hosted until the journal exists, then rolls forward. So this gap is specific to the eject.

Impact

  • A Ctrl-C or CI cancel during vendor silently unpatches the project. Only git diff shows it.
  • The documented recovery path ("re-run the command") doesn't help. vendor no longer sees any hosted pin, and there's no manifest, so it exits 0 having done nothing. The user has to know to git checkout -- package-lock.json .npmrc or run scan --mode vendored.
  • The window lasts about 115–140 ms per run with one package, on a local disk (measured with strace -tt). With a single package it contains six git subprocess spawns. Per-package git probes plausibly widen it with more packages.

Repro (Linux, npm 10.9.4 / 12.2.1, Node 22.22.0)

A small LD_PRELOAD shim delivers a signal on the Nth rename(2). That stands in for a Ctrl-C landing at that moment. A local mock of the patch API and patch server serves one free patch for ms@2.0.0, the transitive copy under debug@2.6.9.

# killren.c: kill(getpid(), $KILL_SIG or SIGKILL) on the $KILL_AT-th rename()/renameat()
gcc -shared -fPIC -o killren.so killren.c -ldl

mkdir p && cd p && echo '{"name":"p","version":"1.0.0","private":true}' > package.json
npm i ms@2.1.3 debug@2.6.9
A="--api-url http://127.0.0.1:18080 --org o --api-token fake --patch-server-url http://127.0.0.1:18080"
socket-patch scan --mode hosted --yes $A        # pins debug/node_modules/ms to the hosted tarball, writes .npmrc
git init -q && git add -A && git commit -qm hosted

LD_PRELOAD=$PWD/../killren.so KILL_AT=3 socket-patch vendor --yes $A    # killed, exit 137
git status --short        #  D .npmrc   M package-lock.json   ?? .socket/
grep '"resolved".*ms-2.0.0' package-lock.json   # https://registry.npmjs.org/ms/-/ms-2.0.0.tgz
ls .socket/vendor/.commit-journal.json          # No such file
rm -rf node_modules && npm ci && head -c 40 node_modules/debug/node_modules/ms/index.js   # unpatched
socket-patch vendor --yes $A    # "No manifest found, nothing to vendor."  exit 0
socket-patch vendor --check $A  # (no output) exit 0
socket-patch vex --output v.json $A   # "...nothing to attest"

Order of writes in a successful eject (strace -f -e rename,unlink):

unlink("./.npmrc")                                   <- upstream restore, direct
rename(stage, "./package-lock.json")                 <- upstream restore, direct (now registry/unpatched)
rename(stage, ".socket/vendor/npm/<uuid>/ms-2.0.0.tgz")
rename(stage, ".socket/vendor/npm/<uuid>/.gitignore")
rename(stage, ".socket/vendor/npm/<uuid>/.gitattributes")
rename(stage, ".socket/vendor/npm/<uuid>/socket-patch.vendor.json")
rename(stage, ".socket/vendor/.commit-journal.json")  <- first durable recovery point
rename(stage, "./package-lock.json")                 <- vendored wiring
rename(stage, ".socket/vendor/state.json")
unlink(".socket/vendor/.commit-journal.json")

Expected vs actual

Expected. CLI_CONTRACT.md, vendor eject: "The eject is ONE planned transition, all-or-nothing … If any package then fails, exactly the files the eject wrote are put back … the project stays hosted exactly as before".

The vendored crash semantics: "before the journal is durable, nothing is committed — the lockfiles and ledgers are the pre-run ones and the run's artifacts are unreferenced orphans; after it, the next command that takes the apply lock replays the journal".

An interrupted eject should leave the project hosted (pre-journal) or finish vendored (post-journal), as the scan --mode vendored takeover does.

Actual. An interrupt at any of renames 2–6 (main) or 2–6 (v5.0.1) leaves the lock un-hosted, with no journal and no recovery. The same happens on SIGINT and SIGTERM: the CLI installs no handler that runs the EjectSnapshot restore.

Matrix (each cell run at least twice)

Build npm Interrupt Kill point Result
main b787454 10.9.4 (v3 lock) SIGKILL renames 2,3,4,5,6 fail: lock on registry, .npmrc deleted, no journal, re-run noManifest exit 0
main b787454 10.9.4 SIGKILL rename 1 .npmrc already deleted, lock still hosted, stray .socket-stage-package-lock.json-* at the root; npm 12 npm ci would then fail EALLOWREMOTE (fails closed)
main b787454 10.9.4 SIGKILL renames 7, 8 pass (journal replayed by the next locked command)
main b787454 10.9.4 SIGINT / SIGTERM rename 3 fail (same state as SIGKILL)
main b787454 12.2.1, install-strategy=linked, pre-existing .npmrc SIGKILL rename 3 fail: lock on registry, npm ci installs unpatched .store copy, re-run noManifest exit 0
v5.0.1 (npm package) 10.9.4 SIGKILL renames 2–6 fail (same)
main b787454 10.9.4 SIGKILL during the artifact download (mock delays the tarball 8 s) — pass: the download happens before the restore, so the lock is still hosted
main b787454 10.9.4 scan --mode vendored takeover, SIGKILL renames 1–8 pass: hosted until the journal, then rolls forward

Not OS-specific: the ordering is in shared code. macOS / Windows were not run (this routine's probe branches are currently blocked).

First bad version

Present in v5.0.1 and on main b787454 (#1648 kept the ordering).

Suspect code

  • crates/socket-patch-cli/src/commands/vendor.rs:2029: run_eject calls restore_upstream(.., dry_run: false, ..) with no GroupCommit, so the restore's lock rewrite and .npmrc removal reach the disk directly. Recovery relies on EjectSnapshot (vendor.rs:1992), which lives only in memory.
  • Compare vendor.rs:3405. The takeover takes group.savepoint() and runs restore_upstream inside the group-commit overlay, so the restore lands in the same journaled commit as the vendored wiring.
  • vendor.rs:2239: a re-run reports No manifest found, nothing to vendor. with exit 0 even though .socket/vendor/npm/<uuid>/ holds an orphan for a package the lock no longer pins. A hint toward git checkout / scan --mode vendored would at least make the state visible.

Probe runs: none (probe branches are blocked for this routine; Linux only).

Activity

  1. mikolalysenko commented on Oct 28, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Triage: priority:p1 (npm). Confirmed on main b787454: run_eject (crates/socket-patch-cli/src/commands/vendor.rs) runs the wet restore_upstream before the vendored backend opens its group commit, so the lock rewrite and .npmrc removal hit the disk before .socket/vendor/.commit-journal.json exists. No duplicate found among open issues.

    [agent] Claiming this issue (shared root cause: eject's upstream restore is written outside the vendored group commit). Branch: agent/fix-eject-restore-journal. Claim-ID: 2026-10-28T13:27:00Z-06b3a0


    Generated by Claude Code

  2. mikolalysenko commented on Oct 28, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Draft PR: #1783


    Generated by Claude Code

  3. added 3 commits that reference this issue on Oct 28, 2026
    cc29273
    fa35cf3
    1ff7c0c
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions