#define _GNU_SOURCE
#include "test-common.h"
#include <sys/stat.h>
#include <sys/xattr.h>
#include <sys/time.h>
#include <fcntl.h>
#include <unistd.h>
#include <utime.h>
* Test for inode stability after copy-up in overlay filesystem.
*
* When a file is copied from the base layer to the delta layer (copy-up),
* the kernel caches the original inode number. If we return a different
* inode after copy-up, the kernel's cache becomes inconsistent, causing
* ENOENT errors or other failures.
*
* This test verifies that inode numbers remain stable when copy-up is
* triggered by various syscalls:
* - write() / pwrite() - writing to a file
* - truncate() / ftruncate() - changing file size
* - chmod() / fchmod() - changing permissions
* - chown() / fchown() - changing ownership
* - rename() - moving/renaming a file
* - link() - creating hard links
* - utimes() / utimensat() - changing timestamps
* - setxattr() - setting extended attributes
* - fallocate() - allocating file space
*
* Test setup (in test-run-syscalls.sh):
* Files named copyup_<syscall>_test.txt are created in the base layer
* before the overlay is mounted.
*
* Related to Linux overlayfs's trusted.overlay.origin mechanism.
*/
* Helper: Check that inode is stable after a copy-up operation.
* Returns 0 on success, -1 on failure.
*/
static int check_inode_stable(const char *path, ino_t expected_ino, const char *op_name) {
struct stat st;
int result = stat(path, &st);
if (result < 0) {
fprintf(stderr, " stat after %s failed: %s\n", op_name, strerror(errno));
return -1;
}
if (st.st_ino != expected_ino) {
fprintf(stderr, " INODE CHANGED after %s: was %lu, now %lu\n",
op_name, (unsigned long)expected_ino, (unsigned long)st.st_ino);
return -1;
}
return 0;
}
* Helper: Check if a file exists in the base layer (skip test if not).
* Returns the original inode, or 0 if the file doesn't exist.
*/
static ino_t get_base_layer_inode(const char *base_path, const char *filename, const char *test_name) {
char path[512];
struct stat st;
snprintf(path, sizeof(path), "%s/%s", base_path, filename);
if (stat(path, &st) < 0) {
if (errno == ENOENT) {
printf(" (Skipping %s test - %s not in base layer)\n", test_name, filename);
return 0;
}
fprintf(stderr, " stat on %s failed: %s\n", filename, strerror(errno));
return 0;
}
return st.st_ino;
}
* Test 1: write() triggered copy-up
*
* Writing to a base layer file should trigger copy-up while preserving
* the original inode number.
*/
static int test_write_copyup(const char *base_path) {
char path[512];
ino_t orig_ino;
int fd, result;
orig_ino = get_base_layer_inode(base_path, "copyup_write_test.txt", "write copyup");
if (orig_ino == 0) return 0;
snprintf(path, sizeof(path), "%s/copyup_write_test.txt", base_path);
fd = open(path, O_WRONLY | O_APPEND);
TEST_ASSERT_ERRNO(fd >= 0, "open for write should succeed");
result = write(fd, " appended data", 14);
TEST_ASSERT_ERRNO(result == 14, "write should succeed");
close(fd);
TEST_ASSERT(check_inode_stable(path, orig_ino, "write") == 0,
"inode must remain stable after write copy-up");
fd = open(path, O_RDONLY);
TEST_ASSERT_ERRNO(fd >= 0, "open after write should succeed");
struct stat st;
result = fstat(fd, &st);
TEST_ASSERT_ERRNO(result == 0, "fstat after write should succeed");
TEST_ASSERT(st.st_ino == orig_ino, "fstat must return stable inode after write copy-up");
close(fd);
return 0;
}
* Test 2: truncate() triggered copy-up
*
* Truncating a base layer file should trigger copy-up while preserving
* the original inode number.
*/
static int test_truncate_copyup(const char *base_path) {
char path[512];
ino_t orig_ino;
int result;
orig_ino = get_base_layer_inode(base_path, "copyup_truncate_test.txt", "truncate copyup");
if (orig_ino == 0) return 0;
snprintf(path, sizeof(path), "%s/copyup_truncate_test.txt", base_path);
result = truncate(path, 10);
TEST_ASSERT_ERRNO(result == 0, "truncate should succeed");
TEST_ASSERT(check_inode_stable(path, orig_ino, "truncate") == 0,
"inode must remain stable after truncate copy-up");
int fd = open(path, O_WRONLY);
TEST_ASSERT_ERRNO(fd >= 0, "open for ftruncate should succeed");
result = ftruncate(fd, 5);
TEST_ASSERT_ERRNO(result == 0, "ftruncate should succeed");
struct stat st;
result = fstat(fd, &st);
TEST_ASSERT_ERRNO(result == 0, "fstat after ftruncate should succeed");
TEST_ASSERT(st.st_ino == orig_ino, "fstat must return stable inode after ftruncate");
close(fd);
return 0;
}
* Test 3: chmod() triggered copy-up
*
* Changing permissions on a base layer file should trigger copy-up
* while preserving the original inode number.
*/
static int test_chmod_copyup(const char *base_path) {
char path[512];
ino_t orig_ino;
int result;
orig_ino = get_base_layer_inode(base_path, "copyup_chmod_test.txt", "chmod copyup");
if (orig_ino == 0) return 0;
snprintf(path, sizeof(path), "%s/copyup_chmod_test.txt", base_path);
result = chmod(path, 0755);
TEST_ASSERT_ERRNO(result == 0, "chmod should succeed");
TEST_ASSERT(check_inode_stable(path, orig_ino, "chmod") == 0,
"inode must remain stable after chmod copy-up");
int fd = open(path, O_RDONLY);
TEST_ASSERT_ERRNO(fd >= 0, "open for fchmod should succeed");
result = fchmod(fd, 0700);
TEST_ASSERT_ERRNO(result == 0, "fchmod should succeed");
struct stat st;
result = fstat(fd, &st);
TEST_ASSERT_ERRNO(result == 0, "fstat after fchmod should succeed");
TEST_ASSERT(st.st_ino == orig_ino, "fstat must return stable inode after fchmod");
close(fd);
return 0;
}
* Test 4: chown() triggered copy-up
*
* Changing ownership on a base layer file should trigger copy-up
* while preserving the original inode number.
*
* Note: This may fail without root privileges, which is expected.
*/
static int test_chown_copyup(const char *base_path) {
char path[512];
ino_t orig_ino;
struct stat st;
int result;
orig_ino = get_base_layer_inode(base_path, "copyup_chown_test.txt", "chown copyup");
if (orig_ino == 0) return 0;
snprintf(path, sizeof(path), "%s/copyup_chown_test.txt", base_path);
result = stat(path, &st);
TEST_ASSERT_ERRNO(result == 0, "stat for chown should succeed");
result = chown(path, st.st_uid, st.st_gid);
if (result < 0 && (errno == EPERM || errno == ENOSYS)) {
printf(" (Skipping chown test - operation not permitted)\n");
return 0;
}
TEST_ASSERT_ERRNO(result == 0, "chown should succeed");
TEST_ASSERT(check_inode_stable(path, orig_ino, "chown") == 0,
"inode must remain stable after chown copy-up");
result = lchown(path, st.st_uid, st.st_gid);
TEST_ASSERT_ERRNO(result == 0, "lchown should succeed");
TEST_ASSERT(check_inode_stable(path, orig_ino, "lchown") == 0,
"inode must remain stable after lchown copy-up");
return 0;
}
* Test 5: rename() triggered copy-up
*
* Renaming a base layer file should trigger copy-up while preserving
* the original inode number (at the new path).
*/
static int test_rename_copyup(const char *base_path) {
char orig_path[512], new_path[512];
ino_t orig_ino;
int result;
orig_ino = get_base_layer_inode(base_path, "copyup_rename_test.txt", "rename copyup");
if (orig_ino == 0) return 0;
snprintf(orig_path, sizeof(orig_path), "%s/copyup_rename_test.txt", base_path);
snprintf(new_path, sizeof(new_path), "%s/copyup_rename_test_renamed.txt", base_path);
unlink(new_path);
result = rename(orig_path, new_path);
TEST_ASSERT_ERRNO(result == 0, "rename should succeed");
TEST_ASSERT(check_inode_stable(new_path, orig_ino, "rename") == 0,
"inode must remain stable after rename copy-up");
struct stat st;
result = stat(orig_path, &st);
TEST_ASSERT(result < 0 && errno == ENOENT,
"original path should not exist after rename");
unlink(new_path);
return 0;
}
* Test 6: link() triggered copy-up
*
* Creating a hard link to a base layer file should trigger copy-up
* while preserving the original inode number for both paths.
*/
static int test_link_copyup(const char *base_path) {
char orig_path[512], link_path[512], link2_path[512];
struct stat st_orig, st_link;
ino_t orig_ino;
int result;
orig_ino = get_base_layer_inode(base_path, "copyup_link_test.txt", "link copyup");
if (orig_ino == 0) return 0;
snprintf(orig_path, sizeof(orig_path), "%s/copyup_link_test.txt", base_path);
snprintf(link_path, sizeof(link_path), "%s/copyup_link_test_hardlink.txt", base_path);
snprintf(link2_path, sizeof(link2_path), "%s/copyup_link_test_hardlink2.txt", base_path);
unlink(link_path);
unlink(link2_path);
result = link(orig_path, link_path);
if (result < 0 && (errno == ENOSYS || errno == EOPNOTSUPP)) {
printf(" (Skipping link copyup test - link syscall not supported)\n");
return 0;
}
TEST_ASSERT_ERRNO(result == 0, "link should succeed");
TEST_ASSERT(check_inode_stable(orig_path, orig_ino, "link (original)") == 0,
"original inode must remain stable after link copy-up");
result = stat(link_path, &st_link);
TEST_ASSERT_ERRNO(result == 0, "stat on hard link should succeed");
if (st_link.st_ino != orig_ino) {
fprintf(stderr, " hard link inode mismatch: expected %lu, got %lu\n",
(unsigned long)orig_ino, (unsigned long)st_link.st_ino);
}
TEST_ASSERT(st_link.st_ino == orig_ino,
"hard link must have same inode as original");
result = stat(orig_path, &st_orig);
TEST_ASSERT_ERRNO(result == 0, "stat on original should succeed");
TEST_ASSERT(st_orig.st_nlink >= 2,
"link count should be at least 2 after creating hard link");
result = link(orig_path, link2_path);
TEST_ASSERT_ERRNO(result == 0, "creating second hard link should succeed");
result = stat(link2_path, &st_link);
TEST_ASSERT_ERRNO(result == 0, "stat on second hard link should succeed");
TEST_ASSERT(st_link.st_ino == orig_ino,
"second hard link must have same inode as original");
TEST_ASSERT(check_inode_stable(orig_path, orig_ino, "link (after second link)") == 0,
"original inode must remain stable after second link");
result = lstat(orig_path, &st_orig);
TEST_ASSERT_ERRNO(result == 0, "lstat on original should succeed");
TEST_ASSERT(st_orig.st_ino == orig_ino,
"lstat must return same inode after link copy-up");
result = lstat(link_path, &st_link);
TEST_ASSERT_ERRNO(result == 0, "lstat on hard link should succeed");
TEST_ASSERT(st_link.st_ino == orig_ino,
"lstat on hard link must return same inode");
result = unlink(link_path);
TEST_ASSERT_ERRNO(result == 0, "unlink first hard link should succeed");
TEST_ASSERT(check_inode_stable(orig_path, orig_ino, "link (after unlink)") == 0,
"original inode must remain stable after unlinking hard link");
TEST_ASSERT(check_inode_stable(link2_path, orig_ino, "link (remaining link)") == 0,
"remaining hard link must have same inode after unlink");
unlink(link2_path);
return 0;
}
* Test 7: utimes() / utimensat() triggered copy-up
*
* Changing timestamps on a base layer file should trigger copy-up
* while preserving the original inode number.
*/
static int test_utimes_copyup(const char *base_path) {
char path[512];
ino_t orig_ino;
int result;
struct timeval times[2];
struct timespec ts[2];
orig_ino = get_base_layer_inode(base_path, "copyup_utimes_test.txt", "utimes copyup");
if (orig_ino == 0) return 0;
snprintf(path, sizeof(path), "%s/copyup_utimes_test.txt", base_path);
times[0].tv_sec = 1000000000;
times[0].tv_usec = 0;
times[1].tv_sec = 1000000000;
times[1].tv_usec = 0;
result = utimes(path, times);
if (result < 0 && errno == ENOSYS) {
printf(" (Skipping utimes copyup test - utimes not supported)\n");
return 0;
}
TEST_ASSERT_ERRNO(result == 0, "utimes should succeed");
TEST_ASSERT(check_inode_stable(path, orig_ino, "utimes") == 0,
"inode must remain stable after utimes copy-up");
ts[0].tv_sec = 1000000001;
ts[0].tv_nsec = 0;
ts[1].tv_sec = 1000000001;
ts[1].tv_nsec = 0;
result = utimensat(AT_FDCWD, path, ts, 0);
if (result < 0 && errno == ENOSYS) {
printf(" (utimensat not supported, skipping that part)\n");
return 0;
}
TEST_ASSERT_ERRNO(result == 0, "utimensat should succeed");
TEST_ASSERT(check_inode_stable(path, orig_ino, "utimensat") == 0,
"inode must remain stable after utimensat copy-up");
int fd = open(path, O_RDWR);
if (fd >= 0) {
ts[0].tv_sec = 1000000002;
ts[1].tv_sec = 1000000002;
result = futimens(fd, ts);
if (result == 0) {
struct stat st;
result = fstat(fd, &st);
TEST_ASSERT_ERRNO(result == 0, "fstat after futimens should succeed");
TEST_ASSERT(st.st_ino == orig_ino,
"fstat must return stable inode after futimens");
}
close(fd);
}
return 0;
}
* Test 8: setxattr() triggered copy-up
*
* Setting extended attributes on a base layer file should trigger copy-up
* while preserving the original inode number.
*
* Note: Extended attributes may not be supported on all filesystems.
*/
static int test_xattr_copyup(const char *base_path) {
char path[512];
ino_t orig_ino;
int result;
const char *value = "test_value";
orig_ino = get_base_layer_inode(base_path, "copyup_xattr_test.txt", "xattr copyup");
if (orig_ino == 0) return 0;
snprintf(path, sizeof(path), "%s/copyup_xattr_test.txt", base_path);
result = setxattr(path, "user.test_attr", value, strlen(value), 0);
if (result < 0) {
if (errno == ENOTSUP || errno == EOPNOTSUPP || errno == ENOSYS) {
printf(" (Skipping xattr copyup test - xattr not supported)\n");
return 0;
}
if (errno == EPERM) {
printf(" (Skipping xattr copyup test - permission denied)\n");
return 0;
}
}
TEST_ASSERT_ERRNO(result == 0, "setxattr should succeed");
TEST_ASSERT(check_inode_stable(path, orig_ino, "setxattr") == 0,
"inode must remain stable after setxattr copy-up");
result = lsetxattr(path, "user.test_attr2", value, strlen(value), 0);
if (result == 0) {
TEST_ASSERT(check_inode_stable(path, orig_ino, "lsetxattr") == 0,
"inode must remain stable after lsetxattr copy-up");
}
result = removexattr(path, "user.test_attr");
if (result == 0) {
TEST_ASSERT(check_inode_stable(path, orig_ino, "removexattr") == 0,
"inode must remain stable after removexattr copy-up");
}
return 0;
}
* Test 9: fallocate() triggered copy-up
*
* Allocating space in a base layer file should trigger copy-up
* while preserving the original inode number.
*/
static int test_fallocate_copyup(const char *base_path) {
char path[512];
ino_t orig_ino;
int fd, result;
orig_ino = get_base_layer_inode(base_path, "copyup_fallocate_test.txt", "fallocate copyup");
if (orig_ino == 0) return 0;
snprintf(path, sizeof(path), "%s/copyup_fallocate_test.txt", base_path);
fd = open(path, O_RDWR);
TEST_ASSERT_ERRNO(fd >= 0, "open for fallocate should succeed");
result = fallocate(fd, 0, 0, 1024);
if (result < 0) {
if (errno == ENOTSUP || errno == EOPNOTSUPP || errno == ENOSYS) {
printf(" (Skipping fallocate copyup test - fallocate not supported)\n");
close(fd);
return 0;
}
}
TEST_ASSERT_ERRNO(result == 0, "fallocate should succeed");
struct stat st;
result = fstat(fd, &st);
TEST_ASSERT_ERRNO(result == 0, "fstat after fallocate should succeed");
if (st.st_ino != orig_ino) {
fprintf(stderr, " fstat inode mismatch after fallocate: expected %lu, got %lu\n",
(unsigned long)orig_ino, (unsigned long)st.st_ino);
}
TEST_ASSERT(st.st_ino == orig_ino,
"fstat must return stable inode after fallocate copy-up");
close(fd);
TEST_ASSERT(check_inode_stable(path, orig_ino, "fallocate") == 0,
"inode must remain stable after fallocate copy-up");
return 0;
}
* Main entry point for copyup inode stability tests.
*
* Runs all copy-up triggered tests and reports results.
*/
int test_copyup_inode_stability(const char *base_path) {
int result;
result = test_write_copyup(base_path);
if (result != 0) return result;
result = test_truncate_copyup(base_path);
if (result != 0) return result;
result = test_chmod_copyup(base_path);
if (result != 0) return result;
result = test_chown_copyup(base_path);
if (result != 0) return result;
result = test_rename_copyup(base_path);
if (result != 0) return result;
result = test_link_copyup(base_path);
if (result != 0) return result;
result = test_utimes_copyup(base_path);
if (result != 0) return result;
result = test_xattr_copyup(base_path);
if (result != 0) return result;
result = test_fallocate_copyup(base_path);
if (result != 0) return result;
return 0;
}