[PATCH v4 1/8] fs/acl: Add ACL_USER_OBJ permissions test

Sachin Sant <[email protected]>
Newsgroups gmane.linux.ltp
Message-ID <[email protected]>
Add acl_user_obj01 test to validate ACL_USER_OBJ (owner) permissions.
This test verifies that:
- Traditional permission bits restrict owner access
- ACL_USER_OBJ entries override permission bits
- Owner permissions work correctly with ACL entries

The test creates a directory with restrictive permissions, verifies
that file creation is denied, then sets ACL_USER_OBJ with write
permissions and confirms that file creation succeeds.

The patch also adds acl_lib.h containing shared helper functions
and definitions for ACL and extended attribute tests. This
library provides:

- Test user management (ltpuser1, ltpuser2, ltpuser3)
- ACL entry creation and manipulation helpers
- File operation helpers with different uid/gid
- Test path management and cleanup functions
- Extended attribute test definitions
- reset_test_path_no_chown variant to skip chown step

Signed-off-by: Sachin Sant <[email protected]>
---
V4 changes:
- Add -U flag in create_user_if_needed() to useradd for guaranteed
  user-private groups.
- Move EOPNOTSUPP handling into set_acl_file() helper
- v3 link https://lore.kernel.org/ltp/[email protected]/T/#t

V3 changes:
- Updated copyright header as per LTP format.
- v2 link https://lore.kernel.org/ltp/[email protected]/T/#t

V2 changes:
- Added reset_test_path_no_chown variant to skip chown step.
 acl_link01 and xattr_test01 tests are updated to use this
 variant.
- Updated acl_user_obj01.c to correct incorrect description
- v1 link https://lore.kernel.org/ltp/[email protected]/T/#t

V1 changes:
- Use ACL_LIBS variable instead of hardcoded -lacl in Makefile
- Move ACL header includes inside feature guards in acl_lib.h
- Use HAVE_LIBACL guards in .c code
- Report TCONF when libacl is not available
- rfc link https://lore.kernel.org/ltp/[email protected]/T/#t

---
 runtest/fs                               |   3 +
 testcases/kernel/fs/acl/.gitignore       |   1 +
 testcases/kernel/fs/acl/Makefile         |  10 +
 testcases/kernel/fs/acl/acl_lib.h        | 357 +++++++++++++++++++++++
 testcases/kernel/fs/acl/acl_user_obj01.c | 116 ++++++++
 5 files changed, 487 insertions(+)
 create mode 100644 testcases/kernel/fs/acl/.gitignore
 create mode 100644 testcases/kernel/fs/acl/Makefile
 create mode 100644 testcases/kernel/fs/acl/acl_lib.h
 create mode 100644 testcases/kernel/fs/acl/acl_user_obj01.c

diff --git a/runtest/fs b/runtest/fs
index 1d753e0dd..2a878744b 100644
--- a/runtest/fs
+++ b/runtest/fs
@@ -87,3 +87,6 @@ binfmt_misc01 binfmt_misc01.sh
 binfmt_misc02 binfmt_misc02.sh
 
 squashfs01 squashfs01
+
+# Run the acl tests
+acl_user_obj01 acl_user_obj01
diff --git a/testcases/kernel/fs/acl/.gitignore b/testcases/kernel/fs/acl/.gitignore
new file mode 100644
index 000000000..d9c46db11
--- /dev/null
+++ b/testcases/kernel/fs/acl/.gitignore
@@ -0,0 +1 @@
+/acl_user_obj01
diff --git a/testcases/kernel/fs/acl/Makefile b/testcases/kernel/fs/acl/Makefile
new file mode 100644
index 000000000..e094549a7
--- /dev/null
+++ b/testcases/kernel/fs/acl/Makefile
@@ -0,0 +1,10 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# Copyright (c) 2026 IBM
+
+top_srcdir		?= ../../../..
+
+include $(top_srcdir)/include/mk/testcases.mk
+
+LDLIBS			+= $(ACL_LIBS)
+
+include $(top_srcdir)/include/mk/generic_leaf_target.mk
diff --git a/testcases/kernel/fs/acl/acl_lib.h b/testcases/kernel/fs/acl/acl_lib.h
new file mode 100644
index 000000000..7d238ce0f
--- /dev/null
+++ b/testcases/kernel/fs/acl/acl_lib.h
@@ -0,0 +1,357 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (c) 2026 IBM
+ * Original shell test by Kai Zhao ([email protected])
+ * Converted to C by Sachin Sant <[email protected]>
+ *
+ * Common library for ACL and extended attribute tests
+ */
+
+#ifndef ACL_LIB_H
+#define ACL_LIB_H
+
+#include <pwd.h>
+#include <grp.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <sys/xattr.h>
+
+#include "config.h"
+#include "tst_test.h"
+#include "tst_safe_stdio.h"
+
+#ifdef HAVE_LIBACL
+#include <sys/acl.h>
+#include <acl/libacl.h>
+#endif
+
+#define MNTPOINT	"mntpoint"
+#define TESTDIR		MNTPOINT "/testdir"
+#define TESTFILE	TESTDIR "/testfile"
+#define TESTSYMLINK	TESTDIR "/testsymlink"
+#define XATTR_BACKUP_FILE MNTPOINT "/xattr_backup.txt"
+#define TEST_USER1	"ltpuser1"
+#define TEST_USER2	"ltpuser2"
+#define TEST_USER3	"ltpuser3"
+
+#define USER1_CREATED (1 << 0)
+#define USER2_CREATED (1 << 1)
+#define USER3_CREATED (1 << 2)
+
+/* Extended attribute test values */
+#define XATTR_TEST_DIR_NAME	"user.test_attr"
+#define XATTR_TEST_DIR_VALUE	"test_value"
+#define XATTR_TEST_DIR_SIZE	10
+#define XATTR_TEST_FILE_NAME	"user.file_attr"
+#define XATTR_TEST_FILE_VALUE	"file_val"
+#define XATTR_TEST_FILE_SIZE	8
+#define XATTR_TEST1_NAME	"user.test1"
+#define XATTR_TEST1_VALUE	"value1"
+#define XATTR_TEST1_SIZE	6
+#define XATTR_TEST2_NAME	"user.test2"
+#define XATTR_TEST2_VALUE	"value2"
+#define XATTR_TEST2_SIZE	6
+
+/* Global variables for test users */
+extern uid_t user1_uid, user2_uid, user3_uid;
+extern gid_t user1_gid, user2_gid, user3_gid;
+extern int users_created;
+
+/* Helper functions */
+static inline void reset_test_path_no_chown(void)
+{
+	if (unlink(TESTSYMLINK) == -1 && errno != ENOENT)
+		tst_res(TWARN | TERRNO, "unlink(%s) failed", TESTSYMLINK);
+
+	if (unlink(TESTFILE) == -1 && errno != ENOENT)
+		tst_res(TWARN | TERRNO, "unlink(%s) failed", TESTFILE);
+
+	if (rmdir(TESTDIR) == -1 && errno != ENOENT)
+		tst_res(TWARN | TERRNO, "rmdir(%s) failed", TESTDIR);
+
+	SAFE_MKDIR(TESTDIR, 0755);
+}
+
+static inline void reset_test_path(void)
+{
+	reset_test_path_no_chown();
+	SAFE_CHOWN(TESTDIR, user1_uid, user1_gid);
+}
+
+static inline void cleanup_testfile(void)
+{
+	if (unlink(TESTFILE) == -1 && errno != ENOENT)
+		tst_res(TWARN | TERRNO, "unlink(%s) failed", TESTFILE);
+}
+
+#ifdef HAVE_LIBACL
+static inline void safe_acl_free(void *ptr)
+{
+	if (ptr && acl_free(ptr) == -1)
+		tst_res(TWARN | TERRNO, "acl_free failed");
+}
+
+static inline void safe_add_perm(acl_permset_t permset, acl_perm_t perm)
+{
+	if (acl_add_perm(permset, perm) == -1)
+		tst_brk(TBROK | TERRNO, "acl_add_perm failed");
+}
+
+static inline void safe_fill_permset(acl_permset_t permset, int perms)
+{
+	if (acl_clear_perms(permset) == -1)
+		tst_brk(TBROK | TERRNO, "acl_clear_perms failed");
+
+	if (perms & ACL_READ)
+		safe_add_perm(permset, ACL_READ);
+
+	if (perms & ACL_WRITE)
+		safe_add_perm(permset, ACL_WRITE);
+
+	if (perms & ACL_EXECUTE)
+		safe_add_perm(permset, ACL_EXECUTE);
+}
+
+/*
+ * Helper to clear ACL mask permissions.
+ * Finds the ACL_MASK entry in the given ACL and sets its permissions to 0.
+ */
+static inline void clear_acl_mask_perms(acl_t acl)
+{
+	acl_entry_t entry;
+	acl_permset_t permset;
+	acl_tag_t tag;
+	int i;
+
+	for (i = ACL_FIRST_ENTRY; ; i = ACL_NEXT_ENTRY) {
+		int ret = acl_get_entry(acl, i, &entry);
+
+		if (ret == -1)
+			tst_brk(TBROK | TERRNO, "acl_get_entry failed");
+
+		if (ret != 1)
+			break;
+
+		if (acl_get_tag_type(entry, &tag) == -1)
+			tst_brk(TBROK | TERRNO, "acl_get_tag_type failed");
+
+		if (tag != ACL_MASK)
+			continue;
+
+		if (acl_get_permset(entry, &permset) == -1)
+			tst_brk(TBROK | TERRNO, "acl_get_permset failed");
+
+		safe_fill_permset(permset, 0);
+
+		if (acl_set_permset(entry, permset) == -1)
+			tst_brk(TBROK | TERRNO, "acl_set_permset failed");
+
+		break;
+	}
+}
+
+/*
+ * Helper to add an ACL entry with optional qualifier.
+ * For ACL_USER and ACL_GROUP tags, qualifier must be provided.
+ * For other tags (ACL_USER_OBJ, ACL_GROUP_OBJ, ACL_OTHER, ACL_MASK),
+ * qualifier should be NULL.
+ */
+static inline void add_acl_entry_with_qualifier(acl_t acl, acl_tag_t tag,
+						const void *qualifier,
+						int perms)
+{
+	acl_entry_t entry;
+	acl_permset_t permset;
+
+	if (acl_create_entry(&acl, &entry) == -1)
+		tst_brk(TBROK | TERRNO, "acl_create_entry failed");
+
+	if (acl_set_tag_type(entry, tag) == -1)
+		tst_brk(TBROK | TERRNO, "acl_set_tag_type failed");
+
+	/* Set qualifier for ACL_USER and ACL_GROUP entries */
+	if (qualifier && acl_set_qualifier(entry, qualifier) == -1)
+		tst_brk(TBROK | TERRNO, "acl_set_qualifier failed");
+
+	if (acl_get_permset(entry, &permset) == -1)
+		tst_brk(TBROK | TERRNO, "acl_get_permset failed");
+
+	safe_fill_permset(permset, perms);
+
+	if (acl_set_permset(entry, permset) == -1)
+		tst_brk(TBROK | TERRNO, "acl_set_permset failed");
+}
+
+/* Convenience wrapper for entries without qualifiers */
+static inline void add_acl_entry(acl_t acl, acl_tag_t tag, int perms)
+{
+	add_acl_entry_with_qualifier(acl, tag, NULL, perms);
+}
+
+/* Convenience wrapper for named user entries */
+static inline void add_named_user_acl_entry(acl_t acl, uid_t uid, int perms)
+{
+	add_acl_entry_with_qualifier(acl, ACL_USER, &uid, perms);
+}
+
+/* Convenience wrapper for named group entries */
+static inline void add_named_group_acl_entry(acl_t acl, gid_t gid, int perms)
+{
+	add_acl_entry_with_qualifier(acl, ACL_GROUP, &gid, perms);
+}
+
+static inline void add_empty_acl_entry(acl_t acl, acl_tag_t tag)
+{
+	add_acl_entry(acl, tag, 0);
+}
+
+static inline void set_acl_file(const char *path, acl_type_t type, acl_t acl)
+{
+	if (acl_valid(acl) == -1)
+		tst_brk(TBROK | TERRNO, "acl_valid failed");
+
+	if (acl_set_file(path, type, acl) == -1) {
+		if (errno == EOPNOTSUPP)
+			tst_brk(TCONF | TERRNO, "ACL not supported on %s",
+				path);
+		tst_brk(TBROK | TERRNO, "acl_set_file(%s) failed", path);
+	}
+}
+#endif
+
+static inline int create_file_as(uid_t uid, gid_t gid, mode_t mode,
+				 int use_umask, mode_t mask)
+{
+	pid_t pid;
+	int status;
+
+	pid = SAFE_FORK();
+	if (!pid) {
+		int fd, err;
+
+		if (setgroups(0, NULL) == -1) {
+			err = errno;
+			_exit(err);
+		}
+
+		if (setgid(gid) == -1) {
+			err = errno;
+			_exit(err);
+		}
+
+		if (setuid(uid) == -1) {
+			err = errno;
+			_exit(err);
+		}
+
+		if (use_umask)
+			umask(mask);
+
+		fd = open(TESTFILE, O_CREAT | O_WRONLY, mode);
+		if (fd >= 0) {
+			close(fd);
+			_exit(0);
+		}
+
+		err = errno;
+		_exit(err);
+	}
+
+	SAFE_WAITPID(pid, &status, 0);
+
+	if (!WIFEXITED(status))
+		tst_brk(TBROK, "Child terminated abnormally");
+
+	return WEXITSTATUS(status);
+}
+
+static inline int try_create_as(uid_t uid, gid_t gid, mode_t mode)
+{
+	return create_file_as(uid, gid, mode, 0, 0);
+}
+
+static inline int create_with_umask_as(uid_t uid, gid_t gid, mode_t mode,
+					mode_t mask)
+{
+	return create_file_as(uid, gid, mode, 1, mask);
+}
+
+static inline void create_user_if_needed(const char *username, int flag)
+{
+	struct passwd *pw;
+
+	errno = 0;
+	pw = getpwnam(username);
+	if (!pw && errno != 0)
+		tst_brk(TBROK | TERRNO, "getpwnam(%s) failed", username);
+
+	if (!pw) {
+		if (tst_cmd((const char *[]){"useradd", "-M", "-U", username,
+			    NULL}, NULL, NULL, TST_CMD_PASS_RETVAL) != 0)
+			tst_brk(TBROK, "Failed to create user %s", username);
+		users_created |= flag;
+	}
+}
+
+static inline void create_test_users(void)
+{
+	create_user_if_needed(TEST_USER1, USER1_CREATED);
+	create_user_if_needed(TEST_USER2, USER2_CREATED);
+	create_user_if_needed(TEST_USER3, USER3_CREATED);
+}
+
+static inline void init_test_users(void)
+{
+	struct passwd *pw;
+
+	create_test_users();
+
+	pw = SAFE_GETPWNAM(TEST_USER1);
+	user1_uid = pw->pw_uid;
+	user1_gid = pw->pw_gid;
+
+	pw = SAFE_GETPWNAM(TEST_USER2);
+	user2_uid = pw->pw_uid;
+	user2_gid = pw->pw_gid;
+
+	pw = SAFE_GETPWNAM(TEST_USER3);
+	user3_uid = pw->pw_uid;
+	user3_gid = pw->pw_gid;
+}
+
+static inline void cleanup_test_users(void)
+{
+	if (users_created & USER1_CREATED)
+		tst_cmd((const char *[]){"userdel", TEST_USER1, NULL},
+			NULL, NULL, TST_CMD_PASS_RETVAL);
+
+	if (users_created & USER2_CREATED)
+		tst_cmd((const char *[]){"userdel", TEST_USER2, NULL},
+			NULL, NULL, TST_CMD_PASS_RETVAL);
+
+	if (users_created & USER3_CREATED)
+		tst_cmd((const char *[]){"userdel", TEST_USER3, NULL},
+			NULL, NULL, TST_CMD_PASS_RETVAL);
+}
+
+static inline void cleanup_test_paths(void)
+{
+	if (unlink(TESTSYMLINK) == -1 && errno != ENOENT)
+		tst_res(TWARN | TERRNO, "unlink(%s) failed", TESTSYMLINK);
+
+	if (unlink(TESTFILE) == -1 && errno != ENOENT)
+		tst_res(TWARN | TERRNO, "unlink(%s) failed", TESTFILE);
+
+	if (unlink(XATTR_BACKUP_FILE) == -1 && errno != ENOENT)
+		tst_res(TWARN | TERRNO, "unlink(%s) failed",
+			XATTR_BACKUP_FILE);
+
+	if (rmdir(TESTDIR) == -1 && errno != ENOENT)
+		tst_res(TWARN | TERRNO, "rmdir(%s) failed", TESTDIR);
+}
+
+#endif /* ACL_LIB_H */
diff --git a/testcases/kernel/fs/acl/acl_user_obj01.c b/testcases/kernel/fs/acl/acl_user_obj01.c
new file mode 100644
index 000000000..6b97e4ccc
--- /dev/null
+++ b/testcases/kernel/fs/acl/acl_user_obj01.c
@@ -0,0 +1,116 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (c) 2026 IBM
+ *
+ * Original shell test by Kai Zhao ([email protected])
+ * Converted to C by Sachin Sant <[email protected]>
+ */
+
+/*\
+ * Test ACL_USER_OBJ permissions.
+ *
+ * Verify that owner permissions (ACL_USER_OBJ) correctly control access
+ * to files and directories. The test validates that:
+ * - ACL_USER_OBJ permissions are applied directly as the owner bits
+ * - Setting ACL_USER_OBJ=rwx via acl_set_file() overrides a previous
+ *   chmod restriction
+ * - Owner permissions work independently of group and other permissions
+ */
+
+#include "acl_lib.h"
+
+uid_t user1_uid, user2_uid, user3_uid;
+gid_t user1_gid, user2_gid, user3_gid;
+int users_created = 0;
+
+#ifdef HAVE_LIBACL
+
+static void run(void)
+{
+	acl_t acl;
+	int err;
+
+	tst_res(TINFO, "Testing ACL_USER_OBJ permissions");
+	reset_test_path();
+
+	/* Test 1: Verify permission bits deny access */
+	SAFE_CHMOD(TESTDIR, 0500);
+
+	err = try_create_as(user1_uid, user1_gid, 0644);
+	if (!err) {
+		cleanup_testfile();
+		tst_res(TFAIL, "Created file without write permission");
+		return;
+	}
+
+	if (err != EACCES) {
+		errno = err;
+		tst_res(TFAIL | TERRNO, "Expected EACCES from owner create");
+		return;
+	}
+
+	tst_res(TPASS, "File creation denied by permission bits");
+
+	/* Test 2: Verify ACL_USER_OBJ grants access */
+	acl = acl_init(3);
+	if (!acl)
+		tst_brk(TBROK | TERRNO, "acl_init failed");
+
+	add_acl_entry(acl, ACL_USER_OBJ,
+		      ACL_READ | ACL_WRITE | ACL_EXECUTE);
+	add_empty_acl_entry(acl, ACL_GROUP_OBJ);
+	add_empty_acl_entry(acl, ACL_OTHER);
+
+	set_acl_file(TESTDIR, ACL_TYPE_ACCESS, acl);
+	safe_acl_free(acl);
+
+	err = try_create_as(user1_uid, user1_gid, 0644);
+	if (err) {
+		errno = err;
+		tst_res(TFAIL | TERRNO,
+			"Failed to create file with ACL_USER_OBJ rwx");
+		return;
+	}
+
+	cleanup_testfile();
+	tst_res(TPASS, "ACL_USER_OBJ permissions work correctly");
+}
+
+static void setup(void)
+{
+	init_test_users();
+	reset_test_path();
+}
+
+static void cleanup(void)
+{
+	cleanup_test_paths();
+	cleanup_test_users();
+}
+
+static struct tst_test test = {
+	.test_all = run,
+	.setup = setup,
+	.cleanup = cleanup,
+	.needs_root = 1,
+	.mount_device = 1,
+	.mntpoint = MNTPOINT,
+	.forks_child = 1,
+	.filesystems = (struct tst_fs[]) {
+		{.type = "ext2", .mnt_data = "acl"},
+		{.type = "ext3", .mnt_data = "acl"},
+		{.type = "ext4", .mnt_data = "acl"},
+		{.type = "xfs"},
+		{.type = "btrfs"},
+		{}
+	},
+	.needs_cmds = (struct tst_cmd[]) {
+		{.cmd = "useradd"},
+		{.cmd = "userdel"},
+		{}
+	}
+};
+
+#else
+TST_TEST_TCONF("libacl or ACL headers are not available");
+#endif
-- 
2.39.1


-- 
Mailing list info: https://lists.linux.it/listinfo/ltp
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.