All of lore.kernel.org
 help / color / mirror / Atom feed
From: Mark Hatle <mark.hatle@kernel.crashing.org>
To: yocto-patches@lists.yoctoproject.org, paul@pbarker.dev,
	changqing.li@windriver.com
Cc: richard.purdie@linuxfoundation.org
Subject: [pseudo][PATCH 2/5] tests: Add mv then hardlink testing
Date: Tue, 12 May 2026 17:20:40 -0500	[thread overview]
Message-ID: <1778624443-20857-3-git-send-email-mark.hatle@kernel.crashing.org> (raw)
In-Reply-To: <1778624443-20857-1-git-send-email-mark.hatle@kernel.crashing.org>

From: Mark Hatle <mark.hatle@amd.com>

Per the discussion:
   https://lists.openembedded.org/g/openembedded-core/topic/119214074#msg236712

Changqing Li found an issue with renameat (mv) from an ignored to an
included path was not updating the database properly.  This could lead
to an abort, but would definitely cause the new file/symlink to have
the wrong (in pseudo terms) uid/gid.

The test-mv-hardlink.sh is an attempt to implement the test case
suggested by Paul Barker.  It was noted that both rename and renameat
may suffer from the same issue, so additional test cases for each
implementation was added as well.

AI-Generated: Implemented with the assistance of github CoPilot (Claude Opus 4.6)

Signed-off-by: Mark Hatle <mark.hatle@amd.com>
---
 test/test-mv-hardlink.sh       |  52 +++++++++++++++++++++
 test/test-rename-hardlink.c    |  87 +++++++++++++++++++++++++++++++++++
 test/test-rename-hardlink.sh   |  14 ++++++
 test/test-renameat-hardlink.c  | 101 +++++++++++++++++++++++++++++++++++++++++
 test/test-renameat-hardlink.sh |  14 ++++++
 5 files changed, 268 insertions(+)
 create mode 100755 test/test-mv-hardlink.sh
 create mode 100644 test/test-rename-hardlink.c
 create mode 100755 test/test-rename-hardlink.sh
 create mode 100644 test/test-renameat-hardlink.c
 create mode 100755 test/test-renameat-hardlink.sh

diff --git a/test/test-mv-hardlink.sh b/test/test-mv-hardlink.sh
new file mode 100755
index 0000000..2963ef3
--- /dev/null
+++ b/test/test-mv-hardlink.sh
@@ -0,0 +1,52 @@
+#!/bin/bash
+#
+# SPDX-License-Identifier: LGPL-2.1-only
+#
+# Test that rename (mv) from outside PSEUDO_INCLUDE_PATHS followed by
+# hardlink properly tracks file ownership.
+#
+# Reproduces: https://lists.openembedded.org/g/openembedded-core/message/236712
+#
+# In Yocto, files are often moved from ${B} (build dir, outside
+# PSEUDO_INCLUDE_PATHS) to ${D} (image dir, tracked by pseudo) and
+# then hardlinked. If pseudo doesn't track the rename, the hardlink
+# gets recorded with the real UID, causing inconsistent ownership
+# and pseudo abort on subsequent stat.
+
+# Create two directories:
+#   srcdir  - simulates ${B}, outside PSEUDO_INCLUDE_PATHS
+#   destdir - simulates ${D}, inside PSEUDO_INCLUDE_PATHS
+# Use realpath to resolve symlinks, since pseudo canonicalizes paths
+# internally and the PSEUDO_INCLUDE_PATHS prefix must match.
+srcdir=$(mktemp -d "$(realpath "${PWD}")/mv_hl_src_XXXXXX")
+destdir=$(mktemp -d "$(realpath "${PWD}")/mv_hl_dest_XXXXXX")
+trap "rm -rf '$srcdir' '$destdir'" EXIT
+
+# Restrict pseudo tracking to only destdir
+export PSEUDO_INCLUDE_PATHS="$destdir"
+
+echo hello > ${srcdir}/hello.txt
+
+mv ${srcdir}/hello.txt ${destdir}/hello.txt
+ln ${destdir}/hello.txt ${destdir}/hello2.txt
+
+# Both files should report uid 0 under pseudo
+dest_uid=$(\ls -n1 ${destdir}/hello.txt | awk '{ print $3 }')
+link_uid=$(\ls -n1 ${destdir}/hello2.txt | awk '{ print $3 }')
+
+if [ "$dest_uid" != "0" ]; then
+    echo "FAIL: dest uid is $dest_uid, expected 0"
+    exit 1
+fi
+
+if [ "$link_uid" != "0" ]; then
+    echo "FAIL: link uid is $link_uid, expected 0"
+    exit 1
+fi
+
+if [ "$dest_uid" != "$link_uid" ]; then
+    echo "FAIL: UIDs don't match (dest=$dest_uid, link=$link_uid)"
+    exit 1
+fi
+
+exit 0
diff --git a/test/test-rename-hardlink.c b/test/test-rename-hardlink.c
new file mode 100644
index 0000000..d3f7384
--- /dev/null
+++ b/test/test-rename-hardlink.c
@@ -0,0 +1,87 @@
+/*
+ * Test that rename() from outside PSEUDO_INCLUDE_PATHS followed by
+ * hardlink properly tracks file ownership.
+ *
+ * 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 <string.h>
+#include <unistd.h>
+#include <limits.h>
+
+static int failures = 0;
+
+static void check(const char *desc, int condition) {
+	if (!condition) {
+		fprintf(stderr, "FAIL: %s\n", desc);
+		failures++;
+	}
+}
+
+int main(int argc, char *argv[])
+{
+	struct stat st1, st2;
+	char src_path[PATH_MAX];
+	char dest_path[PATH_MAX];
+	char link_path[PATH_MAX];
+	int fd;
+
+	if (argc != 3) {
+		fprintf(stderr, "Usage: %s <src_dir> <dest_dir>\n", argv[0]);
+		return 1;
+	}
+
+	/* Create a file in src_dir (outside PSEUDO_INCLUDE_PATHS) */
+	snprintf(src_path, sizeof(src_path), "%s/testfile.txt", argv[1]);
+	fd = open(src_path, O_CREAT | O_WRONLY, 0644);
+	if (fd < 0) {
+		perror("create source file");
+		return 1;
+	}
+	if (write(fd, "hello\n", 6) != 6) {
+		perror("write");
+		close(fd);
+		return 1;
+	}
+	close(fd);
+
+	/* rename() from untracked src_dir to tracked dest_dir */
+	snprintf(dest_path, sizeof(dest_path), "%s/testfile.txt", argv[2]);
+	if (rename(src_path, dest_path) != 0) {
+		perror("rename");
+		return 1;
+	}
+
+	/* Create a hardlink in the tracked directory */
+	snprintf(link_path, sizeof(link_path), "%s/testfile2.txt", argv[2]);
+	if (link(dest_path, link_path) != 0) {
+		perror("link");
+		return 1;
+	}
+
+	/* Stat both files and verify consistent uid 0 */
+	if (stat(dest_path, &st1) != 0) {
+		perror("stat dest");
+		return 1;
+	}
+	if (stat(link_path, &st2) != 0) {
+		perror("stat link");
+		return 1;
+	}
+
+	check("same inode", st1.st_ino == st2.st_ino);
+	check("UIDs match", st1.st_uid == st2.st_uid);
+	check("dest uid is 0", st1.st_uid == 0);
+	check("link uid is 0", st2.st_uid == 0);
+
+	unlink(link_path);
+	unlink(dest_path);
+
+	return failures;
+}
diff --git a/test/test-rename-hardlink.sh b/test/test-rename-hardlink.sh
new file mode 100755
index 0000000..8f2be60
--- /dev/null
+++ b/test/test-rename-hardlink.sh
@@ -0,0 +1,14 @@
+#!/bin/bash
+#
+# SPDX-License-Identifier: LGPL-2.1-only
+#
+# Test that rename() from outside PSEUDO_INCLUDE_PATHS followed by
+# hardlink properly tracks file ownership.
+
+srcdir=$(mktemp -d "$(realpath "${PWD}")/ren_hl_src_XXXXXX")
+destdir=$(mktemp -d "$(realpath "${PWD}")/ren_hl_dest_XXXXXX")
+trap "rm -rf '$srcdir' '$destdir'" EXIT
+
+export PSEUDO_INCLUDE_PATHS="$destdir"
+
+./test/test-rename-hardlink "$srcdir" "$destdir"
diff --git a/test/test-renameat-hardlink.c b/test/test-renameat-hardlink.c
new file mode 100644
index 0000000..9c4840c
--- /dev/null
+++ b/test/test-renameat-hardlink.c
@@ -0,0 +1,101 @@
+/*
+ * Test that renameat() from outside PSEUDO_INCLUDE_PATHS followed by
+ * hardlink properly tracks file ownership.
+ *
+ * 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 <string.h>
+#include <unistd.h>
+#include <limits.h>
+
+static int failures = 0;
+
+static void check(const char *desc, int condition) {
+	if (!condition) {
+		fprintf(stderr, "FAIL: %s\n", desc);
+		failures++;
+	}
+}
+
+int main(int argc, char *argv[])
+{
+	struct stat st1, st2;
+	char dest_path[PATH_MAX];
+	char link_path[PATH_MAX];
+	int fd, olddirfd, newdirfd;
+
+	if (argc != 3) {
+		fprintf(stderr, "Usage: %s <src_dir> <dest_dir>\n", argv[0]);
+		return 1;
+	}
+
+	/* Open directory fds for renameat */
+	olddirfd = open(argv[1], O_RDONLY | O_DIRECTORY);
+	if (olddirfd < 0) {
+		perror("open src_dir");
+		return 1;
+	}
+	newdirfd = open(argv[2], O_RDONLY | O_DIRECTORY);
+	if (newdirfd < 0) {
+		perror("open dest_dir");
+		close(olddirfd);
+		return 1;
+	}
+
+	/* Create a file in src_dir (outside PSEUDO_INCLUDE_PATHS) */
+	fd = openat(olddirfd, "testfile.txt", O_CREAT | O_WRONLY, 0644);
+	if (fd < 0) {
+		perror("create source file");
+		return 1;
+	}
+	if (write(fd, "hello\n", 6) != 6) {
+		perror("write");
+		close(fd);
+		return 1;
+	}
+	close(fd);
+
+	/* renameat() from untracked src_dir to tracked dest_dir */
+	if (renameat(olddirfd, "testfile.txt", newdirfd, "testfile.txt") != 0) {
+		perror("renameat");
+		return 1;
+	}
+
+	/* Create a hardlink using linkat in the tracked directory */
+	if (linkat(newdirfd, "testfile.txt", newdirfd, "testfile2.txt", 0) != 0) {
+		perror("linkat");
+		return 1;
+	}
+
+	/* Stat both files and verify consistent uid 0 */
+	snprintf(dest_path, sizeof(dest_path), "%s/testfile.txt", argv[2]);
+	snprintf(link_path, sizeof(link_path), "%s/testfile2.txt", argv[2]);
+
+	if (stat(dest_path, &st1) != 0) {
+		perror("stat dest");
+		return 1;
+	}
+	if (stat(link_path, &st2) != 0) {
+		perror("stat link");
+		return 1;
+	}
+
+	check("same inode", st1.st_ino == st2.st_ino);
+	check("UIDs match", st1.st_uid == st2.st_uid);
+	check("dest uid is 0", st1.st_uid == 0);
+	check("link uid is 0", st2.st_uid == 0);
+
+	unlinkat(newdirfd, "testfile2.txt", 0);
+	unlinkat(newdirfd, "testfile.txt", 0);
+	close(olddirfd);
+	close(newdirfd);
+
+	return failures;
+}
diff --git a/test/test-renameat-hardlink.sh b/test/test-renameat-hardlink.sh
new file mode 100755
index 0000000..b7e3a9e
--- /dev/null
+++ b/test/test-renameat-hardlink.sh
@@ -0,0 +1,14 @@
+#!/bin/bash
+#
+# SPDX-License-Identifier: LGPL-2.1-only
+#
+# Test that renameat() from outside PSEUDO_INCLUDE_PATHS followed by
+# hardlink properly tracks file ownership.
+
+srcdir=$(mktemp -d "$(realpath "${PWD}")/renat_hl_src_XXXXXX")
+destdir=$(mktemp -d "$(realpath "${PWD}")/renat_hl_dest_XXXXXX")
+trap "rm -rf '$srcdir' '$destdir'" EXIT
+
+export PSEUDO_INCLUDE_PATHS="$destdir"
+
+./test/test-renameat-hardlink "$srcdir" "$destdir"
-- 
1.8.3.1



  parent reply	other threads:[~2026-05-12 22:20 UTC|newest]

Thread overview: 6+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-05-12 22:20 [pseudo][PATCH 0/5] Fix rename/renameat w/ hardlinks Mark Hatle
2026-05-12 22:20 ` [pseudo][PATCH 1/5] run_tests.sh: Allow the user to specify specific tests to run Mark Hatle
2026-05-12 22:20 ` Mark Hatle [this message]
2026-05-12 22:20 ` [pseudo][PATCH 3/5] renameat2/renameat: only ignore when both old and new path are not in PSEUDO_INCLUDE_PATHS Mark Hatle
2026-05-12 22:20 ` [pseudo][PATCH 4/5] rename: " Mark Hatle
2026-05-12 22:20 ` [pseudo][PATCH 5/5] Makefile.in: Bump version to 1.9.7 Mark Hatle

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=1778624443-20857-3-git-send-email-mark.hatle@kernel.crashing.org \
    --to=mark.hatle@kernel.crashing.org \
    --cc=changqing.li@windriver.com \
    --cc=paul@pbarker.dev \
    --cc=richard.purdie@linuxfoundation.org \
    --cc=yocto-patches@lists.yoctoproject.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.