Skip to content

Commit 1f6ea09

Browse files
[backport] Turbopack: Improve handling of symlink resolution errors (#83805)
Co-authored-by: Luke Sandberg <[email protected]>
1 parent c7d1855 commit 1f6ea09

File tree

8 files changed

+252
-74
lines changed

8 files changed

+252
-74
lines changed

crates/next-core/src/pages_structure.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,11 +103,11 @@ pub async fn find_pages_structure(
103103
next_router_root: FileSystemPath,
104104
page_extensions: Vc<Vec<RcStr>>,
105105
) -> Result<Vc<PagesStructure>> {
106-
let pages_root = project_root.join("pages")?.realpath().owned().await?;
106+
let pages_root = project_root.join("pages")?.realpath().await?;
107107
let pages_root = if *pages_root.get_type().await? == FileSystemEntryType::Directory {
108108
Some(pages_root)
109109
} else {
110-
let src_pages_root = project_root.join("src/pages")?.realpath().owned().await?;
110+
let src_pages_root = project_root.join("src/pages")?.realpath().await?;
111111
if *src_pages_root.get_type().await? == FileSystemEntryType::Directory {
112112
Some(src_pages_root)
113113
} else {

turbopack/crates/turbo-tasks-fs/src/lib.rs

Lines changed: 108 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1393,8 +1393,14 @@ impl FileSystemPath {
13931393
self.fs().metadata(self.clone())
13941394
}
13951395

1396-
pub fn realpath(&self) -> Vc<FileSystemPath> {
1397-
self.realpath_with_links().path()
1396+
// Returns the realpath to the file, resolving all symlinks and reporting an error if the path
1397+
// is invalid.
1398+
pub async fn realpath(&self) -> Result<FileSystemPath> {
1399+
let result = &(*self.realpath_with_links().await?);
1400+
match &result.path_or_error {
1401+
Ok(path) => Ok(path.clone()),
1402+
Err(error) => Err(anyhow::anyhow!(error.as_error_message(self, result))),
1403+
}
13981404
}
13991405

14001406
pub fn rebase(
@@ -1453,15 +1459,37 @@ impl ValueToString for FileSystemPath {
14531459
#[derive(Clone, Debug)]
14541460
#[turbo_tasks::value(shared)]
14551461
pub struct RealPathResult {
1456-
pub path: FileSystemPath,
1462+
pub path_or_error: Result<FileSystemPath, RealPathResultError>,
14571463
pub symlinks: Vec<FileSystemPath>,
14581464
}
14591465

1460-
#[turbo_tasks::value_impl]
1461-
impl RealPathResult {
1462-
#[turbo_tasks::function]
1463-
pub fn path(&self) -> Vc<FileSystemPath> {
1464-
self.path.clone().cell()
1466+
/// Errors that can occur when resolving a path with symlinks.
1467+
/// Many of these can be transient conditions that might happen when package managers are running.
1468+
#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, NonLocalValue, TraceRawVcs)]
1469+
pub enum RealPathResultError {
1470+
TooManySymlinks,
1471+
CycleDetected,
1472+
Invalid,
1473+
NotFound,
1474+
}
1475+
impl RealPathResultError {
1476+
/// Formats the error message
1477+
pub fn as_error_message(&self, orig: &FileSystemPath, result: &RealPathResult) -> String {
1478+
match self {
1479+
RealPathResultError::TooManySymlinks => format!(
1480+
"Symlink {orig} leads to too many other symlinks ({len} links)",
1481+
len = result.symlinks.len()
1482+
),
1483+
RealPathResultError::CycleDetected => {
1484+
format!("Symlink {orig} is in a symlink loop: {:?}", result.symlinks)
1485+
}
1486+
RealPathResultError::Invalid => {
1487+
format!("Symlink {orig} is invalid, it points out of the filesystem root")
1488+
}
1489+
RealPathResultError::NotFound => {
1490+
format!("Symlink {orig} is invalid, it points at a file that doesn't exist")
1491+
}
1492+
}
14651493
}
14661494
}
14671495

@@ -1628,7 +1656,9 @@ pub enum LinkContent {
16281656
// link because there is only **dist** path in `fn write_link`, and we need the raw path if
16291657
// we want to restore the link value in `fn write_link`
16301658
Link { target: RcStr, link_type: LinkType },
1659+
// Invalid means the link is invalid it points out of the filesystem root
16311660
Invalid,
1661+
// The target was not found
16321662
NotFound,
16331663
}
16341664

@@ -2081,8 +2111,8 @@ pub enum RawDirectoryEntry {
20812111
File,
20822112
Directory,
20832113
Symlink,
2114+
// Other just means 'not a file, directory, or symlink'
20842115
Other,
2085-
Error,
20862116
}
20872117

20882118
#[derive(Hash, Clone, Debug, PartialEq, Eq, TraceRawVcs, Serialize, Deserialize, NonLocalValue)]
@@ -2091,7 +2121,7 @@ pub enum DirectoryEntry {
20912121
Directory(FileSystemPath),
20922122
Symlink(FileSystemPath),
20932123
Other(FileSystemPath),
2094-
Error,
2124+
Error(RcStr),
20952125
}
20962126

20972127
impl DirectoryEntry {
@@ -2100,12 +2130,28 @@ impl DirectoryEntry {
21002130
/// `DirectoryEntry::Directory`.
21012131
pub async fn resolve_symlink(self) -> Result<Self> {
21022132
if let DirectoryEntry::Symlink(symlink) = &self {
2103-
let real_path = symlink.realpath().owned().await?;
2104-
match *real_path.get_type().await? {
2105-
FileSystemEntryType::Directory => Ok(DirectoryEntry::Directory(real_path)),
2106-
FileSystemEntryType::File => Ok(DirectoryEntry::File(real_path)),
2107-
_ => Ok(self),
2108-
}
2133+
let result = &*symlink.realpath_with_links().await?;
2134+
let real_path = match &result.path_or_error {
2135+
Ok(path) => path,
2136+
Err(error) => {
2137+
return Ok(DirectoryEntry::Error(
2138+
error.as_error_message(symlink, result).into(),
2139+
));
2140+
}
2141+
};
2142+
Ok(match *real_path.get_type().await? {
2143+
FileSystemEntryType::Directory => DirectoryEntry::Directory(real_path.clone()),
2144+
FileSystemEntryType::File => DirectoryEntry::File(real_path.clone()),
2145+
// Happens if the link is to a non-existent file
2146+
FileSystemEntryType::NotFound => DirectoryEntry::Error(
2147+
format!("Symlink {symlink} points at {real_path} which does not exist").into(),
2148+
),
2149+
FileSystemEntryType::Symlink => bail!(
2150+
"Symlink {symlink} points at a symlink but realpath_with_links returned a \
2151+
path, this is caused by eventual consistency."
2152+
),
2153+
_ => self,
2154+
})
21092155
} else {
21102156
Ok(self)
21112157
}
@@ -2117,7 +2163,7 @@ impl DirectoryEntry {
21172163
| DirectoryEntry::Directory(path)
21182164
| DirectoryEntry::Symlink(path)
21192165
| DirectoryEntry::Other(path) => Some(path),
2120-
DirectoryEntry::Error => None,
2166+
DirectoryEntry::Error(_) => None,
21212167
}
21222168
}
21232169
}
@@ -2129,6 +2175,7 @@ pub enum FileSystemEntryType {
21292175
File,
21302176
Directory,
21312177
Symlink,
2178+
/// These would be things like named pipes, sockets, etc.
21322179
Other,
21332180
Error,
21342181
}
@@ -2157,7 +2204,7 @@ impl From<&DirectoryEntry> for FileSystemEntryType {
21572204
DirectoryEntry::Directory(_) => FileSystemEntryType::Directory,
21582205
DirectoryEntry::Symlink(_) => FileSystemEntryType::Symlink,
21592206
DirectoryEntry::Other(_) => FileSystemEntryType::Other,
2160-
DirectoryEntry::Error => FileSystemEntryType::Error,
2207+
DirectoryEntry::Error(_) => FileSystemEntryType::Error,
21612208
}
21622209
}
21632210
}
@@ -2175,7 +2222,6 @@ impl From<&RawDirectoryEntry> for FileSystemEntryType {
21752222
RawDirectoryEntry::Directory => FileSystemEntryType::Directory,
21762223
RawDirectoryEntry::Symlink => FileSystemEntryType::Symlink,
21772224
RawDirectoryEntry::Other => FileSystemEntryType::Other,
2178-
RawDirectoryEntry::Error => FileSystemEntryType::Error,
21792225
}
21802226
}
21812227
}
@@ -2300,7 +2346,6 @@ async fn read_dir(path: FileSystemPath) -> Result<Vc<DirectoryContent>> {
23002346
RawDirectoryEntry::Directory => DirectoryEntry::Directory(entry_path),
23012347
RawDirectoryEntry::Symlink => DirectoryEntry::Symlink(entry_path),
23022348
RawDirectoryEntry::Other => DirectoryEntry::Other(entry_path),
2303-
RawDirectoryEntry::Error => DirectoryEntry::Error,
23042349
};
23052350
normalized_entries.insert(name.clone(), entry);
23062351
}
@@ -2331,64 +2376,79 @@ async fn get_type(path: FileSystemPath) -> Result<Vc<FileSystemEntryType>> {
23312376

23322377
#[turbo_tasks::function]
23332378
async fn realpath_with_links(path: FileSystemPath) -> Result<Vc<RealPathResult>> {
2334-
let mut current_vc = path.clone();
2379+
let mut current_path = path;
23352380
let mut symlinks: IndexSet<FileSystemPath> = IndexSet::new();
23362381
let mut visited: AutoSet<RcStr> = AutoSet::new();
2382+
let mut error = RealPathResultError::TooManySymlinks;
23372383
// Pick some arbitrary symlink depth limit... similar to the ELOOP logic for realpath(3).
23382384
// SYMLOOP_MAX is 40 for Linux: https://unix.stackexchange.com/q/721724
23392385
for _i in 0..40 {
2340-
let current = current_vc.clone();
2341-
if current.is_root() {
2386+
if current_path.is_root() {
23422387
// fast path
23432388
return Ok(RealPathResult {
2344-
path: current_vc,
2389+
path_or_error: Ok(current_path),
23452390
symlinks: symlinks.into_iter().collect(),
23462391
}
23472392
.cell());
23482393
}
23492394

2350-
if !visited.insert(current.path.clone()) {
2395+
if !visited.insert(current_path.path.clone()) {
2396+
error = RealPathResultError::CycleDetected;
23512397
break; // we detected a cycle
23522398
}
23532399

23542400
// see if a parent segment of the path is a symlink and resolve that first
2355-
let parent = current_vc.parent();
2401+
let parent = current_path.parent();
23562402
let parent_result = parent.realpath_with_links().owned().await?;
2357-
let basename = current
2403+
let basename = current_path
23582404
.path
23592405
.rsplit_once('/')
2360-
.map_or(current.path.as_str(), |(_, name)| name);
2361-
if parent_result.path != parent {
2362-
current_vc = parent_result.path.join(basename)?;
2363-
}
2406+
.map_or(current_path.path.as_str(), |(_, name)| name);
23642407
symlinks.extend(parent_result.symlinks);
2408+
let parent_path = match parent_result.path_or_error {
2409+
Ok(path) => {
2410+
if path != parent {
2411+
current_path = path.join(basename)?;
2412+
}
2413+
path
2414+
}
2415+
Err(parent_error) => {
2416+
error = parent_error;
2417+
break;
2418+
}
2419+
};
23652420

23662421
// use `get_type` before trying `read_link`, as there's a good chance of a cache hit on
23672422
// `get_type`, and `read_link` isn't the common codepath.
2368-
if !matches!(*current_vc.get_type().await?, FileSystemEntryType::Symlink) {
2423+
if !matches!(
2424+
*current_path.get_type().await?,
2425+
FileSystemEntryType::Symlink
2426+
) {
23692427
return Ok(RealPathResult {
2370-
path: current_vc,
2428+
path_or_error: Ok(current_path),
23712429
symlinks: symlinks.into_iter().collect(), // convert set to vec
23722430
}
23732431
.cell());
23742432
}
23752433

2376-
if let LinkContent::Link { target, link_type } = &*current_vc.read_link().await? {
2377-
symlinks.insert(current_vc.clone());
2378-
current_vc = if link_type.contains(LinkType::ABSOLUTE) {
2379-
current_vc.root().owned().await?
2380-
} else {
2381-
parent_result.path
2434+
match &*current_path.read_link().await? {
2435+
LinkContent::Link { target, link_type } => {
2436+
symlinks.insert(current_path.clone());
2437+
current_path = if link_type.contains(LinkType::ABSOLUTE) {
2438+
current_path.root().owned().await?
2439+
} else {
2440+
parent_path
2441+
}
2442+
.join(target)?;
23822443
}
2383-
.join(target)?;
2384-
} else {
2385-
// get_type() and read_link() might disagree temporarily due to turbo-tasks
2386-
// eventual consistency or if the file gets invalidated before the directory does
2387-
return Ok(RealPathResult {
2388-
path: current_vc,
2389-
symlinks: symlinks.into_iter().collect(), // convert set to vec
2444+
LinkContent::NotFound => {
2445+
error = RealPathResultError::NotFound;
2446+
break;
2447+
}
2448+
LinkContent::Invalid => {
2449+
error = RealPathResultError::Invalid;
2450+
break;
23902451
}
2391-
.cell());
23922452
}
23932453
}
23942454

@@ -2400,7 +2460,7 @@ async fn realpath_with_links(path: FileSystemPath) -> Result<Vc<RealPathResult>>
24002460
// Returning the followed symlinks is still important, even if there is an error! Otherwise
24012461
// we may never notice if the symlink loop is fixed.
24022462
Ok(RealPathResult {
2403-
path,
2463+
path_or_error: Err(error),
24042464
symlinks: symlinks.into_iter().collect(),
24052465
}
24062466
.cell())

0 commit comments

Comments
 (0)