Summary
createBackup() with a multi-component exclude pattern (e.g. excludes: ['.cache/foo/*/node_modules']) silently drops the entire first path component (.cache/) from the archive — not just the matched subpath. The backup reports success and the archive looks valid, so the data loss only surfaces on a later restoreBackup(), by which point the original container filesystem may be gone.
We hit this in production-like usage: our workspace's .entrydesk/ directory (application state) vanished from every backup because we passed excludes: ['node_modules', '.entrydesk/sites/*/node_modules']. Every sleep→wake cycle then restored a workspace missing that tree.
Root cause
Two ingredients, both in this repo:
-
packages/sandbox-container/src/services/backup-service.ts expands every user exclude P into [P, ... ${P}] before writing the -ef file for mksquashfs -wildcards:
const userExcludePatterns = normalizedExcludes.flatMap((pattern) => [
pattern,
`... ${pattern}`
]);
The gitignore-derived patterns take the same expansion (relativePaths.flatMap((path) => [path, ... ${path}])), and gitignore paths are very often multi-component (dist/foo, packages/x/build), so gitignore: true is affected even more broadly.
-
The container image (Dockerfile, FROM ubuntu:22.04) ships squashfs-tools 4.5, which has a matching bug: a non-anchored ... a/b pattern excludes the whole first component a at the archive root, instead of only a/b. squashfs-tools 4.6.1 (ubuntu 24.04) and 4.7 behave correctly.
Reproduction (inside the sandbox container image, mksquashfs 4.5)
mkdir -p /ws/alpha/beta /ws/keep
touch /ws/alpha/file.txt /ws/keep/file.txt
printf '%s\n' '... alpha/beta/gamma' > /tmp/ex.txt # note: gamma does not even exist
mksquashfs /ws /tmp/out.sqsh -no-progress -wildcards -ef /tmp/ex.txt
unsquashfs -l /tmp/out.sqsh
# squashfs-root
# squashfs-root/keep
# squashfs-root/keep/file.txt
# -> alpha/ is gone entirely
Same commands on ubuntu:24.04 (squashfs-tools 4.6.1) keep alpha/ and only exclude the named subpath. Verified on @cloudflare/sandbox 0.12.3; the expansion is unchanged on main as of today.
Impact
- Any
BackupOptions.excludes entry containing / silently truncates the archive at its first path component.
gitignore: true can drop arbitrary top-level directories whenever an ignored path is nested.
- The failure is invisible at backup time (
createBackup succeeds, plausible archive size) and destructive at restore time.
Suggested fixes
Either (ideally both):
- Don't emit the
... P variant for patterns containing /. Anchored multi-component patterns already match from the source root; the recursive variant is only meaningful for single-component names. This fixes the data loss even on mksquashfs 4.5.
- Bump the base image / squashfs-tools to ≥ 4.6 so non-anchored multi-component patterns behave per the documented semantics.
A backup-time integrity check (e.g. verifying a known sentinel file made it into the archive) would also turn this class of bug from silent data loss into a loud failure, but that's a separate hardening suggestion.
Summary
createBackup()with a multi-component exclude pattern (e.g.excludes: ['.cache/foo/*/node_modules']) silently drops the entire first path component (.cache/) from the archive — not just the matched subpath. The backup reports success and the archive looks valid, so the data loss only surfaces on a laterrestoreBackup(), by which point the original container filesystem may be gone.We hit this in production-like usage: our workspace's
.entrydesk/directory (application state) vanished from every backup because we passedexcludes: ['node_modules', '.entrydesk/sites/*/node_modules']. Every sleep→wake cycle then restored a workspace missing that tree.Root cause
Two ingredients, both in this repo:
packages/sandbox-container/src/services/backup-service.tsexpands every user excludePinto[P,... ${P}]before writing the-effile formksquashfs -wildcards:The gitignore-derived patterns take the same expansion (
relativePaths.flatMap((path) => [path,... ${path}])), and gitignore paths are very often multi-component (dist/foo,packages/x/build), sogitignore: trueis affected even more broadly.The container image (
Dockerfile,FROM ubuntu:22.04) ships squashfs-tools 4.5, which has a matching bug: a non-anchored... a/bpattern excludes the whole first componentaat the archive root, instead of onlya/b. squashfs-tools 4.6.1 (ubuntu 24.04) and 4.7 behave correctly.Reproduction (inside the sandbox container image, mksquashfs 4.5)
Same commands on ubuntu:24.04 (squashfs-tools 4.6.1) keep
alpha/and only exclude the named subpath. Verified on@cloudflare/sandbox0.12.3; the expansion is unchanged onmainas of today.Impact
BackupOptions.excludesentry containing/silently truncates the archive at its first path component.gitignore: truecan drop arbitrary top-level directories whenever an ignored path is nested.createBackupsucceeds, plausible archive size) and destructive at restore time.Suggested fixes
Either (ideally both):
... Pvariant for patterns containing/. Anchored multi-component patterns already match from the source root; the recursive variant is only meaningful for single-component names. This fixes the data loss even on mksquashfs 4.5.A backup-time integrity check (e.g. verifying a known sentinel file made it into the archive) would also turn this class of bug from silent data loss into a loud failure, but that's a separate hardening suggestion.