From 0199eb91e3cd51255f17f15664e4d8d00629375f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=9F=83=E5=8D=9A=E6=8B=89=E9=85=B1?= Date: Sun, 19 Apr 2026 22:47:02 +0800 Subject: [PATCH] link2symlink: accept O_TMPFILE anonymous inodes in linkat copy-path gate Extend handle_linkat_from_proc_fd() so that, in addition to the classic " (deleted)" orphan-inode case (open() + unlink() followed by linkat(/proc/self/fd/N, AT_SYMLINK_FOLLOW, newpath)), a readlink target of the O_TMPFILE magic form "//#" is also recognised and routed through the existing user-space copy branch. The copy body (stat + open(O_CREAT|O_EXCL) + read/write loop) is byte- identical for both source types and needs no change; only the gate is relaxed, by parsing for the "//#" trailer as an OR with the existing " (deleted)" suffix check. Motivation: Alpine apk's atomic-publish pattern uses fd = open(dir, O_TMPFILE | O_RDWR, mode); write(fd, ...); linkat(AT_FDCWD, "/proc/self/fd/", AT_FDCWD, newpath, AT_SYMLINK_FOLLOW); On kernels where link2symlink is already required, that flow produces a /proc//fd/ magic symlink whose readlink resolves to "//#" rather than " (deleted)", so the original gate returned 0 and let the unsupported hardlink syscall reach the kernel, causing apk to fail its triggers/scripts/installed-db writes. Known semantic limitation (documented in a code comment): writes to the source fd performed after the linkat call are not reflected in the target file, because the copy snapshots the data at linkat time rather than hijacking the fd. All surveyed consumers of O_TMPFILE+linkat (apk, dpkg, ld-linux) follow a write-fully -> publish -> stop-writing ordering and are unaffected; a future program relying on post-publish writes would need an fd-hijack approach instead. Tested on HarmonyOS NEXT / kernel 5.10.43 aarch64 inside an aoco_untrusted_app SELinux domain that denies app_data_file:file link, by running `apk add --no-cache` for a representative package set (alpine-base, bash, busybox-extras, ...): pre-patch apk fails with EPERM while writing triggers/scripts.tar.gz/installed; post-patch the install completes and the resulting files are byte-identical to the source O_TMPFILE content. --- src/extension/link2symlink/link2symlink.c | 36 +++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/src/extension/link2symlink/link2symlink.c b/src/extension/link2symlink/link2symlink.c index d5f6241d..1d7b893f 100644 --- a/src/extension/link2symlink/link2symlink.c +++ b/src/extension/link2symlink/link2symlink.c @@ -1,5 +1,6 @@ #include /* rename(2), */ #include /* atoi */ +#include /* bool, true, false */ #include /* symlink(2), symlinkat(2), readlink(2), lstat(2), unlink(2), unlinkat(2)*/ #include /* str*, strrchr, strcat, strcpy, strncpy, strncmp */ #include /* lstat(2), */ @@ -7,6 +8,7 @@ #include /* E*, */ #include /* PATH_MAX, */ #include /* isdigit, */ +#include /* O_RDONLY, O_WRONLY, O_CREAT, O_EXCL (copy path) */ #include "cli/note.h" #include "extension/extension.h" @@ -563,13 +565,43 @@ static int handle_linkat_from_proc_fd(Tracee *tracee) { return 0; } - /* Ensure provided path is symlink to " (deleted)" file */ + /* Ensure provided path is a symlink to either a " (deleted)" file + * (classic open+unlink orphan inode) or to an O_TMPFILE anonymous + * inode whose magic-link readlink returns "//#". + * + * The original l2s path only recognised the " (deleted)" suffix; + * this extends the gate to also accept O_TMPFILE anonymous inodes + * (used by Alpine apk's atomic-publish pattern: open(O_TMPFILE) + * then linkat(/proc/self/fd/N, AT_SYMLINK_FOLLOW, dest)). The + * user-space copy below works byte-identically for both source + * types (read from fd, write to new file). + * + * Known semantic limitation: if the tracee writes to the source fd + * after the linkat call, the target file will not see the new bytes + * (the copy snapshots the file at linkat time). All known tools + * that use O_TMPFILE+linkat (apk, dpkg, ld-linux) follow a + * write-fully → publish → stop-writing pattern and will not trigger + * this; if a future program relies on post-linkat writes syncing to + * the target, an fd-hijack mechanism would be needed instead. */ char target_path[PATH_MAX] = {}; int status = readlink(proc_path, target_path, sizeof(target_path)); if (status < 10 || status >= (ssize_t) sizeof(target_path)) { return 0; } - if (0 != memcmp(&target_path[status - 10], DELETED_SUFFIX, 10)) { + bool is_deleted = (0 == memcmp(&target_path[status - 10], DELETED_SUFFIX, 10)); + bool is_o_tmpfile = false; + { + /* O_TMPFILE magic-link form: "//#". */ + const char *slash = strrchr(target_path, '/'); + if (slash != NULL && slash[1] == '#') { + const char *p = slash + 2; + while (*p != '\0' && isdigit((unsigned char) *p)) + p++; + if (*p == '\0' && p > slash + 2) + is_o_tmpfile = true; + } + } + if (!is_deleted && !is_o_tmpfile) { return 0; }