What happens
src/lib/diagnostics/tarball.ts:28 stages the debug bundle at a path other local users can
predict, and lets tar create it:
const partial = `${output}.partial.${process.pid}`;
Nothing creates that file first, so the spawnSync("tar", ...) at
src/lib/diagnostics/tarball.ts:29 chooses its mode and the success path renames it to output
at src/lib/diagnostics/tarball.ts:51. Two things follow, both observed on main (337c1eed5)
with the default umask 022:
The bundle is published group- and world-readable — bundle mode: 644. That is the file the
command tells users to attach to a GitHub issue
(src/lib/diagnostics/debug.ts:514-517).
tar follows a symlink planted at the staging path. After planting one and letting tar
write, the symlink target began 037 213 — the gzip magic number, i.e. it was overwritten with
tarball content.
Both are reachable because the command's own example writes into a world-writable directory
(src/commands/debug.ts:88):
nemoclaw debug --output /tmp/nemoclaw-debug.tar.gz
There, any local user can read the bundle, or pre-plant a symlink at
/tmp/nemoclaw-debug.tar.gz.partial.<pid> — a small, enumerable space — to have a file of their
choosing overwritten with the tool's privileges.
Why this looks like drift
The repository already stages temp files safely: src/lib/shields/timer.ts:167 uses
fs.openSync(tempPath, "wx", 0o600), src/lib/share-command.ts:233 uses
{ mode: 0o600, flag: "wx" }, and src/lib/shields/index.ts:393 uses O_EXCL | O_NOFOLLOW.
The staging path here arrived in #4506 (f8c85f37e), whose stated goal was to leave no partial
tarball and rename atomically. Permissions and predictability were not in that change's scope.
Environment
macOS 26.5.1 (25F80), Node v26.7.0, main at 337c1eed5.
What happens
src/lib/diagnostics/tarball.ts:28stages the debug bundle at a path other local users canpredict, and lets
tarcreate it:Nothing creates that file first, so the
spawnSync("tar", ...)atsrc/lib/diagnostics/tarball.ts:29chooses its mode and the success path renames it tooutputat
src/lib/diagnostics/tarball.ts:51. Two things follow, both observed onmain(337c1eed5)with the default
umask 022:The bundle is published group- and world-readable —
bundle mode: 644. That is the file thecommand tells users to attach to a GitHub issue
(
src/lib/diagnostics/debug.ts:514-517).tarfollows a symlink planted at the staging path. After planting one and lettingtarwrite, the symlink target began
037 213— the gzip magic number, i.e. it was overwritten withtarball content.
Both are reachable because the command's own example writes into a world-writable directory
(
src/commands/debug.ts:88):There, any local user can read the bundle, or pre-plant a symlink at
/tmp/nemoclaw-debug.tar.gz.partial.<pid>— a small, enumerable space — to have a file of theirchoosing overwritten with the tool's privileges.
Why this looks like drift
The repository already stages temp files safely:
src/lib/shields/timer.ts:167usesfs.openSync(tempPath, "wx", 0o600),src/lib/share-command.ts:233uses{ mode: 0o600, flag: "wx" }, andsrc/lib/shields/index.ts:393usesO_EXCL | O_NOFOLLOW.The staging path here arrived in #4506 (
f8c85f37e), whose stated goal was to leave no partialtarball and rename atomically. Permissions and predictability were not in that change's scope.
Environment
macOS 26.5.1 (25F80), Node v26.7.0,
mainat337c1eed5.