[pseudo][PATCH 03/11] test: Add test for linkat chroot path stripping
Mark Hatle <[email protected]>
| Newsgroups | org.yoctoproject.lists.yocto-patches |
|---|---|
| Message-ID | <[email protected]> |
From: Mark Hatle <[email protected]> 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 <[email protected]> Signed-off-by: Mark Hatle <[email protected]> --- 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 --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" -- 1.8.3.1