Skip to content

fix(npm): reject symlinked package materialization dirs - #36191

Merged
nathanwhit merged 2 commits into
denoland:mainfrom
nathanwhit:fix/npm-materialization-symlink-dirs
Jul 21, 2026
Merged

nathanwhit merged 2 commits into
denoland:mainfrom
nathanwhit:fix/npm-materialization-symlink-dirs

Conversation

@nathanwhit

Copy link
Copy Markdown
Member

Summary

  • reject symlinked package materialization destinations
  • apply the check to both hard-link and copy paths
  • cover both helpers with in-memory filesystem regressions

Details

Package initialization previously treated an existing symlinked destination
as a directory. Creating the destination and then hard-linking or copying
package files could therefore write through that redirected path.

Materialization now checks each destination directory before and after
creation using symlink-aware metadata. Existing ordinary directories continue
to work as before, while a symlinked destination returns an ordinary
filesystem error.

Validation

  • ./tools/format.js --check
  • cargo test -p deno_npm_cache hard_link_dir_recursive -- --nocapture
  • cargo test -p deno_npm_installer copy_dir_recursive -- --nocapture

AI assistance was used to help implement and test this change.

@bartlomieju bartlomieju left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice fix β€” the ensure_not_symlink before/after create_dir_all approach is right, and covering all three helpers with in-memory-fs regressions is great. Two things worth considering:

1. clone_dir_recursive_except_node_modules_child (local.rs) has the same vector but isn't hardened. It's used for patch packages and workspace members (local.rs:582, hoisted.rs:536) and still does let _ = fs_remove_dir_all(to) then fs_create_dir_all(to) then hard_link_file/fs_copy with no symlink check. On Linux, remove_dir_all on a symlinked to fails with NotADirectory and the error is discarded, so the symlink stays, create_dir_all succeeds (target is a dir), and package files get written through the redirected path β€” the exact write-through this PR closes in the other three helpers. Worth routing this destination through create_dir_all_no_symlink/ensure_not_symlink too so the fix is complete.

2. Minor: ensure_not_symlink/create_dir_all_no_symlink are duplicated verbatim in both libs/npm_cache/fs_util.rs and libs/npm_installer/fs.rs. Since npm_installer already depends on npm_cache (it calls deno_npm_cache::hard_link_dir_recursive), these could be exported once from npm_cache and reused rather than kept in two places where they can drift. Non-blocking.

@nathanwhit
nathanwhit merged commit 9ec3275 into denoland:main Jul 21, 2026
136 checks passed
bartlomieju pushed a commit that referenced this pull request Jul 23, 2026
## Summary

- reject symlinked package materialization destinations
- apply the check to both hard-link and copy paths
- cover both helpers with in-memory filesystem regressions

## Details

Package initialization previously treated an existing symlinked
destination
as a directory. Creating the destination and then hard-linking or
copying
package files could therefore write through that redirected path.

Materialization now checks each destination directory before and after
creation using symlink-aware metadata. Existing ordinary directories
continue
to work as before, while a symlinked destination returns an ordinary
filesystem error.

## Validation

- `./tools/format.js --check`
- `cargo test -p deno_npm_cache hard_link_dir_recursive -- --nocapture`
- `cargo test -p deno_npm_installer copy_dir_recursive -- --nocapture`

AI assistance was used to help implement and test this change.
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.

2 participants