Repository navigation
Work around vendor kernels whose syscall 452 is not fchmodat2 (RRD-2830) - #31
Merged
Merged
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds a narrowly scoped workaround for vendor kernels where syscall 452 collides with fchmodat2, causing GNU tar to abort when extracting archives that contain symlinks (notably impacting RoonServer 2.71 container install/self-update on certain QNAP kernels).
Changes:
- Add an
LD_PRELOADshim that interceptsfchmodat()and emulates the pre-glibc-2.39 behavior only for the specificEFAULT+AT_SYMLINK_NOFOLLOWfailure mode. - Add an entrypoint probe that detects the failure by extracting a one-symlink archive and installs a PATH-scoped tar wrapper only when needed.
- Add smoke/runtime tests to ensure the shim ships, loads, and stays dormant on healthy kernels; compile the shim in a builder stage to avoid toolchain leakage.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
tests/smoke.sh |
Adds smoke assertions that the shim is present/loadable and no tar override/toolchain is baked into the image. |
tests/runtime.sh |
Adds runtime assertions that the shim does not activate on CI’s healthy kernel. |
fchmodat-compat.c |
Introduces the LD_PRELOAD shim implementing the targeted fchmodat() fallback behavior. |
entrypoint.sh |
Adds runtime probing and best-effort installation of a tar PATH shim that injects the preload only when the kernel is affected. |
Dockerfile |
Adds a builder stage to compile the shim and copies only the .so into the runtime image. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
decriptor
force-pushed
the
shaw/qnap-fchmodat2
branch
2 times, most recently
from
August 12, 2026 22:28
7987386 to
4e4b7d4
Compare
RoonServer 2.71 cannot install or self-update in a container on QNAP: tar
aborts with "Cannot change mode to rwxr-xr-x: Bad address" (RRD-2830).
Root cause, confirmed on a TS-453Be running QTS 5.2.9 / kernel 5.10.60-qnap:
fchmodat2(..., AT_SYMLINK_NOFOLLOW) = -1 EFAULT (Bad address)
glibc 2.39+ implements fchmodat with flags ≠ 0 via the fchmodat2 syscall
(452, Linux 6.6+). GNU tar uses AT_SYMLINK_NOFOLLOW when restoring modes of
directories, files, and symlinks. That kernel is 5.10 and has no fchmodat2 —
but QNAP put six vendor syscalls on 450-455 before upstream claimed those
numbers. Per their GPL source, GPL_QTS/src/linux-5.10/arch/x86/entry/syscalls/syscall_64.tbl:
450 qnap_rmdir 451 qnap_unlink 452 qnap_find_filename
453 qsendfile 454 qsendfile64 455 qtier_anchor
452 is qnap_find_filename, declared in that tree's fs/namei.c as
SYSCALL_DEFINE2(qnap_find_filename, const char __user *, path,
const char __user *, file)
so it reads glibc's argument 0 — a dirfd — as a userspace string pointer and
faults in getname(). glibc only falls back to its pre-2.39 emulation on ENOSYS,
so EFAULT propagates; tar ignores EOPNOTSUPP for SYMTYPE but treats EFAULT as
fatal. Public failures are mostly directory mode restores.
The table jumps 440 -> 450, matching a syscall sweep on the running kernel:
441-449 and 456+ correctly return ENOSYS, 450-455 do not. So this is a
collision with vendor syscalls, not a missing feature. It is not seccomp
(--security-opt seccomp=unconfined behaves identically) and not the filesystem
(it reproduces on overlayfs and on an ext4 bind mount alike). It is also not
specific to Roon: any archive whose members need a flagged fchmodat fails the
same way, which is why unrelated images break on the same hardware.
The fix restores glibc 2.36's behaviour for this one call via a small
LD_PRELOAD, scoped so unaffected hosts are untouched:
- Installed only after a probe extracts a tiny archive with /usr/bin/tar
and sees "Bad address" — no syscall numbers or vendor matching. The
wrapper lives in the container's writable layer, so a restart keeps it
(a no-op once 452 is really fchmodat2). Recreating the container re-probes.
- Preload is scoped to tar through a PATH wrapper, so RoonServer never
loads it. RoonServer/start.sh invokes `tar` unqualified, so this covers
self-update as well as first install.
- The real fchmodat runs first; its result is returned untouched unless it
failed with exactly EFAULT on a no-follow request.
hash -r after installing the wrapper is load-bearing: unqualified tar must
resolve through PATH so the wrapper we just wrote is what the verify extract
and later install use.
The wrapper is best-effort: a full /tmp, a --read-only rootfs, or --user
without write access to /usr/local degrades to "no shim" rather than aborting
the entrypoint. LD_PRELOAD is prepended so an operator-set preload survives.
A missing /proc/self/fd/N is mapped ENOENT → EOPNOTSUPP. On a successful
emulation the caller's entry errno is restored, because chmod/close leave
errno unchanged on success.
After install, the probe archive is re-extracted through the PATH wrapper to
verify the preload actually took (ld.so ignores an unusable LD_PRELOAD). A
failed verify removes the wrapper so the next start retries instead of
reporting "already active". Restart only treats our wrapper plus the .so as
active; a foreign /usr/local/bin/tar is left alone.
Verified on the affected NAS: detection fires, "Shim verified", 2.71 build
1683 installs, and a restart reports the shim already active. Healthy 6.12
kernel: probe stays dormant.
Tests: smoke.sh asserts the library ships and loads, that a stub making
fchmodat(..., AT_SYMLINK_NOFOLLOW) return EFAULT still extracts a dir+symlink
archive through the shim, and that restart does not call a foreign
/usr/local/bin/tar the shim. runtime.sh asserts the probe stays dormant on
CI's healthy kernel.
This is a workaround for a third-party kernel defect and should not outlive it.
QNAP cannot fix it cheaply — those syscall numbers are ABI for their own
userspace — so retirement is gated on affected models aging out, observable by
the detection line disappearing from support logs.
decriptor
force-pushed
the
shaw/qnap-fchmodat2
branch
from
August 12, 2026 22:53
4e4b7d4 to
ba94d6d
Compare
pargue
approved these changes
Aug 13, 2026
pargue
left a comment
There was a problem hiding this comment.
I found no actionable correctness, security, or operational issues.
1 task done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
RoonServer 2.71 cannot install or self-update in a container on QNAP. tar aborts on the three
.roonhostalias symlinks and the updater reportsupdate installer exited with 1:Root cause
Confirmed on a TS-453Be running QTS 5.2.9 / kernel 5.10.60-qnap:
tar restores a symlink's mode for every symlink in an archive, whatever its target. glibc 2.39+ implements that via
fchmodat2(syscall 452, Linux 6.6+). That kernel is 5.10 and has nofchmodat2— but QNAP put six vendor syscalls on 450-455 before upstream claimed those numbers. From their GPL source,GPL_QTS/src/linux-5.10/arch/x86/entry/syscalls/syscall_64.tbl:452 is
qnap_find_filename, declared in that tree'sfs/namei.casso it reads glibc's argument 0 — a dirfd — as a userspace string pointer and faults in
getname(). glibc only falls back to its pre-2.39 emulation onENOSYS, soEFAULTpropagates. tar ignoresEOPNOTSUPPhere (a symlink's mode is meaningless on Linux) but treatsEFAULTas fatal.The table jumps 440 → 450, matching a syscall sweep on the running kernel: 441-449 and 456+ correctly return
ENOSYS, 450-455 do not. So this is a collision with vendor syscalls, not a missing feature.Ruled out on hardware: not seccomp (
--security-opt seccomp=unconfinedbehaves identically) and not the filesystem (reproduces on overlayfs and on an ext4 bind mount alike). Also not Roon-specific — any archive containing any symlink fails the same way, which is why unrelated images break on the same NAS.The fix
Restores glibc 2.36's behaviour for this one call via a small
LD_PRELOAD, scoped three ways so unaffected hosts are untouched:RoonServer/start.shinvokestarunqualified, so this covers self-update as well as first install.fchmodatruns first and its result is returned untouched unless it failed with exactlyEFAULTon a no-follow request.Every filesystem step is best-effort: a full
/tmp, a--read-onlyrootfs or a non-root--userdegrades to "no shim" rather than stopping the container from starting.Verification
/usr/local/bin/tarcommand -v tar/usr/bin/tar/usr/local/bin/tarOn the NAS the entrypoint logs
Shim verified: extracting a symlink now succeeds, and a restart reportsshim already active. Locally: 63/63 smoke, 9/9 healthcheck.Tests pin the blast radius, not just the fix —
smoke.shasserts the library ships, loads as a preload, that nothing overrides tar in the image, and that the builder stage's toolchain did not leak;runtime.shasserts the shim stays dormant on CI's healthy kernel, since a misfiring probe would put anLD_PRELOADon tar for every user on every platform.Lifespan
This works around a third-party kernel defect and should not outlive it. QNAP cannot fix it cheaply — those syscall numbers are ABI for their own userspace, so renumbering breaks their binaries and third-party QPKGs, and a rebase to ≥6.6 is not happening for this hardware generation. Retirement is gated on affected models aging out, observable by the detection line disappearing from support logs.
The second commit addresses review feedback (set -e safety, verifying the shim actually loads before claiming it,
LD_PRELOADprepending, glibc'sENOENT→EOPNOTSUPPmapping, and two tests that could pass without verifying anything). Happy to squash on merge.