Skip to content

Close sysroot escape via relative symlinks#185

Merged
jserv merged 1 commit into
sysprog21:mainfrom
Max042004:fix-symlink-sysroot-escape
Jul 11, 2026
Merged

Close sysroot escape via relative symlinks#185
jserv merged 1 commit into
sysprog21:mainfrom
Max042004:fix-symlink-sysroot-escape

Conversation

@Max042004

@Max042004 Max042004 commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • A relative openat(dirfd, name) against a dirfd already inside --sysroot skipped containment entirely; a symlink reachable through it (absolute target, or relative with enough ..) escaped the sysroot with no check, unlike the identical escape through an absolute path. Fixed by re-validating the reconstructed absolute path through the existing containment-checked resolver.
  • Darwin's linkat(2) can reject a hard link to a symlink source on non-APFS filesystems (reproduced as ENOTSUP on Case-sensitive HFS+); sys_linkat now falls back to symlinkat with the same target.

Close #130

Test

make check (green, includes new test-sysroot-symlink-escape)
make test-linkat-symlink-fallback (standalone, builds a scratch HFS+ image via hdiutil; green)


Summary by cubic

Fixes a sysroot escape where relative dirfd *at() calls could traverse symlinks outside --sysroot. Also adds a Darwin linkat fallback so linking a symlink source behaves like Linux.

  • Bug Fixes
    • For relative *at() under --sysroot, rebuild the absolute guest path from the dirfd base, run realpath + containment checks, and use the verified host path; reject absolute-target and deep-.. symlink escapes with ELOOP, including AT_FDCWD after chdir and faccessat.
    • On Darwin, if linkat on a symlink source fails with EPERM/ENOTSUP (without AT_SYMLINK_FOLLOW), fall back to symlinkat with the same target to match Linux.
    • Tests: add test-sysroot-symlink-escape to make check, and a standalone test-linkat-symlink-fallback (HFS+ image; skips when unavailable).

Written for commit 65fb574. Summary will update on new commits.

Review in cubic

A directory fd opened inside the sysroot let a relative
openat(dirfd, name) escape it entirely. proc_resolve_sysroot_path_
flags() only runs its sysroot-prefix + realpath() containment check
on absolute guest paths, since it has no dirfd context to reconstruct
a host location from a relative one. A symlink reachable through
dirfd -- a relative target with enough ".." components, or an
absolute target -- therefore walked straight out of the sysroot with
no check at all, even though the identical escape through an
absolute guest path was already rejected with ELOOP. Verified by
staging such a symlink and reading a host file outside the sysroot
through the relative dirfd path.

path_translate_at() now reconstructs the absolute guest path from
the dirfd's guest base path and re-validates it through the same
containment-checked resolver the absolute-path surface already uses;
realpath() inside that resolver collapses ".." and any symlink
indirection, including an absolute target, before the prefix check
runs.

Separately, Darwin's linkat(2) can reject a hard link to a symlink
itself (without AT_SYMLINK_FOLLOW) on non-APFS filesystems --
reproduced as ENOTSUP on a Case-sensitive HFS+ volume, where Linux
allows it unconditionally. sys_linkat() now falls back to creating a
plain symlink with the same target when linkat() fails with
EPERM/ENOTSUP and the source is a symlink.

Close sysprog21#130
cubic-dev-ai[bot]

This comment was marked as resolved.

@jserv jserv requested a review from doanbaotrung July 9, 2026 08:50
@doanbaotrung

Copy link
Copy Markdown
Collaborator

1. Directory Traversal Vulnerability Fix

  • Fix Target: Guest absolute/relative symbolic links escaping the sysroot boundary are rejected with ELOOP (40) instead of being resolved to host targets.
  • Test script: Run a Python 3 guest script that sets up an absolute symbolic link (/abs-link -> ~) and attempts to resolve it.

Execution Command & Log Output:

$elfuse/build/elfuse --sysroot ~/prefixes/ubuntu-arm64/rootfs /usr/bin/python3 -c "
import os
try:
    os.stat('/abs-link')
except OSError as e:
    print('Error:', e)
"
Error: [Errno 40] Too many levels of symbolic links: '/abs-link'
  • Result: PASS. The guest process fails to escape the sysroot via the absolute symlink, cleanly returning ELOOP (40) to the guest.

2. linkat Fallback for Symlinks

  • Fix Target: When the host filesystem (e.g. Case-Sensitive HFS+ on macOS) does not support hard-linking a symlink directly (without AT_SYMLINK_FOLLOW), linkat() fails with EPERM or ENOTSUP. sys_linkat() falls back to calling symlinkat to duplicate the link target, ensuring Linux-compatible success.
  • Test script: Use Python ctypes to invoke linkat directly via the guest C library (libc.so.6) with flags=0 (no follow) on a symbolic link, under a mocked host linkat failure of ENOTSUP.

Execution Command & Log Output:

$elfuse/build/elfuse --sysroot ~/prefixes/ubuntu-arm64/rootfs /usr/bin/python3 -u -c "
import ctypes
import os

path = '/tmp/d1'
with open(path + '/target.txt', 'w') as f:
    f.write('hello\n')
sym_path = path + '/sym'
os.symlink('target.txt', sym_path)

# Load libc and call linkat
libc = ctypes.CDLL('libc.so.6', use_errno=True)
libc.linkat.argtypes = [ctypes.c_int, ctypes.c_char_p, ctypes.c_int, ctypes.c_char_p, ctypes.c_int]
libc.linkat.restype = ctypes.c_int

res = libc.linkat(-100, b'/tmp/d1/sym', -100, b'/tmp/d1/hardlink', 0)
print('linkat returned:', res)

is_lnk = os.path.islink('/tmp/d1/hardlink')
tgt = os.readlink('/tmp/d1/hardlink')
print(f'Fallback result is a symlink: {is_lnk} (Expected: True)')
print(f'Fallback symlink target: {tgt} (Expected: target.txt)')
"
sys_symlinkat: target=target.txt, host_path=~/prefixes/ubuntu-arm64/rootfs/tmp/d1/sym
symlinkat returned 0, errno=0
sys_linkat entrance: oldpath=/tmp/d1/sym, newpath=/tmp/d1/hardlink, flags=0
host linkat returned -1, errno=45 (MOCKED ENOTSUP)
linkat returned: 0
Fallback result is a symlink: True (Expected: True)
Fallback symlink target: target.txt (Expected: target.txt)
  • Result: PASS. The guest C library call to linkat returns 0 (success) to the guest program, and the target is cleanly recreated as a symbolic link pointing to target.txt.

@Max042004 Max042004 force-pushed the fix-symlink-sysroot-escape branch 3 times, most recently from e9d380f to ad4c615 Compare July 10, 2026 14:45

@jserv jserv left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Squash and refine commit messages.

@Max042004 Max042004 force-pushed the fix-symlink-sysroot-escape branch from ad4c615 to 65fb574 Compare July 11, 2026 12:57
@jserv jserv merged commit 5ef02f1 into sysprog21:main Jul 11, 2026
19 checks passed
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.

Directory Traversal Vulnerability in sysroot_path_is_contained and linkat failures for Symlinks

3 participants