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"
