Skip to content

PRoot returns inconsistent metadata for hard-linked regular files (scandir reports symlink, lstat reports regular file) #418

Description

@szgk

Hi, and thank you for creating and maintaining this amazing open-source project.

I've been using PRoot on a Google Pixel 9a through Termux.

While building a Next.js project with pnpm and Turbopack, I encountered the following error:

Invalid symlink

After investigating the issue, I found what appears to be a bug in PRoot's handling of filesystem metadata for hard-linked files. I've included a minimal reproduction and the investigation results below.

Summary

When running Next.js (Turbopack) inside Termux + PRoot Ubuntu, a project installed with pnpm hardlink mode fails to build with:

TurbopackInternalError: Invalid symlink

The investigation indicates that PRoot returns inconsistent metadata for hard-linked regular files.

For the same filesystem path:

  • os.scandir().is_symlink() reports True
  • os.lstat() reports a regular file
  • os.readlink() returns EINVAL
  • st_nlink == 2 (hard-linked file)

As a result, applications that trust directory enumeration metadata (such as Turbopack) treat the file as a symbolic link and fail with Invalid symlink.


Environment

  • Device: Google Pixel 9a
  • Android: 17
  • Termux: googleplay.2026.06.21
  • Ubuntu: 25.10(PRoot v5.1.0)
  • PRoot-distro: v4.38.0
  • pnpm: 10.34.5
  • Next.js: 16.2.12
  • Turbopack: bundled with Next.js 16.2.12

Reproduction

1. Create a new Next.js project

pnpm create next-app proot-test
cd proot-test

2. Install dependencies using hardlink mode

rm -rf node_modules

pnpm install --package-import-method=hardlink

3. Verify hard-linked files exist

find node_modules -links +1 -print | head

Example:

node_modules/.pnpm/has-flag@4.0.0/node_modules/has-flag/package.json

4. Build with Turbopack

pnpm build

Result:

Error [TurbopackInternalError]: Invalid symlink

Relevant stack:

- Execution of try_get_next_package failed
- Execution of read_package_json failed
- Execution of <FileSource as Asset>::content failed
- Invalid symlink

Metadata inspection

The following script finds one hard-linked regular file and compares the metadata returned by os.scandir() and os.lstat().

import os
import stat
import sys

root = "node_modules"
target = None

stack = [root]

while stack:
    directory = stack.pop()

    try:
        entries = list(os.scandir(directory))
    except OSError:
        continue

    for entry in entries:
        path = entry.path

        try:
            st = os.lstat(path)
        except OSError:
            continue

        if stat.S_ISDIR(st.st_mode):
            stack.append(path)
            continue

        if stat.S_ISREG(st.st_mode) and st.st_nlink > 1:
            target = path
            break

    if target:
        break

if target is None:
    print("No hard-linked regular file found.")
    sys.exit(1)

parent = os.path.dirname(target)
name = os.path.basename(target)

print("target:", target)

for entry in os.scandir(parent):
    if entry.name != name:
        continue

    st = os.lstat(target)

    print("scandir is_symlink:", entry.is_symlink())
    print("scandir inode     :", entry.inode())
    print("lstat inode       :", st.st_ino)
    print("lstat mode        :", oct(st.st_mode))
    print("lstat is regular  :", stat.S_ISREG(st.st_mode))
    print("lstat is symlink  :", stat.S_ISLNK(st.st_mode))
    print("nlink             :", st.st_nlink)

    try:
        print("readlink          :", os.readlink(target))
    except OSError as error:
        print("readlink error    :", repr(error))

    break

Actual output

target: node_modules/.pnpm/has-flag@4.0.0/node_modules/has-flag/package.json

scandir is_symlink: True
scandir inode     : 1523265

lstat inode       : 1349956
lstat mode        : 0o100644
lstat is regular  : True
lstat is symlink  : False
nlink             : 2

readlink error    : OSError(22, 'Invalid argument')

Expected behavior

For the same filesystem entry:

os.scandir().is_symlink() == False

entry.inode() == os.lstat().st_ino

os.readlink() should not be called on a regular file.

Actual behavior

For the same filesystem path:

API Result
os.scandir().is_symlink() True
os.lstat() Regular file
st_nlink 2
os.readlink() EINVAL

Additionally:

entry.inode() != os.lstat().st_ino

which indicates inconsistent metadata for the same filesystem entry.


Workaround

Removing node_modules and reinstalling using copy mode avoids the issue.

rm -rf node_modules

pnpm install --package-import-method=copy

pnpm build

The build succeeds.


Suspected cause

PRoot appears to return inconsistent metadata during directory enumeration.

A hard-linked regular file is reported as a symbolic link by os.scandir(), while os.lstat() correctly reports it as a regular file.

Applications that trust directory enumeration metadata subsequently attempt to resolve the file as a symbolic link.

Since the file is actually a regular file, readlink() fails with:

EINVAL (Invalid argument)

which ultimately causes errors such as:

TurbopackInternalError: Invalid symlink

Additional notes

This issue is reproducible only when pnpm installs packages using hardlink mode.

Reinstalling with:

pnpm install --package-import-method=copy

eliminates the issue, strongly suggesting that the bug is triggered by hard-linked regular files.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions