diff mbox series

[pseudo,09/11] test: Add test symlinkat and related

Message ID 1777312601-1393-10-git-send-email-mark.hatle@kernel.crashing.org
State New
Headers show
Series Various fixes, release 1.9.6 | expand

Commit Message

Mark Hatle April 27, 2026, 5:56 p.m. UTC
From: Mark Hatle <mark.hatle@amd.com>

Add a test case that verifies link, linkat, symlink, symlinkat, and
readlink.

Test both with and without chroot.

AI-Generated: test cases generated by github copilot (claude opus 6.4)

Signed-off-by: Mark Hatle <mark.hatle@amd.com>
Signed-off-by: Mark Hatle <mark.hatle@kernel.crashing.org>
---
 test/test-link-symlink.c  | 175 ++++++++++++++++++++++++++++++++++++++++++++++
 test/test-link-symlink.sh |  11 +++
 2 files changed, 186 insertions(+)
 create mode 100644 test/test-link-symlink.c
 create mode 100755 test/test-link-symlink.sh
diff mbox series

Patch

diff --git a/test/test-link-symlink.c b/test/test-link-symlink.c
new file mode 100644
index 0000000..f8a1026
--- /dev/null
+++ b/test/test-link-symlink.c
@@ -0,0 +1,175 @@ 
+/*
+ * Test link, linkat, symlink, symlinkat, readlink, readlinkat
+ * SPDX-License-Identifier: LGPL-2.1-only
+ */
+#define _GNU_SOURCE
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <string.h>
+#include <limits.h>
+#include <errno.h>
+
+static int failures = 0;
+
+static void check(const char *desc, int condition) {
+    if (!condition) {
+        fprintf(stderr, "FAIL: %s\n", desc);
+        failures++;
+    }
+}
+
+static int test_basic(void) {
+    struct stat st, st2;
+    char buf[PATH_MAX];
+    ssize_t len;
+    int fd, dirfd;
+
+    /* Create a test file */
+    fd = open("test_link_file", O_CREAT | O_WRONLY, 0644);
+    check("write", write(fd, "hello", 5) == 5);
+    close(fd);
+    check("chown", chown("test_link_file", 100, 200) == 0);
+
+    /* symlink */
+    check("symlink", symlink("test_link_file", "test_link_sym") == 0);
+    check("symlink lstat", lstat("test_link_sym", &st) == 0);
+    check("symlink is link", S_ISLNK(st.st_mode));
+
+    /* readlink */
+    len = readlink("test_link_sym", buf, sizeof(buf));
+    check("readlink len", len > 0);
+    buf[len] = '\0';
+    check("readlink value", strcmp(buf, "test_link_file") == 0);
+
+    /* symlink target stat should show chown'd values */
+    stat("test_link_sym", &st);
+    check("symlink target uid", st.st_uid == 100);
+    check("symlink target gid", st.st_gid == 200);
+
+    /* link (hard link) */
+    check("link", link("test_link_file", "test_link_hard") == 0);
+    stat("test_link_hard", &st);
+    stat("test_link_file", &st2);
+    check("hardlink same inode", st.st_ino == st2.st_ino);
+    check("hardlink uid", st.st_uid == 100);
+    check("hardlink gid", st.st_gid == 200);
+
+    /* linkat */
+    dirfd = open(".", O_RDONLY | O_DIRECTORY);
+    check("linkat", linkat(dirfd, "test_link_file", dirfd, "test_link_hard2", 0) == 0);
+    stat("test_link_hard2", &st);
+    check("linkat uid", st.st_uid == 100);
+
+    /* symlinkat */
+    check("symlinkat", symlinkat("test_link_file", dirfd, "test_link_sym2") == 0);
+    check("symlinkat lstat", fstatat(dirfd, "test_link_sym2", &st, AT_SYMLINK_NOFOLLOW) == 0);
+    check("symlinkat is link", S_ISLNK(st.st_mode));
+
+    /* readlinkat */
+    len = readlinkat(dirfd, "test_link_sym2", buf, sizeof(buf));
+    check("readlinkat len", len > 0);
+    buf[len] = '\0';
+    check("readlinkat value", strcmp(buf, "test_link_file") == 0);
+
+    close(dirfd);
+    unlink("test_link_sym");
+    unlink("test_link_sym2");
+    unlink("test_link_hard");
+    unlink("test_link_hard2");
+    unlink("test_link_file");
+
+    return 0;
+}
+
+static int test_chroot(const char *chroot_dir) {
+    struct stat st;
+    char buf[PATH_MAX];
+    char path[PATH_MAX];
+    ssize_t len;
+    int fd, dirfd;
+
+    /* Create a file inside the chroot directory */
+    snprintf(path, sizeof(path), "%s/cr_link_file", chroot_dir);
+    fd = open(path, O_CREAT | O_WRONLY, 0644);
+    if (fd < 0) { perror("open chroot file"); return 1; }
+    check("chroot: write", write(fd, "hello", 5) == 5);
+    close(fd);
+    check("chroot: chown", chown(path, 100, 200) == 0);
+
+    if (chroot(chroot_dir) != 0) {
+        perror("chroot");
+        return 1;
+    }
+    if (chdir("/") != 0) {
+        perror("chdir /");
+        return 1;
+    }
+
+    /* symlink with absolute target inside chroot.
+     * The fix in pseudo_util.c strips a doubled chroot prefix
+     * from absolute symlink targets during path resolution.
+     * Without the fix, stat through this symlink would fail or
+     * resolve to the wrong path.
+     */
+    check("chroot: symlink abs", symlink("/cr_link_file", "/cr_sym_abs") == 0);
+    check("chroot: stat through abs symlink", stat("/cr_sym_abs", &st) == 0);
+    check("chroot: abs symlink uid", st.st_uid == 100);
+    check("chroot: abs symlink gid", st.st_gid == 200);
+
+    /* readlink should return the chroot-relative target */
+    len = readlink("/cr_sym_abs", buf, sizeof(buf));
+    check("chroot: readlink abs len", len > 0);
+    if (len > 0) {
+        buf[len] = '\0';
+        check("chroot: readlink abs value", strcmp(buf, "/cr_link_file") == 0);
+    }
+
+    /* symlink with relative target inside chroot */
+    check("chroot: symlink rel", symlink("cr_link_file", "/cr_sym_rel") == 0);
+    check("chroot: stat through rel symlink", stat("/cr_sym_rel", &st) == 0);
+    check("chroot: rel symlink uid", st.st_uid == 100);
+
+    /* symlinkat with absolute target inside chroot */
+    dirfd = open("/", O_RDONLY | O_DIRECTORY);
+    check("chroot: open dirfd", dirfd >= 0);
+    check("chroot: symlinkat abs", symlinkat("/cr_link_file", dirfd, "cr_symat_abs") == 0);
+    check("chroot: stat symlinkat abs", fstatat(dirfd, "cr_symat_abs", &st, 0) == 0);
+    check("chroot: symlinkat abs uid", st.st_uid == 100);
+
+    /* readlinkat should return the chroot-relative target */
+    len = readlinkat(dirfd, "cr_symat_abs", buf, sizeof(buf));
+    check("chroot: readlinkat abs len", len > 0);
+    if (len > 0) {
+        buf[len] = '\0';
+        check("chroot: readlinkat abs value", strcmp(buf, "/cr_link_file") == 0);
+    }
+
+    /* linkat inside chroot */
+    check("chroot: linkat", linkat(dirfd, "cr_link_file", dirfd, "cr_hard", 0) == 0);
+    check("chroot: stat linkat", fstatat(dirfd, "cr_hard", &st, 0) == 0);
+    check("chroot: linkat uid", st.st_uid == 100);
+
+    close(dirfd);
+    return 0;
+}
+
+int main(int argc, char *argv[]) {
+    int rc;
+
+    rc = test_basic();
+    if (rc)
+        return rc;
+
+    if (argc > 1) {
+        rc = test_chroot(argv[1]);
+        if (rc)
+            return rc;
+    }
+
+    return failures;
+}
diff --git a/test/test-link-symlink.sh b/test/test-link-symlink.sh
new file mode 100755
index 0000000..f758f37
--- /dev/null
+++ b/test/test-link-symlink.sh
@@ -0,0 +1,11 @@ 
+#!/bin/bash
+#
+# SPDX-License-Identifier: LGPL-2.1-only
+#
+# Test link, linkat, symlink, symlinkat, readlink, readlinkat (basic + chroot)
+
+CHROOT_DIR=$(mktemp -d "${PWD}/chroot_link_XXXXXX")
+trap "rm -rf '$CHROOT_DIR'" 0
+
+rm -f test_link_file test_link_sym test_link_sym2 test_link_hard test_link_hard2
+./test/test-link-symlink "$CHROOT_DIR"