Skip to content

[NemoClaw 0.0.22] snapshot create/rebuild/backup-all blocked by safeTarExtract symlink audit on internal .openclaw-data/* symlinks (PR #2163) #2317

@shidsaa

Description

@shidsaa

Description

nemoclaw <sandbox> rebuild, nemoclaw <sandbox> snapshot create, and nemoclaw backup-all all fail with:

SECURITY: tar extraction blocked: post-extraction symlink audit failed:
symlink escape: /<host>/.nemoclaw/rebuild-backups/<sandbox>/<timestamp>/workspace/media
             -> /sandbox/.openclaw-data/media
             (resolves to /sandbox/.openclaw-data/media)

Every state subsystem fails together because the audit aborts the entire extraction on the first symlink found:

Failed directories: agents, extensions, workspace, skills, hooks, identity,
                    devices, canvas, cron, memory, telegram, credentials

Net effect: no sandbox can be rebuilt or snapshotted in 0.0.22. The only recovery path is nemoclaw onboard --recreate-sandbox which loses state entirely.

Why it happens

PR #2163 merged 2026-04-21 (the day before the 0.0.22 tag) hardened src/lib/sandbox-state.ts against tar-slip path traversal (a real HIGH-severity class vuln where a compromised sandbox could write ../../.ssh/authorized_keys-style entries to the host). The new safeTarExtract() function calls fs.realpathSync() on every extracted symlink and rejects any target falling outside the extraction root.

That audit correctly catches the path-traversal class, but over-rejects absolute symlinks whose target exists legitimately inside the sandbox's writable dir. The workspace/media symlink is created at image build time by Dockerfile.base (pattern established in PR #555):

/sandbox/.openclaw/workspace/media -> /sandbox/.openclaw-data/media

This absolute path is valid inside the sandbox, but when the backup is extracted on the host, the target (/sandbox/.openclaw-data/media) doesn't exist on the host and certainly isn't inside the extraction root — so the audit flags it as a true escape.

Same pattern affects at least: workspace/media, exec-approvals.json, telegram/, credentials/, canvas/, cron/, memory/, skills/, flows/, hooks/, identity/, devices/, agents/, extensions/ — essentially every symlink set up by the .openclaw / .openclaw-data split.

Suggested fix

Three viable approaches (ordered by how much I'd recommend them):

Option A — Resolve symlink targets relative to the archive's extraction root

// in src/lib/sandbox-state.ts safeTarExtract post-extraction audit
const target = fs.readlinkSync(symlinkPath);
const resolvedTarget = path.isAbsolute(target)
  ? path.resolve(extractRoot, target.replace(/^\//, ''))  // strip leading / and resolve under extractRoot
  : path.resolve(path.dirname(symlinkPath), target);
if (!resolvedTarget.startsWith(extractRoot)) {
  // true escape — block
}

This preserves the security guarantee (still blocks paths that would actually escape) while accepting absolute paths that legitimately point into the sandbox-internal dir structure.

Option B — Explicit allowlist of sandbox-internal prefixes

const ALLOWED_INTERNAL_PREFIXES = ['/sandbox/.openclaw-data/'];
if (path.isAbsolute(target) && ALLOWED_INTERNAL_PREFIXES.some(p => target.startsWith(p))) {
  // known-safe internal symlink — allow
  continue;
}

Coordinate the prefix list with the agent manifest's writableDir so it stays in sync.

Option C — Rewrite absolute symlinks to relative at backup time

Before creating the tar archive, traverse the source tree and replace any absolute symlink like workspace/media -> /sandbox/.openclaw-data/media with a relative one workspace/media -> ../media. Avoids the issue entirely and is more portable across host vs sandbox paths. Most invasive change but cleanest long-term.

Relationship to PR #2227

That PR's mutable-default refactor would eliminate the .openclaw / .openclaw-data split entirely, removing the symlinks that trip the audit. If that lands soon, Option A/B might only need to live long enough to un-brick users who can't immediately migrate to the refactored layout.

Reproduction Steps

  1. Fresh install NemoClaw 0.0.22 via curl -fsSL https://www.nvidia.com/nemoclaw.sh | bash. Complete onboarding (any sandbox name, e.g. testbot).
  2. Run a snapshot:
    NEMOCLAW_REBUILD_VERBOSE=1 nemoclaw testbot snapshot create
  3. Expected: a snapshot is created at ~/.nemoclaw/rebuild-backups/testbot/<timestamp>/ with the 12 state directories.
  4. Actual:
    Creating snapshot of 'testbot'...
    [sandbox-state] backupSandboxState: agent=openclaw, writableDir=/sandbox/.openclaw-data, stateDirs=[...]
    [sandbox-state] Existing dirs in sandbox: [agents,extensions,workspace,skills,hooks,identity,
                    devices,canvas,cron,memory,telegram,credentials] (12/12)
    [sandbox-state] Downloading via SSH+tar: tar -cf - -C /sandbox/.openclaw-data agents ...
    [sandbox-state] SSH+tar download: exit=0, stdout=184320 bytes, stderr=
    [sandbox-state] SECURITY: tar extraction blocked: post-extraction symlink audit failed:
                    symlink escape: .../workspace/media -> /sandbox/.openclaw-data/media
                    (resolves to /sandbox/.openclaw-data/media)
    Snapshot failed.
    Failed directories: agents, extensions, workspace, skills, hooks, identity, devices,
                        canvas, cron, memory, telegram, credentials
    

Same failure for nemoclaw testbot rebuild and nemoclaw backup-all.

Environment

Hardware:                NVIDIA DGX Spark (GB10, aarch64) — but this affects every platform
OS:                      Ubuntu 24.04 LTS
NemoClaw CLI:            v0.0.22 (symlink-audit regression introduced by PR #2163 merged 2026-04-21)
OpenClaw (bundled):      2026.4.2 (d74a122)
Cluster image:           ghcr.io/nvidia/openshell/cluster:0.0.29
Install method:          curl -fsSL https://www.nvidia.com/nemoclaw.sh | bash

Debug Output

List of absolute symlinks inside a fresh sandbox's `/sandbox/.openclaw/` that all point into the writable dir and will trip the audit:


$ openshell doctor exec -- kubectl exec -n openshell testbot --container agent -- \
    ls -la /sandbox/.openclaw/ | awk '$1 ~ /^l/ { print }'
lrwxrwxrwx 1 root root 30 agents              -> /sandbox/.openclaw-data/agents
lrwxrwxrwx 1 root root 30 canvas              -> /sandbox/.openclaw-data/canvas
lrwxrwxrwx 1 root root 35 credentials         -> /sandbox/.openclaw-data/credentials
lrwxrwxrwx 1 root root 28 cron                -> /sandbox/.openclaw-data/cron
lrwxrwxrwx 1 root root 31 devices             -> /sandbox/.openclaw-data/devices
lrwxrwxrwx 1 root root 43 exec-approvals.json -> /sandbox/.openclaw-data/exec-approvals.json
lrwxrwxrwx 1 root root 34 extensions          -> /sandbox/.openclaw-data/extensions
lrwxrwxrwx 1 root root 29 flows               -> /sandbox/.openclaw-data/flows
lrwxrwxrwx 1 root root 29 hooks               -> /sandbox/.openclaw-data/hooks
lrwxrwxrwx 1 root root 32 identity            -> /sandbox/.openclaw-data/identity
lrwxrwxrwx 1 root root 28 logs                -> /sandbox/.openclaw-data/logs
lrwxrwxrwx 1 root root 29 media               -> /sandbox/.openclaw-data/media
lrwxrwxrwx 1 root root 30 memory              -> /sandbox/.openclaw-data/memory
lrwxrwxrwx 1 root root 31 sandbox             -> /sandbox/.openclaw-data/sandbox
lrwxrwxrwx 1 root root 30 skills              -> /sandbox/.openclaw-data/skills
lrwxrwxrwx 1 root root 32 telegram            -> /sandbox/.openclaw-data/telegram
lrwxrwxrwx 1 root root 41 update-check.json   -> /sandbox/.openclaw-data/update-check.json
lrwxrwxrwx 1 root root 33 workspace           -> /sandbox/.openclaw-data/workspace


Each of these will trigger `safeTarExtract`'s audit as a false-positive escape, so the first one found aborts the whole extraction.

Logs

`NEMOCLAW_REBUILD_VERBOSE=1` shell capture above (same as "Actual" section of Reproduction Steps).

Optional: attach `nemoclaw debug --output /tmp/nemoclaw-debug.tar.gz --sandbox testbot` tarball — includes the full rebuild log and config audit trail.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: cliCommand line interface, flags, terminal UX, or output

    Type

    No fields configured for Bug.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions