Files
vercel__next.js/test/e2e/app-dir/webpack-loader-fs/test-file-loader.js
Tobias Koppers 17a901a74f Guard filesystem reads against unresolved symlinks (#97902)
### What?

Adds debug-only OS realpath validation to successful `DiskFileSystem`
file and directory reads. When a successfully canonicalized path differs
from the supplied path, the read returns a normal task error naming both
paths.

Fixes pattern/glob traversal and NFT tracing so physical filesystem
access uses resolved paths while logical paths remain available for
user-visible specifiers and complete symlink-chain recovery.

### Why?

Reading through an unresolved symlink parent gives the same filesystem
object multiple path identities. That can make Turbo Tasks dependency
tracking and invalidation inconsistent and can produce invalid
deployment ZIPs when NFT output contains files below unresolved links.

The checks return errors rather than asserting because paths can
disagree temporarily under eventual consistency. Propagating a task
error avoids panicking a worker thread while still exposing invalid
callers during development.

### How?

The validation lives directly in `DiskFileSystem::read` and
`DiskFileSystem::raw_read_dir`. It calls the OS canonicalization API
inline instead of the Turbo Tasks realpath task, keeping the diagnostic
out of the task dependency graph. The guard runs only after the OS read
succeeds, so missing/non-directory probes preserve their existing
behavior.

`read_matches` resolves each physical directory immediately before
enumeration while retaining logical `PatternMatch` paths.

`read_glob` and `track_glob` now resolve their initial directory before
enumeration. Symlinks discovered later through wildcard segments are
also traversed through resolved targets. `ReadGlobResult` deliberately
retains logical paths rooted at the supplied base, allowing consumers to
call `realpath_with_links` and recover the complete symlink chain.

Consumers follow that contract explicitly:

- NFT includes expand each logical match with `realpath_with_links`,
emit resolved files and every traversed symlink, skip resolved directory
targets, and deterministically deduplicate/sort output.
- `import.meta.glob` uses recursive logical keys as the source of
user-visible requests, while module resolution follows and tracks
symlinks.
- the hash-glob example resolves returned logical paths before reading.

Webpack-loader context dependencies are covered for both
`path/to/symlink/inner/path/*` and `path/to/*/inner/path/*`. The loader
fixture performs its directory read with Node `fs`, reports the
directory using `addContextDependency`, and Turbopack tracks the
resolved target.

### Verification

- `cargo fmt -p turbo-tasks-fs -p turbopack-ecmascript -p next-api --
--check`
- `cargo clippy -p turbo-tasks-fs -p turbopack-ecmascript -p next-api
--all-targets`
- `cargo test -p turbo-tasks-fs` (128 passed)
- `cargo test -p next-api` (7 passed)
- `cargo check -p turbo-tasks-fs --examples`
- `import.meta.glob` symlink execution fixture (1 passed)
- Nine targeted node-file-trace CI cases with `release-with-assertions`
(9 passed)
- `pnpm build-all`
- `webpack-loader-fs` Turbopack dev e2e (1 passed)
- `build-trace-extra-entries-turbo` Turbopack production e2e (1 passed)
- twoslash Turbopack production, normal mode (4 passed)
- twoslash Turbopack production, cache-components mode (4 passed)
- `bench/heavy-npm-deps` Turbopack development smoke test (HTTP 200)

### Notes

The disk guard is cross-platform, while its symlink-parent regression
test is Unix-only, matching neighbouring symlink tests. On Windows, OS
canonicalization can also normalize casing and 8.3 short names; a debug
read using a non-canonical spelling will therefore return the same
diagnostic error.

<!-- NEXT_JS_LLM -->


<!-- fleet 1d32e12c-f4ec-4f22-862a-c85f0005805c -->

---------

Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
2026-08-28 08:31:06 +02:00

35 lines
1.1 KiB
JavaScript

const fs = require('fs')
const path = require('path')
module.exports = async function (content) {
let dir = path.dirname(this.resourcePath)
let read1 = await new Promise((res, rej) =>
this.fs.readFile(path.join(dir, 'test.txt'), (err, data) => {
if (err) return rej(err)
res(data)
})
)
let read2 = await new Promise((res, rej) =>
this.fs.readFile(path.join(dir, 'test.txt'), 'utf8', (err, data) => {
if (err) return rej(err)
res(data)
})
)
let read3 = await new Promise((res, rej) =>
this.fs.readFile(path.join(dir, 'test.mp4'), (err, data) => {
if (err) return rej(err)
res(data)
})
)
const globDir = path.join(dir, 'path/to/symlink/inner/path')
const globEntries = await new Promise((res, rej) =>
fs.readdir(globDir, (err, entries) => {
if (err) return rej(err)
res(entries)
})
)
this.addContextDependency(globDir)
return `module.exports = "Buffer read: ${read1 instanceof Buffer ? read1.length : 0}, string read: '${read2.trim()}', binary read: ${read3.length}, glob read: '${globEntries.sort().join(',')}'"`
}