Absolute sysroot symlinks fail on case-sensitive APFS
@henrybear327 is already working on this.
Since Sep 20, 2026.
Assessment
This issue has not been assessed yet.
Description
Summary
A staged absolute symlink inside a case-sensitive sysroot is resolved from the
macOS host root instead of the guest root when its original guest path is a
forced system path such as /bin/sh.
An Alpine aarch64 rootfs contains:
/bin/sh -> /bin/busybox
Both entries exist inside the rootfs. Launching /bin/sh, opening it from a
running guest, or passing it to guest execve returns ENOENT. Launching
/bin/busybox directly succeeds. A relative symlink in the same /bin
directory also succeeds.
The failure is specific to the byte-exact resolver used for a case-sensitive
sysroot. The casefold resolver used on the default case-insensitive APFS volume
already follows the link in the guest namespace.
Environment
- Host: macOS 26.7, Darwin 25.6.0, Apple Silicon arm64
- elfuse branch:
main - elfuse commit:
c0458b4365e47a603d345996ef73d1579748db20 build/elfuseSHA-256:
294ef103b85170c108032dcb366acd6a433e8011ecc469d78927f890834bd679- Guest fixture: Alpine 3.21, aarch64
- Sysroot filesystem: case-sensitive APFS sparsebundle
Reproducer
The commands use a private copy of the existing Alpine fixture. They do not
modify externals/test-fixtures/rootfs.
Create and mount a case-sensitive sparsebundle:
REPRO=build/absolute-sh-symlink-repro
IMAGE="$REPRO/rootfs.sparsebundle"
ROOTFS="$REPRO/rootfs"
mkdir -p "$ROOTFS"
hdiutil create -size 1g -fs "Case-sensitive APFS" \
-type SPARSEBUNDLE -volname elfuse-absolute-sh "$IMAGE"
hdiutil attach "$IMAGE" -mountpoint "$ROOTFS" -nobrowse
Copy the fixture without following its symlinks:
rsync -a externals/test-fixtures/rootfs/ "$ROOTFS/"
mkdir -p "$ROOTFS/work/home"
ls -l "$ROOTFS/bin/sh" "$ROOTFS/bin/busybox"
The last command should show:
... rootfs/bin/sh -> /bin/busybox
... rootfs/bin/busybox
Run the failing entry path:
build/elfuse --timeout 0 \
--sysroot "$ROOTFS" \
--workdir /work \
--clear-env \
--env PATH=/usr/sbin:/usr/bin:/sbin:/bin \
--env HOME=/work/home \
--env LC_ALL=C \
/bin/sh -c 'printf "sh-ok\n"'
printf 'status=%s\n' "$?"
Expected result
The absolute symlink target is absolute in the guest namespace. /bin/sh
therefore resolves to <sysroot>/bin/busybox, prints sh-ok, and exits with
status 0.
This matches Linux and the QEMU aarch64 reference run.
Actual result
ERROR src/main.c:667: cannot read /bin/sh (resolved to .../rootfs/bin/sh): No such file or directory
status=1
The result was identical across three runs and with --verbose. The same
result was reproduced from a clean build of main at the commit above.
Controls
Direct execution of the target succeeds under the same elfuse binary and
sysroot:
build/elfuse --timeout 0 \
--sysroot "$ROOTFS" \
--workdir /work \
--clear-env \
--env PATH=/usr/sbin:/usr/bin:/sbin:/bin \
--env HOME=/work/home \
--env LC_ALL=C \
/bin/busybox sh -c 'printf "busybox-ok\n"'
Observed result:
busybox-ok
The same failure occurs after the guest has started:
/bin/busybox sh -c 'exec /bin/sh -c "printf runtime-exec-ok\\n"'
Guest access and open calls on /bin/sh also return ENOENT. A relative
link from /bin/cat to ../usr/bin/coreutils works in the same rootfs. The
absolute target, not the target file or executable format, distinguishes the
failure.
Detach the image after the failing run and controls:
hdiutil detach "$ROOTFS"
Root cause
path_translate_at() delegates a following lookup to
proc_resolve_sysroot_path_flags() in src/syscall/proc-state.c.
The resolver classifies /bin/sh as a forced guest system path. On a
case-sensitive sysroot, casefold_active() is false and this condition skips
the byte-exact symlink walker:
} else if (!forced_original_path) {
casefold_verdict_t verdict = resolve_byte_exact_through_links(
sr, lookup, follow_final, buf, bufsz, followed, sizeof(followed),
&followed_relative_link);
/* ... */
} else {
present = sysroot_path_exists(buf, follow_final);
}
The final branch probes the prefixed spelling
<sysroot>/bin/sh with the host stat or open behavior. Darwin follows the
stored absolute target /bin/busybox from the host root. Host /bin/busybox
does not exist, so the probe and the caller return ENOENT even though
<sysroot>/bin/busybox exists.
On a case-insensitive sysroot, casefold_active() is true and
resolve_through_links() reads the link, rebases the absolute target in the
guest namespace, and reaches the correct file. This explains why the existing
symlink-target coverage does not reproduce the case-sensitive failure.
The problematic else if (!forced_original_path) condition was introduced by
commit 5555c08fad5586f2932ff6e567ad8a1f903d0f2a (Follow absolute sysroot links to host paths).
Impact
The shared path resolver serves more than initial ELF loading. The bug affects
follow-style operations on staged absolute symlinks in forced guest system
paths, including:
- initial executable loading;
- guest
execve; open,stat, andaccess;- script and package-manager paths that invoke
/bin/sh.
An Alpine apk busybox trigger exits with status 127 when it tries to invoke
/bin/sh. The error is independent of the reported mremap failure.
Proposed fix
Use the byte-exact symlink walker for forced system paths when the sysroot is
case-sensitive. The resolver must read a staged link and resolve an absolute
target from the guest root before choosing the sysroot or host spelling.
The host fallback behavior added for staged links still needs coverage. A link
whose resolved guest target is outside forced guest directories may still fall
back to an existing host path, while /bin/sh -> /bin/busybox must remain
inside the sysroot.
Regression coverage
Add a case-sensitive APFS lane that stages an absolute link without passing
through guest symlinkat, because guest-created absolute links are rewritten
to a relative on-disk spelling.
The test should cover:
/bin/sh -> /bin/busyboxas the initial ELF entry;- runtime
execve("/bin/sh", ...); open,stat, andaccessthrough the link;- a relative link in the same directory;
- an allowed staged host bridge link;
- a dangling absolute link and a link loop with their Linux errnos.
Run the portable behavior in the QEMU aarch64 reference lane and the sysroot
behavior in an elfuse case-sensitive APFS lane.
Acceptance criteria
- The reproducer prints
sh-okand exits with status 0. - Runtime
execve,open,stat, andaccessfollow the staged absolute link
in the guest namespace. - Relative symlink behavior and permitted host fallback behavior do not regress.
- Dangling links and link loops retain their Linux errno behavior.
- The relevant sysroot path tests and aarch64 matrix lanes pass.
- Dominant language
- C
- Stars
- 268
- Forks
- 26
- Avg merge
- 1d 13h
- Merged PRs (30d)
- 48
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from sysprog21/elfuse
-
Difficulty 3/5 1-2 days Newbie friendliness 76/100
-
Difficulty 5/5 Over a week Newbie friendliness 45/100
-
`rt_sigreturn` overwrites the restored X8, so a signal taken on an `svc` re-enters it as syscall 2 Open
Difficulty 4/5 3-5 days Newbie friendliness 64/100
-
Difficulty 5/5 Over a week Newbie friendliness 35/100
All issues in sysprog21/elfuse
Similar issues
-
level/task module/gcp type/bug
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
-
Difficulty 1/5 Under an hour Newbie friendliness 86/100
hapostgres/pg_auto_failover#1190 ·
-
docs
Difficulty 1/5 Under an hour Newbie friendliness 85/100
-
P3 sonic-vpp
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
sonic-net/sonic-buildimage#29662 ·
-
Difficulty 1/5 Under an hour Newbie friendliness 94/100
spack/spack-packages#6586 ·