Skip to content

Guard filesystem reads against unresolved symlinks - #97902

Draft
sokra wants to merge 5 commits into
canaryfrom
fix/readdir-realpath-assertion
Draft

Guard filesystem reads against unresolved symlinks#97902
sokra wants to merge 5 commits into
canaryfrom
fix/readdir-realpath-assertion

Conversation

@sokra

@sokra sokra commented Aug 25, 2026

Copy link
Copy Markdown
Member

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 that names both paths.

Fixes pattern and glob traversal so physical directory enumeration uses resolved paths while returned matches keep their logical, symlink-relative spelling.

Why?

Reading a directory through an unresolved symlink parent gives the same filesystem object multiple path identities. That can make Turbo Tasks dependency tracking and invalidation inconsistent. These paths are expected to be resolved before successful reads, matching the resolve-parent-first behavior used by node-file-trace.

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: missing paths, non-directories, and invalid filenames preserve their existing NotFound behavior, including deliberate directory probes on symlinks to files.

read_matches resolves each physical directory immediately before enumeration in both its finite-constant fast path and recursive slow path. It still constructs PatternMatch values from the original logical lookup path, preserving request keys, trace output, and leaf-level symlink affecting sources. Resolving the enumeration path also tracks the parent symlink chain so replacing a link invalidates the walk.

read_glob similarly enumerates the already-resolved directory target while rebuilding returned entries relative to the original glob root. This keeps direct, chained, and unresolvable symlink entries consistently logical.

Verification

  • cargo fmt -p turbo-tasks-fs -p turbopack-core -- --check
  • cargo clippy -p turbo-tasks-fs -p turbopack-core --all-targets
  • cargo test -p turbo-tasks-fs (127 passed)
  • cargo test -p turbopack-core (155 passed)
  • Nine targeted node-file-trace CI cases with release-with-assertions (9 passed): esbuild, ffmpeg installer, loopback/socket.io, four sharp variants, and asset symlink
  • cargo check -p turbo-tasks-fs --release
  • pnpm build-all
  • 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.

@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Failing test suites

Commit: 43d169f | About building and testing Next.js

pnpm test-start-experimental-turbo test/e2e/twoslash/index.test.ts (turbopack) (Experimental) (job)

  • twoslash > should annotate twoslash types default (DD)
  • twoslash > should annotate twoslash types esnext (DD)
Expand output

● twoslash › should annotate twoslash types default

next build failed with code/signal 1

  135 |             if (code || signal)
  136 |               reject(
> 137 |                 new Error(
      |                 ^
  138 |                   `next build failed with code/signal ${code || signal}`
  139 |                 )
  140 |               )

  at ChildProcess.<anonymous> (lib/next-modes/next-start.ts:137:17)

● twoslash › should annotate twoslash types esnext

next build failed with code/signal 1

  135 |             if (code || signal)
  136 |               reject(
> 137 |                 new Error(
      |                 ^
  138 |                   `next build failed with code/signal ${code || signal}`
  139 |                 )
  140 |               )

  at ChildProcess.<anonymous> (lib/next-modes/next-start.ts:137:17)

pnpm test-start-experimental-turbo test/e2e/twoslash/standalone.test.ts (turbopack) (Experimental) (job)

  • output: standalone with twoslash > should annotate twoslash types default (DD)
  • output: standalone with twoslash > should annotate twoslash types esnext (DD)
Expand output

● output: standalone with twoslash › should annotate twoslash types default

ENOENT: no such file or directory, stat '/tmp/next-install-295136c6376a685c7061c5bba84f82805263f1221868c7f1f24524c79aed9867/.next/standalone'

● output: standalone with twoslash › should annotate twoslash types esnext

ENOENT: no such file or directory, stat '/tmp/next-install-295136c6376a685c7061c5bba84f82805263f1221868c7f1f24524c79aed9867/.next/standalone'

pnpm test-start-turbo test/production/build-trace-extra-entries-turbo/build-trace-extra-entries-turbo.test.ts (turbopack) (job)

  • build trace with extra entries > production mode > should build and trace correctly (DD)
Expand output

● build trace with extra entries › production mode › should build and trace correctly

expect(received).toBe(expected) // Object.is equality

Expected: 0
Received: 1

  13 |     it('should build and trace correctly', async () => {
  14 |       const { exitCode } = await next.build()
> 15 |       expect(exitCode).toBe(0)
     |                        ^
  16 |
  17 |       const appTrace = JSON.parse(
  18 |         await next.readFile('.next/server/pages/_app.js.nft.json')

  at Object.toBe (production/build-trace-extra-entries-turbo/build-trace-extra-entries-turbo.test.ts:15:24)

pnpm test-start-turbo test/e2e/twoslash/standalone.test.ts (turbopack) (job)

  • output: standalone with twoslash > should annotate twoslash types default (DD)
  • output: standalone with twoslash > should annotate twoslash types esnext (DD)
Expand output

● output: standalone with twoslash › should annotate twoslash types default

ENOENT: no such file or directory, stat '/tmp/next-install-e7559082920480339e16a5adc3ea8930c0440942d3bbabb3ea12f30da813092e/.next/standalone'

● output: standalone with twoslash › should annotate twoslash types esnext

ENOENT: no such file or directory, stat '/tmp/next-install-e7559082920480339e16a5adc3ea8930c0440942d3bbabb3ea12f30da813092e/.next/standalone'

pnpm test-start-turbo test/e2e/twoslash/index.test.ts (turbopack) (job)

  • twoslash > should annotate twoslash types default (DD)
  • twoslash > should annotate twoslash types esnext (DD)
Expand output

● twoslash › should annotate twoslash types default

next build failed with code/signal 1

  135 |             if (code || signal)
  136 |               reject(
> 137 |                 new Error(
      |                 ^
  138 |                   `next build failed with code/signal ${code || signal}`
  139 |                 )
  140 |               )

  at ChildProcess.<anonymous> (lib/next-modes/next-start.ts:137:17)

● twoslash › should annotate twoslash types esnext

next build failed with code/signal 1

  135 |             if (code || signal)
  136 |               reject(
> 137 |                 new Error(
      |                 ^
  138 |                   `next build failed with code/signal ${code || signal}`
  139 |                 )
  140 |               )

  at ChildProcess.<anonymous> (lib/next-modes/next-start.ts:137:17)

@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Stats from current PR

🔴 2 regressions

Metric Canary PR Change Trend
Webpack Build Time (cached) 24.124s 24.678s 🔴 +554ms (+2%) ▁▁█▁▁
Webpack Warm (First Request) 3.313s 3.473s 🔴 +160ms (+5%) ▁▂█▁▁
📊 All Metrics
📖 Metrics Glossary

Dev Server Metrics:

  • Listen = TCP port starts accepting connections
  • First Request = HTTP server returns successful response
  • Cold = Fresh build (no cache)
  • Warm = With cached build artifacts

Build Metrics:

  • Fresh = Clean build (no .next directory)
  • Cached = With existing .next directory

Change Thresholds:

  • Time: Changes < 50ms AND < 10%, OR < 2% are insignificant
  • Size: Changes < 1KB AND < 1% are insignificant
  • All other changes are flagged to catch regressions

⚡ Dev Server

Metric Canary PR Change Trend
Cold (Listen) 814ms 813ms ██▁██
Cold (Ready in log) 810ms 804ms ▃▃▁▅█
Cold (First Request) 1.304s 1.284s ▁▁▁▂█
Warm (Listen) 814ms 813ms ██▁██
Warm (Ready in log) 803ms 799ms ▃▃▁▆█
Warm (First Request) 1.279s 1.275s ▁▂▁▂█
📦 Dev Server (Webpack) (Legacy)

📦 Dev Server (Webpack)

Metric Canary PR Change Trend
Cold (Listen) 812ms 813ms ▂▂▇▂▂
Cold (Ready in log) 793ms 807ms ▁▁█▁▂
Cold (First Request) 3.326s 3.378s ▁▂█▁▂
Warm (Listen) 813ms 813ms ▂▂█▂▂
Warm (Ready in log) 795ms 817ms ▁▂█▁▁
Warm (First Request) 3.313s 3.473s 🔴 +160ms (+5%) ▁▂█▁▁

⚡ Production Builds

Metric Canary PR Change Trend
Fresh Build 5.744s 5.595s ▂▁▁▁▆
Cached Build 2.964s 2.925s ▁▁▁▁▁
📦 Production Builds (Webpack) (Legacy)

📦 Production Builds (Webpack)

Metric Canary PR Change Trend
Fresh Build 23.922s 23.781s ▁▁█▁▁
Cached Build 24.124s 24.678s 🔴 +554ms (+2%) ▁▁█▁▁
node_modules Size 553 MB 553 MB █████
📦 Bundle Sizes

Bundle Sizes

⚡ Turbopack

Client

Main Bundles
Canary PR Change
017_4moyv5m34.js gzip 117 B 117 B
05jocc85-b73_.js gzip 341 B N/A -
096twxhe86hwd.js gzip 7.53 kB N/A -
0cz1d0mv5g_q7.js gzip 39.4 kB 39.4 kB
0e5lgz-xcf6ln.js gzip 273 B 273 B
0enhddgqs2dl-.js gzip 265 B 265 B
0h34mejwa_7pd.js gzip 354 B 354 B
0w853zxni3ro0.js gzip 1.36 kB N/A -
13lq2aw3_0bgw.js gzip 3.52 kB N/A -
1drifx6-oqf3q.js gzip 5.7 kB N/A -
1elt1qium-r2m.css gzip 115 B 115 B
1m5vdnlf0deiv.js gzip 411 B 411 B
1nigpy_mr0xvj.js gzip 339 B N/A -
1p0l8mn2wz5f2.js gzip 1.97 kB N/A -
1ph_nbsm967ot.js gzip 152 B N/A -
1sqoyy39jpgkq.js gzip 121 B 121 B
1tf1phijqlx9j.js gzip 220 B 220 B
1vusicjyl-6kb.js gzip 71.5 kB N/A -
2-54v5vj7orwp.js gzip 46.8 kB N/A -
2b05_424u1zq-.js gzip 120 B N/A -
2ca40xittx95r.js gzip 8.75 kB N/A -
2kci0vnt-49o-.js gzip 113 B 113 B
2rtriq9lc6zj-.js gzip 116 B N/A -
2sxgl_rcrrn_p.js gzip 399 B N/A -
2vt62i17bhbdu.js gzip 2.35 kB N/A -
342di-crrg9kq.js gzip 119 B N/A -
36hfc2_979aw2.js gzip 119 B N/A -
38wfp6jjrrdfm.js gzip 131 B 131 B
3dl1ilk37-30g.js gzip 117 B N/A -
3h0us7yy7bd_o.js gzip 13.1 kB N/A -
3kvxagoobts7h.js gzip 117 B N/A -
3p99hd9vwe6ae.js gzip 13 kB N/A -
3r0475panhk6m.js gzip 123 B N/A -
3s0i-weykgzrt.js gzip 125 B N/A -
3vjosdbddykpw.js gzip 5.88 kB N/A -
3wijk2ywqgsv9.js gzip 2.52 kB N/A -
3xuguv9lsoqw7.js gzip 65.5 kB N/A -
41u5s3oe2-erp.js gzip 2.29 kB N/A -
turbopack-0d..9ejy.js gzip 3.74 kB 3.74 kB
02zc7sqlpqxn3.js gzip N/A 119 B -
06pzxqp5qiozm.js gzip N/A 5.7 kB -
0dw-iyxdctoxo.js gzip N/A 65.5 kB -
0e28eh3gbwmp0.js gzip N/A 3.51 kB -
0jwho9fkrb_t3.js gzip N/A 2.29 kB -
0longconn5mlm.js gzip N/A 123 B -
0p4l3ozbez2cz.js gzip N/A 2.35 kB -
0sbh_15ejop4o.js gzip N/A 13 kB -
0skdm-9rpid8g.js gzip N/A 117 B -
0wv-h56sz83wn.js gzip N/A 120 B -
11hbt-mw4jxsv.js gzip N/A 71.5 kB -
137xup2t69ovc.js gzip N/A 1.97 kB -
1ezuqvubf86h1.js gzip N/A 5.88 kB -
1fk10s3m1oecx.js gzip N/A 400 B -
1hut5u-lz2yif.js gzip N/A 117 B -
20qhkyyj2_tla.js gzip N/A 116 B -
2imyufe18bsb7.js gzip N/A 1.36 kB -
2p4n3-8-xixh_.js gzip N/A 341 B -
2vqdfieehz5do.js gzip N/A 8.75 kB -
2x99jdcpr5n8x.js gzip N/A 7.53 kB -
2xn85ykzw2v1e.js gzip N/A 13.1 kB -
34knadd6wu9qg.js gzip N/A 119 B -
37b0_t16yn97f.js gzip N/A 153 B -
39jq2b6t6z6up.js gzip N/A 339 B -
3a9a9tvkag_-j.js gzip N/A 2.52 kB -
3d61tgick8g6b.js gzip N/A 46.8 kB -
3t121tj-h0kbm.js gzip N/A 125 B -
Total 299 kB 299 kB ⚠️ +4 B

Server

Middleware
Canary PR Change
middleware-b..fest.js gzip 1.02 kB 1.02 kB
Total 1.02 kB 1.02 kB ⚠️ +2 B
Build Details
Build Manifests
Canary PR Change
_buildManifest.js gzip 846 B 847 B
Total 846 B 847 B ⚠️ +1 B
Build Cache
Canary PR Change
00000001.sst gzip 13.2 MB 12.7 MB 🟢 460 kB (-3%)
00000002.sst gzip 13.4 MB 13 MB 🟢 415 kB (-3%)
00000003.sst gzip 14 MB 15.7 MB 🔴 +1.78 MB (+13%)
00000004.sst gzip 14.3 MB 13.4 MB 🟢 884 kB (-6%)
00000005.sst gzip 10.1 MB 2.75 MB 🟢 7.36 MB (-73%)
00000006.sst gzip 2.75 MB 10.1 MB 🔴 +7.33 MB (+266%)
00000007.sst gzip 59 B 59 B
00000008.meta gzip 89 B 89 B
00000009.meta gzip 297 kB 297 kB
00000010.meta gzip 297 kB 297 kB
00000011.meta gzip 297 kB 297 kB
00000012.sst gzip 52.4 kB 53.1 kB 🔴 +709 B (+1%)
00000013.sst gzip 1.78 MB 1.76 MB 🟢 18.9 kB (-1%)
00000014.sst gzip 59 B 59 B
00000015.meta gzip 116 B 116 B
00000016.meta gzip 327 kB 327 kB
00000017.meta gzip 404 kB 404 kB
00000018.sst gzip 53.3 kB 54 kB 🔴 +630 B (+1%)
00000019.sst gzip 1.36 MB 1.38 MB 🔴 +24.6 kB (+2%)
00000020.sst gzip 59 B 59 B
00000021.meta gzip 116 B 116 B
00000022.meta gzip 327 kB 327 kB
00000023.meta gzip 372 kB 372 kB
00000024.sst gzip 53.3 kB 54 kB 🔴 +609 B (+1%)
00000025.sst gzip 1.36 MB 1.35 MB 🟢 4.72 kB (0%)
00000026.sst gzip 59 B 59 B
00000027.meta gzip 116 B 116 B
00000028.meta gzip 327 kB 327 kB
00000029.meta gzip 372 kB 372 kB
00000030.sst gzip 52.6 kB 53.4 kB 🔴 +805 B (+2%)
00000031.sst gzip 1.36 MB 1.35 MB 🟢 4.92 kB (0%)
00000032.sst gzip 59 B 59 B
00000033.meta gzip 116 B 116 B
00000034.meta gzip 327 kB 327 kB
00000035.meta gzip 372 kB 372 kB
00000036.sst gzip 53.3 kB 53.9 kB 🔴 +649 B (+1%)
00000037.sst gzip 1.36 MB 1.35 MB 🟢 4.77 kB (0%)
00000038.sst gzip 59 B 59 B
00000039.meta gzip 116 B 116 B
00000040.meta gzip 327 kB 327 kB
00000041.meta gzip 372 kB 372 kB
CURRENT gzip 94 B 94 B
LOG gzip 665 B 669 B
Total 79.5 MB 79.5 MB ✅ -22.1 kB

📦 Webpack

Client

Main Bundles
Canary PR Change
3322-HASH.js gzip 65.7 kB N/A -
4191.HASH.js gzip 169 B N/A -
9784-HASH.js gzip 5.6 kB N/A -
b1ad9f4c-HASH.js gzip 63.5 kB N/A -
framework-HASH.js gzip 59.7 kB 59.7 kB
main-app-HASH.js gzip 253 B 254 B
main-HASH.js gzip 39.8 kB 39.7 kB
webpack-HASH.js gzip 1.68 kB 1.68 kB
3577.HASH.js gzip N/A 168 B -
578-HASH.js gzip N/A 65.8 kB -
8590-HASH.js gzip N/A 5.57 kB -
a8984546-HASH.js gzip N/A 63.5 kB -
Total 236 kB 237 kB ⚠️ +107 B
Polyfills
Canary PR Change
polyfills-HASH.js gzip 39.4 kB 39.4 kB
Total 39.4 kB 39.4 kB
Pages
Canary PR Change
_app-HASH.js gzip 194 B 193 B
_error-HASH.js gzip 181 B 182 B
css-HASH.js gzip 334 B 331 B
dynamic-HASH.js gzip 1.81 kB 1.81 kB
edge-ssr-HASH.js gzip 255 B 253 B
head-HASH.js gzip 349 B 351 B
hooks-HASH.js gzip 382 B 384 B
image-HASH.js gzip 5.05 kB 5.05 kB
index-HASH.js gzip 260 B 259 B
link-HASH.js gzip 2.48 kB 2.48 kB
routerDirect..HASH.js gzip 317 B 318 B
script-HASH.js gzip 384 B 386 B
withRouter-HASH.js gzip 316 B 315 B
1afbb74e6ecf..834.css gzip 106 B 106 B
Total 12.4 kB 12.4 kB ✅ -6 B

Server

Edge SSR
Canary PR Change
edge-ssr.js gzip 128 kB 128 kB
page.js gzip 293 kB 293 kB
Total 421 kB 421 kB ✅ -404 B
Middleware
Canary PR Change
middleware-b..fest.js gzip 601 B 602 B
middleware-r..fest.js gzip 156 B 156 B
middleware.js gzip 45.8 kB 45.4 kB
edge-runtime..pack.js gzip 842 B 842 B
Total 47.4 kB 47 kB ✅ -423 B
Build Details
Build Manifests
Canary PR Change
_buildManifest.js gzip 699 B 699 B
Total 699 B 699 B
Build Cache
Canary PR Change
0.pack gzip 4.8 MB 4.79 MB 🟢 4.83 kB (0%)
index.pack gzip 124 kB 124 kB
index.pack.old gzip 124 kB 123 kB 🟢 1.59 kB (-1%)
Total 5.05 MB 5.04 MB ✅ -6.7 kB

🔄 Shared (bundler-independent)

Runtimes
Canary PR Change
app-page-exp...dev.js gzip 375 kB 375 kB
app-page-exp..prod.js gzip 206 kB 206 kB
app-page-tur...dev.js gzip 374 kB 374 kB
app-page-tur..prod.js gzip 206 kB 206 kB
app-page-tur...dev.js gzip 370 kB 370 kB
app-page-tur..prod.js gzip 204 kB 204 kB
app-page.run...dev.js gzip 371 kB 371 kB
app-page.run..prod.js gzip 204 kB 204 kB
app-route-ex...dev.js gzip 82.6 kB 82.6 kB
app-route-ex..prod.js gzip 55.7 kB 55.7 kB
app-route-tu...dev.js gzip 82.6 kB 82.6 kB
app-route-tu..prod.js gzip 55.7 kB 55.7 kB
app-route-tu...dev.js gzip 82.2 kB 82.2 kB
app-route-tu..prod.js gzip 55.4 kB 55.4 kB
app-route.ru...dev.js gzip 82.2 kB 82.2 kB
app-route.ru..prod.js gzip 55.4 kB 55.4 kB
dev-validati...dev.js gzip 134 kB 134 kB
dev-validati...dev.js gzip 134 kB 134 kB
dev-validati...dev.js gzip 132 kB 132 kB
dev-validati...dev.js gzip 132 kB 132 kB
dist_client_...dev.js gzip 324 B 324 B
dist_client_...dev.js gzip 326 B 326 B
dist_client_...dev.js gzip 318 B 318 B
dist_client_...dev.js gzip 317 B 317 B
pages-api-tu...dev.js gzip 46.3 kB 46.3 kB
pages-api-tu..prod.js gzip 34.3 kB 34.3 kB
pages-api.ru...dev.js gzip 46.2 kB 46.2 kB
pages-api.ru..prod.js gzip 34.3 kB 34.3 kB
pages-turbo....dev.js gzip 54.7 kB 54.7 kB
pages-turbo...prod.js gzip 39.7 kB 39.7 kB
pages.runtim...dev.js gzip 54.7 kB 54.7 kB
pages.runtim..prod.js gzip 39.6 kB 39.6 kB
server.runti..prod.js gzip 65.9 kB 65.9 kB
use-cache-pr...dev.js gzip 73.3 kB 73.3 kB
use-cache-pr...dev.js gzip 73.3 kB 73.3 kB
use-cache-pr...dev.js gzip 71.5 kB 71.5 kB
use-cache-pr...dev.js gzip 71.5 kB 71.5 kB
Total 4.1 MB 4.1 MB
📎 Tarball URL
https://vercel-packages.vercel.app/next/commits/43d169f6312e6f6284e97dad2ce22a41d0cacbb1/next

Commit: 43d169f

Comment thread turbopack/crates/turbo-tasks-fs/src/path.rs Outdated
Comment thread turbopack/crates/turbo-tasks-fs/src/path.rs Outdated
@mischnic

Copy link
Copy Markdown
Member

There is a merge conflict already

vercel-fleet-prod Bot and others added 3 commits August 25, 2026 22:12
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
@sokra
sokra force-pushed the fix/readdir-realpath-assertion branch from 0c5e423 to efc933c Compare August 25, 2026 22:12
Comment thread turbopack/crates/turbo-tasks-fs/src/read_glob.rs
vercel-fleet-prod Bot and others added 2 commits August 25, 2026 22:31
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.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.

2 participants