diff mbox series

[yocto-patches,pseudo,3/3] test: Add test for linkat chroot path stripping

Message ID 1777071733-25858-4-git-send-email-mark.hatle@kernel.crashing.org
State New
Headers show
Series Add pending patches to master | expand

Commit Message

Mark Hatle April 24, 2026, 11:02 p.m. UTC
From: Mark Hatle <mark.hatle@amd.com>

A bug in linkat's chroot path stripping logic used strncmp() without
negation, causing it to strip the chroot prefix when the path did NOT
match.  Also it accessed memory out of bounds when the path was shorter
than pseudo_chroot_len. This was fixed in the previous commit.

Add a test that exercises this scenario by creating a chroot with a
long directory path, then calling link() on short paths (/a -> /b)
inside the chroot. The paths are placed at the end of a mapped memory
page (via mmap) with the next page unmapped, so the out-of-bounds read
at oldpath[pseudo_chroot_len] reliably causes a segfault rather than
silently reading adjacent mapped memory.

AI-Generated: Test code written by Claude Opus 4.6.

Signed-off-by: Mark Hatle <mark.hatle@amd.com>
Signed-off-by: Mark Hatle <mark.hatle@kernel.crashing.org>
---
 test/test-linkat-chroot.c  | 69 ++++++++++++++++++++++++++++++++++++++++++++++
 test/test-linkat-chroot.sh | 19 +++++++++++++
 2 files changed, 88 insertions(+)
 create mode 100644 test/test-linkat-chroot.c
 create mode 100755 test/test-linkat-chroot.sh
diff mbox series

Patch

diff --git a/test/test-linkat-chroot.c b/test/test-linkat-chroot.c
new file mode 100644
index 0000000..e02989e
--- /dev/null
+++ b/test/test-linkat-chroot.c
@@ -0,0 +1,69 @@ 
+/*
+ * Test that link/linkat inside a chroot does not segfault
+ * when the path is shorter than the chroot path.
+ * SPDX-License-Identifier: LGPL-2.1-only
+ *
+ * The bug was that linkat's chroot path stripping used strncmp()
+ * without negation, causing an out-of-bounds read at
+ * oldpath[pseudo_chroot_len] when the path was shorter than the
+ * chroot path. To reliably trigger this, we place the path string
+ * at the end of a mapped page with the next page unmapped, so any
+ * out-of-bounds access will SIGSEGV.
+ */
+#define _GNU_SOURCE
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/mman.h>
+
+/* Place str at the end of a page with the next page unmapped.
+ * Any read past the string will hit unmapped memory and SIGSEGV. */
+static char *guarded_string(const char *str) {
+    long pagesize = sysconf(_SC_PAGESIZE);
+    size_t len = strlen(str) + 1;
+    char *pages = mmap(NULL, pagesize * 2, PROT_READ | PROT_WRITE,
+                       MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+    if (pages == MAP_FAILED) {
+        perror("mmap");
+        return NULL;
+    }
+    if (munmap(pages + pagesize, pagesize) == -1) {
+        perror("munmap");
+        return NULL;
+    }
+    char *dest = pages + pagesize - len;
+    memcpy(dest, str, len);
+    return dest;
+}
+
+int main(int argc, char *argv[]) {
+    if (argc != 2) {
+        fprintf(stderr, "usage: %s <chrootdir>\n", argv[0]);
+        return 2;
+    }
+    if (chroot(argv[1]) == -1) {
+        perror("chroot");
+        return 1;
+    }
+    if (chdir("/") == -1) {
+        perror("chdir");
+        return 1;
+    }
+
+    char *oldpath = guarded_string("/a");
+    char *newpath = guarded_string("/b");
+    if (!oldpath || !newpath)
+        return 1;
+
+    /* link() calls linkat() internally. The short paths are placed
+     * at page boundaries so that the buggy out-of-bounds read at
+     * oldpath[pseudo_chroot_len] will SIGSEGV instead of silently
+     * reading adjacent memory. */
+    if (link(oldpath, newpath) == -1) {
+        perror("link");
+        return 1;
+    }
+    return 0;
+}
diff --git a/test/test-linkat-chroot.sh b/test/test-linkat-chroot.sh
new file mode 100755
index 0000000..247a6d7
--- /dev/null
+++ b/test/test-linkat-chroot.sh
@@ -0,0 +1,19 @@ 
+#!/bin/bash
+#
+# Test that link/linkat inside a chroot does not segfault
+# when the path is shorter than the chroot path.
+# SPDX-License-Identifier: LGPL-2.1-only
+#
+
+set -e
+
+# Create a chroot directory with a deeply nested path
+# so pseudo_chroot_len is longer than the filenames used inside
+CHROOTDIR=$(pwd)/linkat_chroot_test/deep/nested/path/to/make/it/long
+mkdir -p "$CHROOTDIR"
+touch "$CHROOTDIR/a"
+trap "rm -rf $(pwd)/linkat_chroot_test test-linkat-chroot" 0
+
+gcc -o test-linkat-chroot test/test-linkat-chroot.c
+
+./test-linkat-chroot "$CHROOTDIR"