Skip to content

Commit a19bfd4

Browse files
committed
tests: Cover directory-mode hardlink tracking
Add a test-lcfs.c unit test that builds a tmpdir containing a real hardlink via lcfs_build() both with and without LCFS_BUILD_TRACK_HARDLINKS, checking that the default preserves today's nlink=1-for-everything behavior and that the new flag produces a proper link_to/nlink=2 pair. Also extend integration.sh with the exact repro from issue composefs#444 (dd + ln + mkcomposefs), checked via composefs-info dump rather than a full mount since this doesn't need root or a kernel driver. Assisted-by: AI Signed-off-by: Colin Walters <walters@verbum.org>
1 parent 8b5d0bf commit a19bfd4

2 files changed

Lines changed: 113 additions & 0 deletions

File tree

tests/integration.sh

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,39 @@ run_test() {
5656

5757
run_test /usr/bin "--no-nlink"
5858

59+
# Regression test for https://github.com/composefs/composefs/issues/444:
60+
# building directly from a directory (as opposed to --from-file) used to
61+
# silently drop hardlinks, giving every file nlink=1 even if two paths
62+
# were hardlinked to each other. This is opt-in via --hardlinks, so also
63+
# confirm the (unchanged) default behavior.
64+
test_hardlinks() {
65+
local dir=hardlink-test
66+
rm -rf "${dir}"
67+
mkdir -p "${dir}/src"
68+
dd if=/dev/urandom of="${dir}/src/file1" bs=1024 count=4 status=none
69+
ln "${dir}/src/file1" "${dir}/src/file2"
70+
71+
# Default (no --hardlinks): every path gets its own independent inode
72+
# with nlink=1, matching this tool's historical behavior.
73+
mkcomposefs "${dir}/src" "${dir}/default.cfs"
74+
default_dump=$(composefs-info dump "${dir}/default.cfs")
75+
test "$(echo "${default_dump}" | awk '$1=="/file1"{print $4}')" = "1"
76+
test "$(echo "${default_dump}" | awk '$1=="/file2"{print $4}')" = "1"
77+
! echo "${default_dump}" | grep -q '^/file[12] [0-9]* @'
78+
79+
# --hardlinks: file1 and file2 are the same inode (nlink=2), with
80+
# exactly one of the two recorded as a hardlink (the "@" marker from
81+
# composefs-dump(5)) pointing at the other.
82+
mkcomposefs --hardlinks "${dir}/src" "${dir}/hardlinks.cfs"
83+
hl_dump=$(composefs-info dump "${dir}/hardlinks.cfs")
84+
test "$(echo "${hl_dump}" | awk '$1=="/file1"{print $4}')" = "2"
85+
test "$(echo "${hl_dump}" | awk '$1=="/file2"{print $4}')" = "2"
86+
test "$(echo "${hl_dump}" | grep -c '^/file[12] [0-9]* @')" = "1"
87+
88+
rm -rf "${dir}"
89+
}
90+
test_hardlinks
91+
5992
check_fsverity () {
6093
fsverity --version >/dev/null 2>&1 || return 1
6194
tmpfile=$(mktemp --tmpdir lcfs-fsverity.XXXXXX)

tests/test-lcfs.c

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
#include <errno.h>
1212
#include <endian.h>
1313
#include <sys/stat.h>
14+
#include <fcntl.h>
15+
#include <stdlib.h>
16+
#include <limits.h>
1417

1518
static inline void lcfs_node_unrefp(struct lcfs_node_s **nodep)
1619
{
@@ -227,6 +230,82 @@ static void test_hardlinked_whiteout_load(void)
227230
assert(errsv == EINVAL);
228231
}
229232

233+
// Verifies that lcfs_build() only detects hardlinked regular files when
234+
// scanning a directory tree if LCFS_BUILD_TRACK_HARDLINKS is passed; by
235+
// default (matching historical behavior) every path gets its own
236+
// independent inode with nlink == 1.
237+
static void test_build_hardlinks(void)
238+
{
239+
char tmpdir[] = "/tmp/test-build-hardlinks.XXXXXX";
240+
assert(mkdtemp(tmpdir) != NULL);
241+
242+
char path1[PATH_MAX], path2[PATH_MAX];
243+
snprintf(path1, sizeof(path1), "%s/file1", tmpdir);
244+
snprintf(path2, sizeof(path2), "%s/file2", tmpdir);
245+
246+
int fd = open(path1, O_WRONLY | O_CREAT | O_TRUNC, 0644);
247+
assert(fd >= 0);
248+
assert(write(fd, "hello", 5) == 5);
249+
close(fd);
250+
251+
assert(link(path1, path2) == 0);
252+
253+
int dirfd = open(tmpdir, O_RDONLY | O_DIRECTORY);
254+
assert(dirfd >= 0);
255+
256+
/* Without LCFS_BUILD_TRACK_HARDLINKS: today's default behavior is
257+
* that every path becomes an independent inode with nlink == 1. */
258+
{
259+
char *failed_path = NULL;
260+
cleanup_node struct lcfs_node_s *root =
261+
lcfs_build(dirfd, ".", 0, &failed_path);
262+
assert(root != NULL);
263+
264+
struct lcfs_node_s *n1 = lcfs_node_lookup_child(root, "file1");
265+
struct lcfs_node_s *n2 = lcfs_node_lookup_child(root, "file2");
266+
assert(n1 != NULL);
267+
assert(n2 != NULL);
268+
assert(lcfs_node_get_hardlink_target(n1) == NULL);
269+
assert(lcfs_node_get_hardlink_target(n2) == NULL);
270+
assert(lcfs_node_get_nlink(n1) == 1);
271+
assert(lcfs_node_get_nlink(n2) == 1);
272+
273+
assert(testwrite_node(root) == 0);
274+
}
275+
276+
/* With LCFS_BUILD_TRACK_HARDLINKS: one of the two paths becomes a
277+
* hardlink to the other, and the target's nlink is bumped to 2. */
278+
{
279+
char *failed_path = NULL;
280+
cleanup_node struct lcfs_node_s *root = lcfs_build(
281+
dirfd, ".", LCFS_BUILD_TRACK_HARDLINKS, &failed_path);
282+
assert(root != NULL);
283+
284+
struct lcfs_node_s *n1 = lcfs_node_lookup_child(root, "file1");
285+
struct lcfs_node_s *n2 = lcfs_node_lookup_child(root, "file2");
286+
assert(n1 != NULL);
287+
assert(n2 != NULL);
288+
289+
/* Whichever of the two is visited first by readdir() becomes
290+
* the canonical inode; the other becomes a hardlink to it. */
291+
struct lcfs_node_s *target1 = lcfs_node_get_hardlink_target(n1);
292+
struct lcfs_node_s *target2 = lcfs_node_get_hardlink_target(n2);
293+
assert((target1 == NULL) != (target2 == NULL));
294+
295+
struct lcfs_node_s *link_node = target1 != NULL ? n1 : n2;
296+
struct lcfs_node_s *target_node = target1 != NULL ? n2 : n1;
297+
assert(lcfs_node_get_hardlink_target(link_node) == target_node);
298+
assert(lcfs_node_get_nlink(target_node) == 2);
299+
300+
assert(testwrite_node(root) == 0);
301+
}
302+
303+
close(dirfd);
304+
unlink(path1);
305+
unlink(path2);
306+
rmdir(tmpdir);
307+
}
308+
230309
// Verifies that lcfs_fd_measure_fsverity fails on a fd without fsverity
231310
static void test_no_verity(void)
232311
{
@@ -255,4 +334,5 @@ int main(int argc, char **argv)
255334
test_xattr_addremove();
256335
test_xattr_doubleadd();
257336
test_hardlinked_whiteout_load();
337+
test_build_hardlinks();
258338
}

0 commit comments

Comments
 (0)