Skip to content

Fix EPERM on WSL2 9P + EFS by replacing fs.copyFileSync with read+write shim#33

Open
WeathermanTony wants to merge 1 commit into
cytostack:mainfrom
WeathermanTony:fix/copyfile-efs-wsl2
Open

Fix EPERM on WSL2 9P + EFS by replacing fs.copyFileSync with read+write shim#33
WeathermanTony wants to merge 1 commit into
cytostack:mainfrom
WeathermanTony:fix/copyfile-efs-wsl2

Conversation

@WeathermanTony
Copy link
Copy Markdown

Summary

openwolf init and openwolf update fail with EPERM when run from WSL2 on a Windows path that's EFS-encrypted. Root cause is not OpenWolf — it's a libuv quirk: fs.copyFileSync uses the copy_file_range syscall on Linux as a fast path, and that syscall fails on the WSL2 9P filesystem when the destination directory has the NTFS Encrypted attribute. Plain read()+write() works fine in the same conditions.

This PR adds a safeCopyFile shim in utils/fs-safe.ts (matching the existing writeJSON/writeText safe-write pattern) and routes all 12 fs.copyFileSync call sites in cli/init.ts and cli/update.ts through it.

Reproduction

# Windows side
mkdir J:\test-efs
cipher /e J:\test-efs
# WSL2 side
cd /mnt/j/test-efs
openwolf init
# -> Error: EPERM: operation not permitted, copyfile '.../OPENWOLF.md'

After this PR: init and update succeed; new files correctly inherit the parent's Encrypted attribute via standard NTFS inheritance.

Why a shim instead of switching to streams or cp

  • Matches the existing fs-safe.ts style (already wraps writeFileSync for the Windows-rename quirk)
  • Synchronous, drop-in for fs.copyFileSync — every call site is a one-token rename
  • Preserves file mode (best-effort) for the hook scripts
  • read+write is fine for the file sizes OpenWolf copies (small templates and hook scripts; not multi-GB blobs where copy_file_range would actually matter)

Verified isolation of the bug

WSL `echo > new_file.txt` to encrypted dir   → OK, file inherits Encrypted=True
WSL `echo > existing.txt`  to encrypted file → OK, overwrite works
WSL `cp src dest` (overwrite)                → OK, plain read+write
Node `fs.copyFileSync(src, new_dest)`        → FAIL EPERM
Node `fs.copyFileSync(src, existing)`        → FAIL EPERM
Node `fs.writeFileSync(d, fs.readFileSync(s))` → OK (this is the shim)

Test plan

  • Reproduce the original failure on a fresh EFS-encrypted directory under /mnt/j/...
  • Apply the patch, confirm openwolf init runs to completion (13 template files copied, hooks registered, daemon started)
  • Verify newly-written files inherit the parent's Encrypted attribute
  • pnpm exec tsc builds clean with no new diagnostics
  • Reviewer: please run on a non-WSL/non-EFS environment to confirm no regression — the shim is functionally equivalent to copyFileSync for the contents OpenWolf copies (small text/JS files), so this should be a no-op on any platform

🤖 Generated with Claude Code

fs.copyFileSync (and the underlying libuv uv_fs_copyfile) uses Linux's
copy_file_range syscall as a fast path. That syscall fails with EPERM
when the destination is on a Windows volume mounted via WSL2 9P AND
the destination directory has the EFS Encrypted attribute. This makes
`openwolf init` and `openwolf update` unusable on any Windows-EFS path
opened from WSL.

Plain read+write avoids copy_file_range and works in all cases. Add
safeCopyFile to utils/fs-safe.ts (matching the existing safe-write
pattern) and replace all 12 fs.copyFileSync call sites in cli/init.ts
and cli/update.ts.

Reproduction:
  1. On Windows, mark a directory EFS-encrypted (cipher /e <dir>)
  2. Open WSL2, cd into the directory via /mnt/<drive>
  3. openwolf init
  -> EPERM at fs.copyFileSync of OPENWOLF.md

After this change: init and update succeed; new files inherit the
parent's Encrypted attribute correctly via standard NTFS inheritance.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant