fs.cpSync fails with EACCES when the destination is on a Docker bind mount (regression in 22.17)
Dieses Issue hat noch niemand übernommen.
- Vorherrschende Sprache
- JavaScript
- Sterne
- 122k
- Forks
- 37.3k
- Ø Merge
- 4 T. 2 Std.
- Gemergte PRs (30 T.)
- 283
Beschreibung
Version
v22.17.1 (also v22.18.0, v22.20.0, v22.23.2, v24.19.0). Last good: v22.16.0.
Platform
Docker Desktop on macOS (Darwin 25.5.0, arm64), default VirtioFS file sharing. Reproduced with the official node:*-slim images, so the container distro is Debian bookworm.
Subsystem
fs
What steps will reproduce the bug?
fs.cpSync(src, dest, { recursive: true }) throws EACCES when dest is on a Docker bind mount. The same call succeeds on 22.16.0, and on 22.17.1 it succeeds when dest is on the container's own filesystem.
On a macOS host:
mkdir -p /tmp/repro/src/nested
printf 'a' > /tmp/repro/src/file.txt
printf 'b' > /tmp/repro/src/nested/inner.txt
for V in 22.16 22.17; do
docker run --rm -v /tmp/repro:/mnt/repro -w /mnt/repro node:$V-slim node -e "
const fs = require('fs');
const dest = 'out-' + process.version.replace(/[^0-9]/g, '_');
try { fs.cpSync('src', dest, { recursive: true }); console.log(process.version, 'OK'); }
catch (e) { console.log(process.version, 'FAIL', e.code, e.syscall, e.path); }
"
done
Output:
v22.16.0 OK
v22.17.1 FAIL EACCES cp out-_22_17_1
How often does it reproduce?
Every time, on every version from 22.17.1 onward that I tested.
What is the expected behavior?
cpSync copies the tree, as it does on 22.16.0 and as cp -R does on the same path in the same container.
What do you see instead?
Error: EACCES: permission denied, cp 'out-_22_17_1'
Additional information
This is not a permission problem. In a single container run on 22.17.1, with the bind mount as CWD:
node v22.17.1
FAIL dest on bind mount EACCES cp bind-dest
OK dest on container fs
control: id=0 access(W_OK)=yes
control: mkdirSync on bind mount OK
control: copyFileSync on bind mount OK
control: cp -R OK
So, on the very path that cpSync rejects:
- the process is root (uid 0) — it also fails as a non-root uid matching the host owner;
fs.accessSync(dir, W_OK)succeeds;fs.mkdirSyncsucceeds;fs.copyFileSyncsucceeds;- coreutils
cp -Rsucceeds; fs.cpSyncwith the same source and a destination on the container's own filesystem succeeds.
Only the combination of cpSync + a bind-mount destination fails.
cpSync does partially complete before throwing: the destination directory is created and at least one entry is copied, so a retry then sees EEXIST/ENOTEMPTY. That makes the failure messy to recover from in a build pipeline.
Bisect (identical mount, source, destination and uid; only the Node binary varies):
| Version | Result |
|---|---|
| 22.13.1 | OK |
| 22.14.0 | OK |
| 22.16.0 | OK |
| 22.17.1 | EACCES |
| 22.18.0 | EACCES |
| 22.20.0 | EACCES |
| 22.23.2 | EACCES |
| 24.19.0 | EACCES |
The boundary lines up with the C++ reimplementation of cpSync, which landed in 22.17. The mechanism is below.
Real-world impact. @nx/next:build finishes by copying the app's public/ directory into its output path with exactly this call:
cpSync(join(projectRoot, 'public'), join(options.outputPath, 'public'), {
dereference: true,
recursive: true,
});
Any Next.js build run inside a container against a bind-mounted workspace therefore fails on Node ≥ 22.17 — after the build has already compiled and typechecked successfully, so it presents as a mysterious late-stage permission error rather than an obvious environment problem. Related historical regressions in the same function: #54285, #54782.
Root cause. Reduced to raw syscalls in the container, as root:
FAIL open dest O_WRONLY|O_CREAT|O_TRUNC mode 0200 errno=13 (Permission denied)
OK open dest O_WRONLY|O_CREAT|O_TRUNC mode 0644
FAIL reopen the 0200 file O_WRONLY errno=13 (Permission denied)
libstdc++'s do_copy_file creates the destination with S_IWUSR (0200) and fchmods it to the source's mode afterwards. This mount refuses to open a mode-0200 file for writing, so the create fails. uv_fs_copyfile creates the destination with the source's mode and is unaffected.
That is confirmed outside Node entirely — plain C++ with gcc 14 and libstdc++, same mount:
FAIL copy_file mount -> mount value=13 msg=Permission denied
OK copy_file mount -> /tmp
OK copy_file /tmp -> /tmp
FAIL copy_file /tmp -> mount value=13 msg=Permission denied
Which is why cpSync is inconsistent with itself, on one mount in one process:
| Call | Result | Path taken |
|---|---|---|
cpSync(file, newDest) |
OK | JS falls through to copyFileSync (libuv) |
cpSync(file, existingDest) |
EACCES | CpSyncOverrideFile, mode == 0 branch |
cpSync(dirWithAFile, dest) |
EACCES | CpSyncCopyDir, regular-file branch |
cpSync(dirNoFiles, dest) |
OK | no file is ever copied |
fs.copyFileSync works on this mount because it uses uv_fs_copyfile. Both failing paths already have a libuv sibling in node_file.cc; they just do not take it.
Workaround. Pin to 22.16.x, or arrange for the destination to live on the container's own filesystem rather than the bind mount.
Beitragsleitfaden
Erste Schritte
- Lies das ganze Issue und danach den Beitragsleitfaden des Projekts.
- Schreib ins Issue, dass du es übernimmst — das erspart doppelte Arbeit.
- Forke das Repository und arbeite in einem Branch.
- Öffne einen Pull Request, der die Issue-Nummer nennt.
Rechercherichtung
Beginne in node_file.cc bei den im Bericht genannten C++-cpSync-Codepfaden, insbesondere bei den Branches für reguläre Dateien, die das Kopieren von Dateien mit libstdc++ statt des bestehenden libuv-Pendants verwenden. Reproduziere das Problem mit den Docker-Bind-Mount-Befehlen und vergleiche cpSync mit copyFileSync. Als erledigt gilt die Aufgabe, wenn rekursives cpSync Dateien auf dem betroffenen Bind-Mount erfolgreich kopiert, ohne dass der Fehler durch eine teilweise Kopie auftritt.
Vom Indexierungsmodell aus dem Issue-Text verfasst.
Bewertung
- Tech-Stack
- cpp, javascript, node.js
- Bereich
- backend, operating-systems
- Issue-Typ
- Bug
- Schwierigkeit
- 4/5
- Geschätzter Aufwand
- 3-5 Tage
- Aktivitätsstatus
- Aktiv
- Klarheit
- Klar beschrieben
- Anfängerfreundlichkeit
- 68/100