All of lore.kernel.org
 help / color / mirror / Atom feed
* [LTP] [PATCH v12 0/8] Convert shell-based ACL test (tacl_xattr.sh) to C
@ 2026-06-16  4:54 Sachin Sant
  2026-06-16  4:54 ` [LTP] [PATCH v12 1/8] fs/acl: Add ACL_USER_OBJ permission test Sachin Sant
                   ` (7 more replies)
  0 siblings, 8 replies; 23+ messages in thread
From: Sachin Sant @ 2026-06-16  4:54 UTC (permalink / raw)
  To: ltp

This patch series converts the existing shell-based ACL
test (tacl_xattr.sh) to C implementation, providing better
integration with the LTP test framework and more precise
control over test execution.

v11 -> v12
Updated patch 6/8. Rest of the patches remain unchanged.

v10 -> v11
Updated patches 1/8 and 5/8. Rest of the patches remain unchanged.

v9 -> v10
Updated patch 2/8 to incorporate review comments. Rest of the
patches remain unchanged.

v8 -> v9
Updated patches 1, 2, 5, and 7 to incorporate review comments.

v7 -> v8
Updated patches 1, 5, 6 and 7 to incorporate review comments.

v6 -> v7
Updated 1/8 to address review comments. Rest of the patches
in the series remain unchanged.

v5 -> v6
Updated patches 1 to 7 to address review comments. Added
HAVE_SYS_XATTR_H guards for systems without xattr support

v4 -> v5
Updated patches 1 to 7 to address review comments. Tests rely on
kernel only implementation and removes dependeny on acl library
and useradd/del commands

v3 -> v4
Updated patches 1/8, 4/8 & 6/8 to address review comments.
No changes to other patches in the series.

v2 -> v3
Updated patches 1 through 7 to address review comments.

v1 -> v2
Updated patches 1/8, 6/8 and 7/8 to address review comments.
Remaining patches remain unchanged

Addresses part of issue #1265

The conversion splits functionality into seven focused test files.
Patch 1/8 adds acl_user_obj01.c
Patch 2/8 adds acl_mask01.c
Patch 3/8 adds acl_other01.c
Patch 4/8 adds acl_inherit01.c
Patch 5/8 adds acl_file_ops01.c
Patch 6/8 adds acl_link01.c
Patch 7/8 adds xattr_test01.c

The code has been successfully tested on SLES (kernel 6.12)
and fedora (kernel 7.0) releases on ppc64le arch.

Test build is clean in various linux distribution releases. CI
build report at https://github.com/sacsant/ltp/actions/runs/27553802425

Sachin Sant (8):
  fs/acl: Add ACL_USER_OBJ permission test
  fs/acl: Add ACL mask interaction tests
  ifs/acl: Add ACL_OTHER permission test
  fs/acl: Add default ACL inheritance test
  fs/acl: Add chmod/chown ACL interaction tests
  fs/acl: Add ACL symlink operations test
  fs/acl: Add extended attributes test
  fs/acl: Remove old shell-based ACL test

 runtest/fs                               |   9 +
 testcases/kernel/fs/acl/.gitignore       |   7 +
 testcases/kernel/fs/acl/Makefile         |   8 +
 testcases/kernel/fs/acl/acl_file_ops01.c | 264 ++++++++
 testcases/kernel/fs/acl/acl_inherit01.c  | 113 ++++
 testcases/kernel/fs/acl/acl_lib.h        | 509 ++++++++++++++
 testcases/kernel/fs/acl/acl_link01.c     | 215 ++++++
 testcases/kernel/fs/acl/acl_mask01.c     | 175 +++++
 testcases/kernel/fs/acl/acl_other01.c    | 104 +++
 testcases/kernel/fs/acl/acl_user_obj01.c | 127 ++++
 testcases/kernel/fs/acl/tacl_xattr.sh    | 807 -----------------------
 testcases/kernel/fs/acl/xattr_test01.c   | 380 +++++++++++
 12 files changed, 1911 insertions(+), 807 deletions(-)
 create mode 100644 testcases/kernel/fs/acl/.gitignore
 create mode 100644 testcases/kernel/fs/acl/Makefile
 create mode 100644 testcases/kernel/fs/acl/acl_file_ops01.c
 create mode 100644 testcases/kernel/fs/acl/acl_inherit01.c
 create mode 100644 testcases/kernel/fs/acl/acl_lib.h
 create mode 100644 testcases/kernel/fs/acl/acl_link01.c
 create mode 100644 testcases/kernel/fs/acl/acl_mask01.c
 create mode 100644 testcases/kernel/fs/acl/acl_other01.c
 create mode 100644 testcases/kernel/fs/acl/acl_user_obj01.c
 delete mode 100755 testcases/kernel/fs/acl/tacl_xattr.sh
 create mode 100644 testcases/kernel/fs/acl/xattr_test01.c

-- 
2.39.1


-- 
Mailing list info: https://lists.linux.it/listinfo/ltp

^ permalink raw reply	[flat|nested] 23+ messages in thread
* [LTP] [PATCH v11 1/8] fs/acl: Add ACL_USER_OBJ permission test
@ 2026-06-15 15:15 Sachin Sant
  2026-06-15 18:35 ` [LTP] " linuxtestproject.agent
  0 siblings, 1 reply; 23+ messages in thread
From: Sachin Sant @ 2026-06-15 15:15 UTC (permalink / raw)
  To: ltp

Add acl_user_obj01 test to validate ACL_USER_OBJ permissions:
- Owner permissions correctly control file/directory access
- ACL_USER_OBJ=rwx via setxattr() overrides chmod restrictions
- Owner permissions work independently of group/other permissions
- Tests use arbitrary UIDs without requiring actual user creation

The patch also adds acl_lib.h containing shared helpers for ACL
manipulation via xattr API, including:
- ACL structure management (acl_init, acl_free, acl_add_entry)
- ACL serialization/deserialization for kernel xattr format
- ACL get/set operations using getxattr/setxattr
- permission testing and file operations
- Support for both ACCESS and DEFAULT ACL types

The implementation uses direct xattr API (getxattr/setxattr) to
test kernel ACL behavior directly. Tests run on ext2/3/4,
XFS, and Btrfs filesystems with ACL support.

Suggested-by: Cyril Hrubis <chrubis@suse.cz>
Signed-off-by: Sachin Sant <sachinp@linux.ibm.com>
---
V11 changes:
- Converted SAFE_TRY_* functions to inline.
- v10 link https://lore.kernel.org/ltp/20260615072514.53026-1-sachinp@linux.ibm.com/T/#t

V10 changes:
- No change

V9 changes:
- Use RST manpage formatted documentation
- v8 link https://lore.kernel.org/ltp/20260613090543.78643-1-sachinp@linux.ibm.com/T/#t

V8 changes:
- Removed no chown variant of reset_test_path
- Use RST manpage formatted documentation
- v7 link https://lore.kernel.org/ltp/20260612171930.11964-1-sachinp@linux.ibm.com/T/#t

V7 changes:
- Use SAFE_ variants of setresgid/setresuid
- Replace TST_EXP_FAIL with TST_EXP_FAIL2
- v6 link https://lore.kernel.org/ltp/20260612102502.24071-1-sachinp@linux.ibm.com/T/#t

V6 changes:
- Added HAVE_SYS_XATTR_H guards for systems without xattr support
- Removed unnecessary error paths and manual status checking
- Uses SAFE_MALLOC() and new helper macros
- Added acl_type_to_name() helper to eliminate duplication
- v5 link https://lore.kernel.org/ltp/20260608092200.92827-1-sachinp@linux.ibm.com/T/#t

V5 changes:
- Switch to kernel only test validation to remove dependency on libacl
  and useradd/del commands.
- v4 link https://lore.kernel.org/ltp/20260604065417.25924-1-sachinp@linux.ibm.com/T/#t

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/20260603140147.50738-1-sachinp@linux.ibm.com/T/#t

V3 changes:
- Updated copyright header as per LTP format.
- v2 link https://lore.kernel.org/ltp/20260603065744.47106-1-sachinp@linux.ibm.com/T/#t

V2 changes:
- Added no chown variant of reset_test_path 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/20260602121958.27494-1-sachinp@linux.ibm.com/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/477836fd-80c8-4168-bfe6-00b374bb2534@linux.ibm.com/T/#t

---
 runtest/fs                               |   3 +
 testcases/kernel/fs/acl/.gitignore       |   1 +
 testcases/kernel/fs/acl/Makefile         |   8 +
 testcases/kernel/fs/acl/acl_lib.h        | 509 +++++++++++++++++++++++
 testcases/kernel/fs/acl/acl_user_obj01.c | 127 ++++++
 5 files changed, 648 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..2d9cba46d
--- /dev/null
+++ b/testcases/kernel/fs/acl/Makefile
@@ -0,0 +1,8 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# Copyright (c) 2026 IBM
+
+top_srcdir		?= ../../../..
+
+include $(top_srcdir)/include/mk/testcases.mk
+
+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..0e2963a0f
--- /dev/null
+++ b/testcases/kernel/fs/acl/acl_lib.h
@@ -0,0 +1,509 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (c) 2026 IBM
+ * Original shell test by Kai Zhao (ltcd3@cn.ibm.com)
+ * Converted to C by Sachin Sant <sachinp@linux.ibm.com>
+ *
+ * Common library for ACL and extended attribute tests using xattr API
+ */
+
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE
+#endif
+
+#ifndef ACL_LIB_H
+#define ACL_LIB_H
+
+#include "config.h"
+
+#include <pwd.h>
+#include <grp.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <string.h>
+#include <stdint.h>
+#include <endian.h>
+#include <sys/fsuid.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+
+#ifdef HAVE_SYS_XATTR_H
+#include <sys/xattr.h>
+#endif
+#include "tst_test.h"
+#include "tst_safe_stdio.h"
+#include "tst_capability.h"
+
+#define MNTPOINT	"mntpoint"
+#define TESTDIR		MNTPOINT "/testdir"
+#define TESTFILE	TESTDIR "/testfile"
+#define TESTSYMLINK	TESTDIR "/testsymlink"
+#define XATTR_BACKUP_FILE MNTPOINT "/xattr_backup.txt"
+
+/* 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
+
+/*
+ * POSIX ACL xattr format definitions
+ * These match the kernel's internal representation
+ */
+#define POSIX_ACL_XATTR_VERSION	0x0002
+
+/* ACL entry tag types */
+#define ACL_UNDEFINED_TAG	0x00
+#define ACL_USER_OBJ		0x01
+#define ACL_USER		0x02
+#define ACL_GROUP_OBJ		0x04
+#define ACL_GROUP		0x08
+#define ACL_MASK		0x10
+#define ACL_OTHER		0x20
+
+/* ACL permissions */
+#define ACL_READ		0x04
+#define ACL_WRITE		0x02
+#define ACL_EXECUTE		0x01
+
+/* ACL xattr names */
+#define XATTR_NAME_POSIX_ACL_ACCESS	"system.posix_acl_access"
+#define XATTR_NAME_POSIX_ACL_DEFAULT	"system.posix_acl_default"
+
+/* ACL type for set/get operations */
+#define ACL_TYPE_ACCESS		1
+#define ACL_TYPE_DEFAULT	2
+
+/* Convert host to little-endian */
+#if __BYTE_ORDER == __LITTLE_ENDIAN
+#define cpu_to_le16(x) (x)
+#define cpu_to_le32(x) (x)
+#define le16_to_cpu(x) (x)
+#define le32_to_cpu(x) (x)
+#else
+#define cpu_to_le16(x) __builtin_bswap16(x)
+#define cpu_to_le32(x) __builtin_bswap32(x)
+#define le16_to_cpu(x) __builtin_bswap16(x)
+#define le32_to_cpu(x) __builtin_bswap32(x)
+#endif
+
+/*
+ * POSIX ACL xattr format as stored in kernel
+ * This is the on-disk/in-xattr representation
+ */
+struct posix_acl_xattr_header {
+	uint32_t a_version;
+};
+
+struct posix_acl_xattr_entry {
+	uint16_t e_tag;
+	uint16_t e_perm;
+	uint32_t e_id;
+};
+
+/*
+ * Combined xattr structure for ACL operations
+ */
+struct acl_xattr {
+	struct posix_acl_xattr_header header;
+	struct posix_acl_xattr_entry entries[];
+};
+
+/*
+ * In-memory ACL representation for building ACLs
+ */
+#define MAX_ACL_ENTRIES 32
+
+struct acl_entry {
+	uint16_t tag;
+	uint16_t perm;
+	uint32_t id;
+};
+
+struct acl {
+	int count;
+	struct acl_entry entries[MAX_ACL_ENTRIES];
+};
+
+/*
+ * try_unlink - unlink a file, ignoring ENOENT
+ * Won't TBROK if file doesn't exist
+ */
+static inline void try_unlink(const char *path)
+{
+	if (unlink(path) == -1 && errno != ENOENT)
+		tst_brk(TBROK | TERRNO, "unlink(%s) failed", path);
+}
+
+/*
+ * try_rmdir - remove a directory, ignoring ENOENT
+ * Won't TBROK if directory doesn't exist
+ */
+static inline void try_rmdir(const char *path)
+{
+	if (rmdir(path) == -1 && errno != ENOENT)
+		tst_brk(TBROK | TERRNO, "rmdir(%s) failed", path);
+}
+
+static inline void reset_test_path(void)
+{
+	try_unlink(TESTSYMLINK);
+	try_unlink(TESTFILE);
+	try_rmdir(TESTDIR);
+
+	SAFE_MKDIR(TESTDIR, 0755);
+}
+
+static inline void cleanup_testfile(void)
+{
+	try_unlink(TESTFILE);
+}
+
+#ifdef HAVE_SYS_XATTR_H
+
+/*
+ * Initialize an empty ACL structure
+ */
+static inline struct acl *acl_init(void)
+{
+	struct acl *acl = SAFE_MALLOC(sizeof(struct acl));
+
+	acl->count = 0;
+	return acl;
+}
+
+/*
+ * Free an ACL structure
+ */
+static inline void acl_free(struct acl *acl)
+{
+	free(acl);
+}
+
+/*
+ * Add an ACL entry to the ACL structure
+ */
+static inline int acl_add_entry(struct acl *acl, uint16_t tag, uint16_t perm,
+				uint32_t id)
+{
+	if (acl->count >= MAX_ACL_ENTRIES) {
+		errno = ENOMEM;
+		return -1;
+	}
+
+	acl->entries[acl->count].tag = tag;
+	acl->entries[acl->count].perm = perm;
+	acl->entries[acl->count].id = id;
+	acl->count++;
+	return 0;
+}
+
+/*
+ * Convert ACL type to xattr name
+ */
+static inline const char *acl_type_to_name(int type)
+{
+	if (type == ACL_TYPE_ACCESS)
+		return XATTR_NAME_POSIX_ACL_ACCESS;
+	else if (type == ACL_TYPE_DEFAULT)
+		return XATTR_NAME_POSIX_ACL_DEFAULT;
+	else
+		return NULL;
+}
+
+/*
+ * Set ACL on a file using xattr.
+ *
+ * The kernel stores access ACLs only when they differ from the file mode.
+ * If the ACL is equivalent to st_mode, the xattr is removed and future
+ * getxattr() calls return ENODATA. Mirror libacl semantics by treating
+ * ENODATA as a valid minimal ACL derived from st_mode.
+ */
+static inline int acl_set_file(const char *path, int type, struct acl *acl)
+{
+	const char *xattr_name;
+	size_t size;
+	char *buf;
+	struct posix_acl_xattr_header *header;
+	struct posix_acl_xattr_entry *entries;
+	int i, ret;
+
+	xattr_name = acl_type_to_name(type);
+	if (!xattr_name) {
+		errno = EINVAL;
+		return -1;
+	}
+
+	size = sizeof(struct posix_acl_xattr_header) +
+	       acl->count * sizeof(struct posix_acl_xattr_entry);
+
+	buf = malloc(size);
+	if (!buf)
+		return -1;
+
+	header = (struct posix_acl_xattr_header *)buf;
+	header->a_version = cpu_to_le32(POSIX_ACL_XATTR_VERSION);
+
+	entries = (struct posix_acl_xattr_entry *)(buf + sizeof(*header));
+
+	for (i = 0; i < acl->count; i++) {
+		entries[i].e_tag = cpu_to_le16(acl->entries[i].tag);
+		entries[i].e_perm = cpu_to_le16(acl->entries[i].perm);
+		entries[i].e_id = cpu_to_le32(acl->entries[i].id);
+	}
+
+	ret = setxattr(path, xattr_name, buf, size, 0);
+	free(buf);
+
+	return ret;
+}
+
+static inline void acl_add_mode_entries(struct acl *acl, mode_t mode)
+{
+	acl_add_entry(acl, ACL_USER_OBJ, (mode >> 6) & 07, 0);
+	acl_add_entry(acl, ACL_GROUP_OBJ, (mode >> 3) & 07, 0);
+	acl_add_entry(acl, ACL_OTHER, mode & 07, 0);
+}
+
+/*
+ * Synthesize an ACL from file mode bits.
+ * Used when no xattr exists for an access ACL.
+ */
+static inline struct acl *acl_from_mode(const char *path)
+{
+	struct acl *acl;
+	struct stat st;
+
+	if (stat(path, &st) < 0)
+		return NULL;
+
+	acl = acl_init();
+	acl_add_mode_entries(acl, st.st_mode);
+
+	return acl;
+}
+
+/*
+ * Get ACL from a file using xattr.
+ *
+ * Access ACLs equivalent to file mode may not have a backing xattr at all.
+ * In that case synthesize the base ACL from st_mode so callers observe the
+ * same behavior as acl_get_file(3).
+ */
+static inline struct acl *acl_get_file(const char *path, int type)
+{
+	const char *xattr_name;
+	ssize_t size;
+	struct acl_xattr *ax;
+	struct acl *acl;
+	int i, count;
+
+	xattr_name = acl_type_to_name(type);
+	if (!xattr_name) {
+		errno = EINVAL;
+		return NULL;
+	}
+
+	size = getxattr(path, xattr_name, NULL, 0);
+	if (size < 0) {
+		if (errno != ENODATA || type != ACL_TYPE_ACCESS)
+			return NULL;
+
+		return acl_from_mode(path);
+	}
+
+	/* Handle race: xattr removed between size check and actual read */
+	if (size == 0)
+		return acl_from_mode(path);
+
+	ax = malloc(size);
+	if (!ax)
+		return NULL;
+
+	size = getxattr(path, xattr_name, ax, size);
+	if (size < 0) {
+		free(ax);
+		/* Handle race: xattr removed between size check and read */
+		if (errno == ENODATA && type == ACL_TYPE_ACCESS)
+			return acl_from_mode(path);
+		return NULL;
+	}
+
+	if (le32_to_cpu(ax->header.a_version) != POSIX_ACL_XATTR_VERSION) {
+		free(ax);
+		errno = EINVAL;
+		return NULL;
+	}
+
+	count = (size - sizeof(ax->header)) /
+		sizeof(struct posix_acl_xattr_entry);
+
+	acl = acl_init();
+	if (!acl) {
+		free(ax);
+		return NULL;
+	}
+
+	for (i = 0; i < count; i++) {
+		uint16_t tag = le16_to_cpu(ax->entries[i].e_tag);
+		uint16_t perm = le16_to_cpu(ax->entries[i].e_perm);
+		uint32_t id = le32_to_cpu(ax->entries[i].e_id);
+
+		if (acl_add_entry(acl, tag, perm, id) < 0) {
+			acl_free(acl);
+			free(ax);
+			return NULL;
+		}
+	}
+
+	free(ax);
+	return acl;
+}
+
+/*
+ * Check if an ACL entry has a specific permission
+ */
+static inline int acl_entry_has_perm(struct acl_entry *entry, uint16_t perm)
+{
+	return (entry->perm & perm) == perm;
+}
+
+/*
+ * Check if an ACL entry has all rwx permissions
+ */
+static inline int acl_entry_has_rwx(struct acl_entry *entry)
+{
+	return acl_entry_has_perm(entry,
+				  ACL_READ | ACL_WRITE | ACL_EXECUTE);
+}
+
+/*
+ * Find an ACL entry by tag type
+ */
+static inline struct acl_entry *acl_find_entry(struct acl *acl, uint16_t tag,
+					       uint32_t id)
+{
+	int i;
+
+	for (i = 0; i < acl->count; i++) {
+		if (acl->entries[i].tag == tag) {
+			if (tag == ACL_USER || tag == ACL_GROUP) {
+				if (acl->entries[i].id == id)
+					return &acl->entries[i];
+			} else {
+				return &acl->entries[i];
+			}
+		}
+	}
+
+	return NULL;
+}
+
+/*
+ * Update ACL mask permissions
+ */
+static inline int acl_set_mask_perms(struct acl *acl, uint16_t perm)
+{
+	struct acl_entry *mask_entry = acl_find_entry(acl, ACL_MASK, 0);
+
+	if (!mask_entry) {
+		errno = ENOENT;
+		return -1;
+	}
+
+	mask_entry->perm = perm;
+	return 0;
+}
+
+static inline void create_file_as(uid_t uid, gid_t gid, mode_t mode,
+				   int use_umask, mode_t mask, int exp_errno)
+{
+	pid_t pid;
+
+	pid = SAFE_FORK();
+	if (!pid) {
+		uid_t fsuid;
+		gid_t fsgid;
+		struct tst_cap_user_header hdr = {
+			.version = 0x20080522,
+			.pid = 0,
+		};
+		struct tst_cap_user_data data[2] = {};
+
+		SAFE_SETGROUPS(0, NULL);
+		SAFE_SETRESGID(gid, gid, gid);
+		SAFE_SETRESUID(uid, uid, uid);
+
+		/* Drop all capabilities to ensure DAC checks are enforced */
+		if (tst_capset(&hdr, data) < 0)
+			tst_brk(TBROK | TERRNO, "capset failed");
+
+		/*
+		 * setfsuid()/setfsgid() return the previous value, not -1 on
+		 * failure. Verify the effective filesystem credentials by
+		 * reading them back.
+		 */
+		setfsuid(uid);
+		fsuid = setfsuid((uid_t)-1);
+		if (fsuid != uid)
+			tst_brk(TBROK,
+				"setfsuid verification failed, expected %u got %u",
+				(unsigned int)uid, (unsigned int)fsuid);
+
+		setfsgid(gid);
+		fsgid = setfsgid((gid_t)-1);
+		if (fsgid != gid)
+			tst_brk(TBROK,
+				"setfsgid verification failed, expected %u got %u",
+				(unsigned int)gid, (unsigned int)fsgid);
+
+		if (use_umask)
+			umask(mask);
+
+		if (exp_errno) {
+			TST_EXP_FAIL2(open(TESTFILE, O_CREAT | O_WRONLY, mode),
+				      exp_errno, "open(%s)", TESTFILE);
+		} else {
+			TST_EXP_FD(open(TESTFILE, O_CREAT | O_WRONLY, mode),
+				   "open(%s)", TESTFILE);
+			if (TST_RET >= 0)
+				SAFE_CLOSE(TST_RET);
+		}
+
+		exit(0);
+	}
+
+	tst_reap_children();
+}
+
+static inline void try_create_as(uid_t uid, gid_t gid, mode_t mode,
+				  int exp_errno)
+{
+	create_file_as(uid, gid, mode, 0, 0, exp_errno);
+}
+
+static inline void create_with_umask_as(uid_t uid, gid_t gid, mode_t mode,
+					 mode_t mask, int exp_errno)
+{
+	create_file_as(uid, gid, mode, 1, mask, exp_errno);
+}
+
+static inline void cleanup_test_paths(void)
+{
+	try_unlink(TESTSYMLINK);
+	try_unlink(TESTFILE);
+	try_rmdir(TESTDIR);
+}
+
+#endif /* HAVE_SYS_XATTR_H */
+
+#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..89291d275
--- /dev/null
+++ b/testcases/kernel/fs/acl/acl_user_obj01.c
@@ -0,0 +1,127 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (c) 2026 IBM
+ *
+ * Original shell test by Kai Zhao (ltcd3@cn.ibm.com)
+ * Converted to C by Sachin Sant <sachinp@linux.ibm.com>
+ */
+
+/*\
+ * Test ACL_USER_OBJ permissions using direct xattr manipulation.
+ *
+ * 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 :manpage:`setxattr(2)` overrides
+ *   a previous :manpage:`chmod(2)` restriction
+ * - Owner permissions work independently of group and other permissions
+ *
+ * This test uses arbitrary UIDs without creating actual users, testing
+ * only the kernel ACL implementation.
+ */
+
+#include "acl_lib.h"
+
+#ifdef HAVE_SYS_XATTR_H
+
+#define TEST_UID 1000
+#define TEST_GID 1000
+
+/*
+ * Test permission bits deny access.
+ * Owner should be denied file creation when directory mode is 0555.
+ */
+static void test_deny_by_mode(void)
+{
+	tst_res(TINFO, "Testing permission bits deny access");
+	reset_test_path();
+
+	SAFE_CHOWN(TESTDIR, TEST_UID, TEST_GID);
+	SAFE_CHMOD(TESTDIR, 0555);
+
+	try_create_as(TEST_UID, TEST_GID, 0644, EACCES);
+}
+
+/*
+ * Test ACL_USER_OBJ grants access.
+ * Setting ACL_USER_OBJ=rwx should restore owner write permission and
+ * allow file creation after the restrictive mode baseline.
+ */
+static void test_grant_by_acl(void)
+{
+	struct acl *acl;
+
+	tst_res(TINFO, "Testing ACL_USER_OBJ grants access");
+	reset_test_path();
+
+	SAFE_CHOWN(TESTDIR, TEST_UID, TEST_GID);
+	SAFE_CHMOD(TESTDIR, 0555);
+
+	acl = acl_init();
+
+	acl_add_entry(acl, ACL_USER_OBJ,
+		      ACL_READ | ACL_WRITE | ACL_EXECUTE, 0);
+	acl_add_entry(acl, ACL_GROUP_OBJ, 0, 0);
+	acl_add_entry(acl, ACL_OTHER, 0, 0);
+
+	if (acl_set_file(TESTDIR, ACL_TYPE_ACCESS, acl) < 0) {
+		if (errno == EOPNOTSUPP) {
+			acl_free(acl);
+			tst_brk(TCONF | TERRNO, "ACL not supported");
+		}
+		acl_free(acl);
+		tst_brk(TBROK | TERRNO, "ACL setup failed");
+	}
+
+	acl_free(acl);
+
+	try_create_as(TEST_UID, TEST_GID, 0644, 0);
+
+	cleanup_testfile();
+}
+
+static void run(unsigned int n)
+{
+	switch (n) {
+	case 0:
+		test_deny_by_mode();
+		break;
+	case 1:
+		test_grant_by_acl();
+		break;
+	}
+}
+
+static void setup(void)
+{
+	reset_test_path();
+}
+
+static void cleanup(void)
+{
+	cleanup_test_paths();
+}
+
+static struct tst_test test = {
+	.test = run,
+	.tcnt = 2,
+	.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"},
+		{}
+	}
+};
+
+#else
+	TST_TEST_TCONF("sys/xattr.h is not available");
+#endif
-- 
2.39.1


-- 
Mailing list info: https://lists.linux.it/listinfo/ltp

^ permalink raw reply related	[flat|nested] 23+ messages in thread
* [LTP] [PATCH v10 1/8] fs/acl: Add ACL_USER_OBJ permission test
@ 2026-06-15  7:25 Sachin Sant
  2026-06-15  8:33 ` [LTP] " linuxtestproject.agent
  0 siblings, 1 reply; 23+ messages in thread
From: Sachin Sant @ 2026-06-15  7:25 UTC (permalink / raw)
  To: ltp

Add acl_user_obj01 test to validate ACL_USER_OBJ permissions:
- Owner permissions correctly control file/directory access
- ACL_USER_OBJ=rwx via setxattr() overrides chmod restrictions
- Owner permissions work independently of group/other permissions
- Tests use arbitrary UIDs without requiring actual user creation

The patch also adds acl_lib.h containing shared helpers for ACL
manipulation via xattr API, including:
- ACL structure management (acl_init, acl_free, acl_add_entry)
- ACL serialization/deserialization for kernel xattr format
- ACL get/set operations using getxattr/setxattr
- permission testing and file operations
- Support for both ACCESS and DEFAULT ACL types

The implementation uses direct xattr API (getxattr/setxattr) to
test kernel ACL behavior directly. Tests run on ext2/3/4,
XFS, and Btrfs filesystems with ACL support.

Suggested-by: Cyril Hrubis <chrubis@suse.cz>
Signed-off-by: Sachin Sant <sachinp@linux.ibm.com>
---
V10 changes:
- No change

V9 changes:
- Use RST manpage formatted documentation
- v8 link https://lore.kernel.org/ltp/20260613090543.78643-1-sachinp@linux.ibm.com/T/#t

V8 changes:
- Removed no chown variant of reset_test_path
- Use RST manpage formatted documentation
- v7 link https://lore.kernel.org/ltp/20260612171930.11964-1-sachinp@linux.ibm.com/T/#t

V7 changes:
- Use SAFE_ variants of setresgid/setresuid
- Replace TST_EXP_FAIL with TST_EXP_FAIL2
- v6 link https://lore.kernel.org/ltp/20260612102502.24071-1-sachinp@linux.ibm.com/T/#t

V6 changes:
- Added HAVE_SYS_XATTR_H guards for systems without xattr support
- Removed unnecessary error paths and manual status checking
- Uses SAFE_MALLOC() and new helper macros
- Added acl_type_to_name() helper to eliminate duplication
- v5 link https://lore.kernel.org/ltp/20260608092200.92827-1-sachinp@linux.ibm.com/T/#t

V5 changes:
- Switch to kernel only test validation to remove dependency on libacl
  and useradd/del commands.
- v4 link https://lore.kernel.org/ltp/20260604065417.25924-1-sachinp@linux.ibm.com/T/#t

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/20260603140147.50738-1-sachinp@linux.ibm.com/T/#t

V3 changes:
- Updated copyright header as per LTP format.
- v2 link https://lore.kernel.org/ltp/20260603065744.47106-1-sachinp@linux.ibm.com/T/#t

V2 changes:
- Added no chown variant of reset_test_path 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/20260602121958.27494-1-sachinp@linux.ibm.com/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/477836fd-80c8-4168-bfe6-00b374bb2534@linux.ibm.com/T/#t

---
 runtest/fs                               |   3 +
 testcases/kernel/fs/acl/.gitignore       |   1 +
 testcases/kernel/fs/acl/Makefile         |   8 +
 testcases/kernel/fs/acl/acl_lib.h        | 507 +++++++++++++++++++++++
 testcases/kernel/fs/acl/acl_user_obj01.c | 127 ++++++
 5 files changed, 646 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..2d9cba46d
--- /dev/null
+++ b/testcases/kernel/fs/acl/Makefile
@@ -0,0 +1,8 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# Copyright (c) 2026 IBM
+
+top_srcdir		?= ../../../..
+
+include $(top_srcdir)/include/mk/testcases.mk
+
+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..7fe68aa59
--- /dev/null
+++ b/testcases/kernel/fs/acl/acl_lib.h
@@ -0,0 +1,507 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (c) 2026 IBM
+ * Original shell test by Kai Zhao (ltcd3@cn.ibm.com)
+ * Converted to C by Sachin Sant <sachinp@linux.ibm.com>
+ *
+ * Common library for ACL and extended attribute tests using xattr API
+ */
+
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE
+#endif
+
+#ifndef ACL_LIB_H
+#define ACL_LIB_H
+
+#include "config.h"
+
+#include <pwd.h>
+#include <grp.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <string.h>
+#include <stdint.h>
+#include <endian.h>
+#include <sys/fsuid.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+
+#ifdef HAVE_SYS_XATTR_H
+#include <sys/xattr.h>
+#endif
+#include "tst_test.h"
+#include "tst_safe_stdio.h"
+#include "tst_capability.h"
+
+#define MNTPOINT	"mntpoint"
+#define TESTDIR		MNTPOINT "/testdir"
+#define TESTFILE	TESTDIR "/testfile"
+#define TESTSYMLINK	TESTDIR "/testsymlink"
+#define XATTR_BACKUP_FILE MNTPOINT "/xattr_backup.txt"
+
+/* 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
+
+/*
+ * POSIX ACL xattr format definitions
+ * These match the kernel's internal representation
+ */
+#define POSIX_ACL_XATTR_VERSION	0x0002
+
+/* ACL entry tag types */
+#define ACL_UNDEFINED_TAG	0x00
+#define ACL_USER_OBJ		0x01
+#define ACL_USER		0x02
+#define ACL_GROUP_OBJ		0x04
+#define ACL_GROUP		0x08
+#define ACL_MASK		0x10
+#define ACL_OTHER		0x20
+
+/* ACL permissions */
+#define ACL_READ		0x04
+#define ACL_WRITE		0x02
+#define ACL_EXECUTE		0x01
+
+/* ACL xattr names */
+#define XATTR_NAME_POSIX_ACL_ACCESS	"system.posix_acl_access"
+#define XATTR_NAME_POSIX_ACL_DEFAULT	"system.posix_acl_default"
+
+/* ACL type for set/get operations */
+#define ACL_TYPE_ACCESS		1
+#define ACL_TYPE_DEFAULT	2
+
+/* Convert host to little-endian */
+#if __BYTE_ORDER == __LITTLE_ENDIAN
+#define cpu_to_le16(x) (x)
+#define cpu_to_le32(x) (x)
+#define le16_to_cpu(x) (x)
+#define le32_to_cpu(x) (x)
+#else
+#define cpu_to_le16(x) __builtin_bswap16(x)
+#define cpu_to_le32(x) __builtin_bswap32(x)
+#define le16_to_cpu(x) __builtin_bswap16(x)
+#define le32_to_cpu(x) __builtin_bswap32(x)
+#endif
+
+/*
+ * POSIX ACL xattr format as stored in kernel
+ * This is the on-disk/in-xattr representation
+ */
+struct posix_acl_xattr_header {
+	uint32_t a_version;
+};
+
+struct posix_acl_xattr_entry {
+	uint16_t e_tag;
+	uint16_t e_perm;
+	uint32_t e_id;
+};
+
+/*
+ * Combined xattr structure for ACL operations
+ */
+struct acl_xattr {
+	struct posix_acl_xattr_header header;
+	struct posix_acl_xattr_entry entries[];
+};
+
+/*
+ * In-memory ACL representation for building ACLs
+ */
+#define MAX_ACL_ENTRIES 32
+
+struct acl_entry {
+	uint16_t tag;
+	uint16_t perm;
+	uint32_t id;
+};
+
+struct acl {
+	int count;
+	struct acl_entry entries[MAX_ACL_ENTRIES];
+};
+
+/*
+ * SAFE_TRY_UNLINK - unlink a file, ignoring ENOENT
+ * Won't TBROK if file doesn't exist
+ */
+#define SAFE_TRY_UNLINK(path) do { \
+	if (unlink(path) == -1 && errno != ENOENT) \
+		tst_brk(TBROK | TERRNO, "unlink(%s) failed", path); \
+} while (0)
+
+/*
+ * SAFE_TRY_RMDIR - remove a directory, ignoring ENOENT
+ * Won't TBROK if directory doesn't exist
+ */
+#define SAFE_TRY_RMDIR(path) do { \
+	if (rmdir(path) == -1 && errno != ENOENT) \
+		tst_brk(TBROK | TERRNO, "rmdir(%s) failed", path); \
+} while (0)
+
+static inline void reset_test_path(void)
+{
+	SAFE_TRY_UNLINK(TESTSYMLINK);
+	SAFE_TRY_UNLINK(TESTFILE);
+	SAFE_TRY_RMDIR(TESTDIR);
+
+	SAFE_MKDIR(TESTDIR, 0755);
+}
+
+static inline void cleanup_testfile(void)
+{
+	SAFE_TRY_UNLINK(TESTFILE);
+}
+
+#ifdef HAVE_SYS_XATTR_H
+
+/*
+ * Initialize an empty ACL structure
+ */
+static inline struct acl *acl_init(void)
+{
+	struct acl *acl = SAFE_MALLOC(sizeof(struct acl));
+
+	acl->count = 0;
+	return acl;
+}
+
+/*
+ * Free an ACL structure
+ */
+static inline void acl_free(struct acl *acl)
+{
+	free(acl);
+}
+
+/*
+ * Add an ACL entry to the ACL structure
+ */
+static inline int acl_add_entry(struct acl *acl, uint16_t tag, uint16_t perm,
+				uint32_t id)
+{
+	if (acl->count >= MAX_ACL_ENTRIES) {
+		errno = ENOMEM;
+		return -1;
+	}
+
+	acl->entries[acl->count].tag = tag;
+	acl->entries[acl->count].perm = perm;
+	acl->entries[acl->count].id = id;
+	acl->count++;
+	return 0;
+}
+
+/*
+ * Convert ACL type to xattr name
+ */
+static inline const char *acl_type_to_name(int type)
+{
+	if (type == ACL_TYPE_ACCESS)
+		return XATTR_NAME_POSIX_ACL_ACCESS;
+	else if (type == ACL_TYPE_DEFAULT)
+		return XATTR_NAME_POSIX_ACL_DEFAULT;
+	else
+		return NULL;
+}
+
+/*
+ * Set ACL on a file using xattr.
+ *
+ * The kernel stores access ACLs only when they differ from the file mode.
+ * If the ACL is equivalent to st_mode, the xattr is removed and future
+ * getxattr() calls return ENODATA. Mirror libacl semantics by treating
+ * ENODATA as a valid minimal ACL derived from st_mode.
+ */
+static inline int acl_set_file(const char *path, int type, struct acl *acl)
+{
+	const char *xattr_name;
+	size_t size;
+	char *buf;
+	struct posix_acl_xattr_header *header;
+	struct posix_acl_xattr_entry *entries;
+	int i, ret;
+
+	xattr_name = acl_type_to_name(type);
+	if (!xattr_name) {
+		errno = EINVAL;
+		return -1;
+	}
+
+	size = sizeof(struct posix_acl_xattr_header) +
+	       acl->count * sizeof(struct posix_acl_xattr_entry);
+
+	buf = malloc(size);
+	if (!buf)
+		return -1;
+
+	header = (struct posix_acl_xattr_header *)buf;
+	header->a_version = cpu_to_le32(POSIX_ACL_XATTR_VERSION);
+
+	entries = (struct posix_acl_xattr_entry *)(buf + sizeof(*header));
+
+	for (i = 0; i < acl->count; i++) {
+		entries[i].e_tag = cpu_to_le16(acl->entries[i].tag);
+		entries[i].e_perm = cpu_to_le16(acl->entries[i].perm);
+		entries[i].e_id = cpu_to_le32(acl->entries[i].id);
+	}
+
+	ret = setxattr(path, xattr_name, buf, size, 0);
+	free(buf);
+
+	return ret;
+}
+
+static inline void acl_add_mode_entries(struct acl *acl, mode_t mode)
+{
+	acl_add_entry(acl, ACL_USER_OBJ, (mode >> 6) & 07, 0);
+	acl_add_entry(acl, ACL_GROUP_OBJ, (mode >> 3) & 07, 0);
+	acl_add_entry(acl, ACL_OTHER, mode & 07, 0);
+}
+
+/*
+ * Synthesize an ACL from file mode bits.
+ * Used when no xattr exists for an access ACL.
+ */
+static inline struct acl *acl_from_mode(const char *path)
+{
+	struct acl *acl;
+	struct stat st;
+
+	if (stat(path, &st) < 0)
+		return NULL;
+
+	acl = acl_init();
+	acl_add_mode_entries(acl, st.st_mode);
+
+	return acl;
+}
+
+/*
+ * Get ACL from a file using xattr.
+ *
+ * Access ACLs equivalent to file mode may not have a backing xattr at all.
+ * In that case synthesize the base ACL from st_mode so callers observe the
+ * same behavior as acl_get_file(3).
+ */
+static inline struct acl *acl_get_file(const char *path, int type)
+{
+	const char *xattr_name;
+	ssize_t size;
+	struct acl_xattr *ax;
+	struct acl *acl;
+	int i, count;
+
+	xattr_name = acl_type_to_name(type);
+	if (!xattr_name) {
+		errno = EINVAL;
+		return NULL;
+	}
+
+	size = getxattr(path, xattr_name, NULL, 0);
+	if (size < 0) {
+		if (errno != ENODATA || type != ACL_TYPE_ACCESS)
+			return NULL;
+
+		return acl_from_mode(path);
+	}
+
+	/* Handle race: xattr removed between size check and actual read */
+	if (size == 0)
+		return acl_from_mode(path);
+
+	ax = malloc(size);
+	if (!ax)
+		return NULL;
+
+	size = getxattr(path, xattr_name, ax, size);
+	if (size < 0) {
+		free(ax);
+		/* Handle race: xattr removed between size check and read */
+		if (errno == ENODATA && type == ACL_TYPE_ACCESS)
+			return acl_from_mode(path);
+		return NULL;
+	}
+
+	if (le32_to_cpu(ax->header.a_version) != POSIX_ACL_XATTR_VERSION) {
+		free(ax);
+		errno = EINVAL;
+		return NULL;
+	}
+
+	count = (size - sizeof(ax->header)) /
+		sizeof(struct posix_acl_xattr_entry);
+
+	acl = acl_init();
+	if (!acl) {
+		free(ax);
+		return NULL;
+	}
+
+	for (i = 0; i < count; i++) {
+		uint16_t tag = le16_to_cpu(ax->entries[i].e_tag);
+		uint16_t perm = le16_to_cpu(ax->entries[i].e_perm);
+		uint32_t id = le32_to_cpu(ax->entries[i].e_id);
+
+		if (acl_add_entry(acl, tag, perm, id) < 0) {
+			acl_free(acl);
+			free(ax);
+			return NULL;
+		}
+	}
+
+	free(ax);
+	return acl;
+}
+
+/*
+ * Check if an ACL entry has a specific permission
+ */
+static inline int acl_entry_has_perm(struct acl_entry *entry, uint16_t perm)
+{
+	return (entry->perm & perm) == perm;
+}
+
+/*
+ * Check if an ACL entry has all rwx permissions
+ */
+static inline int acl_entry_has_rwx(struct acl_entry *entry)
+{
+	return acl_entry_has_perm(entry,
+				  ACL_READ | ACL_WRITE | ACL_EXECUTE);
+}
+
+/*
+ * Find an ACL entry by tag type
+ */
+static inline struct acl_entry *acl_find_entry(struct acl *acl, uint16_t tag,
+					       uint32_t id)
+{
+	int i;
+
+	for (i = 0; i < acl->count; i++) {
+		if (acl->entries[i].tag == tag) {
+			if (tag == ACL_USER || tag == ACL_GROUP) {
+				if (acl->entries[i].id == id)
+					return &acl->entries[i];
+			} else {
+				return &acl->entries[i];
+			}
+		}
+	}
+
+	return NULL;
+}
+
+/*
+ * Update ACL mask permissions
+ */
+static inline int acl_set_mask_perms(struct acl *acl, uint16_t perm)
+{
+	struct acl_entry *mask_entry = acl_find_entry(acl, ACL_MASK, 0);
+
+	if (!mask_entry) {
+		errno = ENOENT;
+		return -1;
+	}
+
+	mask_entry->perm = perm;
+	return 0;
+}
+
+static inline void create_file_as(uid_t uid, gid_t gid, mode_t mode,
+				   int use_umask, mode_t mask, int exp_errno)
+{
+	pid_t pid;
+
+	pid = SAFE_FORK();
+	if (!pid) {
+		uid_t fsuid;
+		gid_t fsgid;
+		struct tst_cap_user_header hdr = {
+			.version = 0x20080522,
+			.pid = 0,
+		};
+		struct tst_cap_user_data data[2] = {};
+
+		SAFE_SETGROUPS(0, NULL);
+		SAFE_SETRESGID(gid, gid, gid);
+		SAFE_SETRESUID(uid, uid, uid);
+
+		/* Drop all capabilities to ensure DAC checks are enforced */
+		if (tst_capset(&hdr, data) < 0)
+			tst_brk(TBROK | TERRNO, "capset failed");
+
+		/*
+		 * setfsuid()/setfsgid() return the previous value, not -1 on
+		 * failure. Verify the effective filesystem credentials by
+		 * reading them back.
+		 */
+		setfsuid(uid);
+		fsuid = setfsuid((uid_t)-1);
+		if (fsuid != uid)
+			tst_brk(TBROK,
+				"setfsuid verification failed, expected %u got %u",
+				(unsigned int)uid, (unsigned int)fsuid);
+
+		setfsgid(gid);
+		fsgid = setfsgid((gid_t)-1);
+		if (fsgid != gid)
+			tst_brk(TBROK,
+				"setfsgid verification failed, expected %u got %u",
+				(unsigned int)gid, (unsigned int)fsgid);
+
+		if (use_umask)
+			umask(mask);
+
+		if (exp_errno) {
+			TST_EXP_FAIL2(open(TESTFILE, O_CREAT | O_WRONLY, mode),
+				      exp_errno, "open(%s)", TESTFILE);
+		} else {
+			TST_EXP_FD(open(TESTFILE, O_CREAT | O_WRONLY, mode),
+				   "open(%s)", TESTFILE);
+			if (TST_RET >= 0)
+				SAFE_CLOSE(TST_RET);
+		}
+
+		exit(0);
+	}
+
+	tst_reap_children();
+}
+
+static inline void try_create_as(uid_t uid, gid_t gid, mode_t mode,
+				  int exp_errno)
+{
+	create_file_as(uid, gid, mode, 0, 0, exp_errno);
+}
+
+static inline void create_with_umask_as(uid_t uid, gid_t gid, mode_t mode,
+					 mode_t mask, int exp_errno)
+{
+	create_file_as(uid, gid, mode, 1, mask, exp_errno);
+}
+
+static inline void cleanup_test_paths(void)
+{
+	SAFE_TRY_UNLINK(TESTSYMLINK);
+	SAFE_TRY_UNLINK(TESTFILE);
+	SAFE_TRY_RMDIR(TESTDIR);
+}
+
+#endif /* HAVE_SYS_XATTR_H */
+
+#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..89291d275
--- /dev/null
+++ b/testcases/kernel/fs/acl/acl_user_obj01.c
@@ -0,0 +1,127 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (c) 2026 IBM
+ *
+ * Original shell test by Kai Zhao (ltcd3@cn.ibm.com)
+ * Converted to C by Sachin Sant <sachinp@linux.ibm.com>
+ */
+
+/*\
+ * Test ACL_USER_OBJ permissions using direct xattr manipulation.
+ *
+ * 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 :manpage:`setxattr(2)` overrides
+ *   a previous :manpage:`chmod(2)` restriction
+ * - Owner permissions work independently of group and other permissions
+ *
+ * This test uses arbitrary UIDs without creating actual users, testing
+ * only the kernel ACL implementation.
+ */
+
+#include "acl_lib.h"
+
+#ifdef HAVE_SYS_XATTR_H
+
+#define TEST_UID 1000
+#define TEST_GID 1000
+
+/*
+ * Test permission bits deny access.
+ * Owner should be denied file creation when directory mode is 0555.
+ */
+static void test_deny_by_mode(void)
+{
+	tst_res(TINFO, "Testing permission bits deny access");
+	reset_test_path();
+
+	SAFE_CHOWN(TESTDIR, TEST_UID, TEST_GID);
+	SAFE_CHMOD(TESTDIR, 0555);
+
+	try_create_as(TEST_UID, TEST_GID, 0644, EACCES);
+}
+
+/*
+ * Test ACL_USER_OBJ grants access.
+ * Setting ACL_USER_OBJ=rwx should restore owner write permission and
+ * allow file creation after the restrictive mode baseline.
+ */
+static void test_grant_by_acl(void)
+{
+	struct acl *acl;
+
+	tst_res(TINFO, "Testing ACL_USER_OBJ grants access");
+	reset_test_path();
+
+	SAFE_CHOWN(TESTDIR, TEST_UID, TEST_GID);
+	SAFE_CHMOD(TESTDIR, 0555);
+
+	acl = acl_init();
+
+	acl_add_entry(acl, ACL_USER_OBJ,
+		      ACL_READ | ACL_WRITE | ACL_EXECUTE, 0);
+	acl_add_entry(acl, ACL_GROUP_OBJ, 0, 0);
+	acl_add_entry(acl, ACL_OTHER, 0, 0);
+
+	if (acl_set_file(TESTDIR, ACL_TYPE_ACCESS, acl) < 0) {
+		if (errno == EOPNOTSUPP) {
+			acl_free(acl);
+			tst_brk(TCONF | TERRNO, "ACL not supported");
+		}
+		acl_free(acl);
+		tst_brk(TBROK | TERRNO, "ACL setup failed");
+	}
+
+	acl_free(acl);
+
+	try_create_as(TEST_UID, TEST_GID, 0644, 0);
+
+	cleanup_testfile();
+}
+
+static void run(unsigned int n)
+{
+	switch (n) {
+	case 0:
+		test_deny_by_mode();
+		break;
+	case 1:
+		test_grant_by_acl();
+		break;
+	}
+}
+
+static void setup(void)
+{
+	reset_test_path();
+}
+
+static void cleanup(void)
+{
+	cleanup_test_paths();
+}
+
+static struct tst_test test = {
+	.test = run,
+	.tcnt = 2,
+	.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"},
+		{}
+	}
+};
+
+#else
+	TST_TEST_TCONF("sys/xattr.h is not available");
+#endif
-- 
2.39.1


-- 
Mailing list info: https://lists.linux.it/listinfo/ltp

^ permalink raw reply related	[flat|nested] 23+ messages in thread
* [LTP] [PATCH v9 1/8] fs/acl: Add ACL_USER_OBJ permission test
@ 2026-06-15  5:29 Sachin Sant
  2026-06-15  6:32 ` [LTP] " linuxtestproject.agent
  0 siblings, 1 reply; 23+ messages in thread
From: Sachin Sant @ 2026-06-15  5:29 UTC (permalink / raw)
  To: ltp

Add acl_user_obj01 test to validate ACL_USER_OBJ permissions:
- Owner permissions correctly control file/directory access
- ACL_USER_OBJ=rwx via setxattr() overrides chmod restrictions
- Owner permissions work independently of group/other permissions
- Tests use arbitrary UIDs without requiring actual user creation

The patch also adds acl_lib.h containing shared helpers for ACL
manipulation via xattr API, including:
- ACL structure management (acl_init, acl_free, acl_add_entry)
- ACL serialization/deserialization for kernel xattr format
- ACL get/set operations using getxattr/setxattr
- permission testing and file operations
- Support for both ACCESS and DEFAULT ACL types

The implementation uses direct xattr API (getxattr/setxattr) to
test kernel ACL behavior directly. Tests run on ext2/3/4,
XFS, and Btrfs filesystems with ACL support.

Suggested-by: Cyril Hrubis <chrubis@suse.cz>
Signed-off-by: Sachin Sant <sachinp@linux.ibm.com>
---
V9 changes:
- Use RST manpage formatted documentation
- v8 link https://lore.kernel.org/ltp/20260613090543.78643-1-sachinp@linux.ibm.com/T/#t

V8 changes:
- Removed no chown variant of reset_test_path
- Use RST manpage formatted documentation
- v7 link https://lore.kernel.org/ltp/20260612171930.11964-1-sachinp@linux.ibm.com/T/#t

V7 changes:
- Use SAFE_ variants of setresgid/setresuid
- Replace TST_EXP_FAIL with TST_EXP_FAIL2
- v6 link https://lore.kernel.org/ltp/20260612102502.24071-1-sachinp@linux.ibm.com/T/#t

V6 changes:
- Added HAVE_SYS_XATTR_H guards for systems without xattr support
- Removed unnecessary error paths and manual status checking
- Uses SAFE_MALLOC() and new helper macros
- Added acl_type_to_name() helper to eliminate duplication
- v5 link https://lore.kernel.org/ltp/20260608092200.92827-1-sachinp@linux.ibm.com/T/#t

V5 changes:
- Switch to kernel only test validation to remove dependency on libacl
  and useradd/del commands.
- v4 link https://lore.kernel.org/ltp/20260604065417.25924-1-sachinp@linux.ibm.com/T/#t

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/20260603140147.50738-1-sachinp@linux.ibm.com/T/#t

V3 changes:
- Updated copyright header as per LTP format.
- v2 link https://lore.kernel.org/ltp/20260603065744.47106-1-sachinp@linux.ibm.com/T/#t

V2 changes:
- Added no chown variant of reset_test_path 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/20260602121958.27494-1-sachinp@linux.ibm.com/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/477836fd-80c8-4168-bfe6-00b374bb2534@linux.ibm.com/T/#t

---
 runtest/fs                               |   3 +
 testcases/kernel/fs/acl/.gitignore       |   1 +
 testcases/kernel/fs/acl/Makefile         |   8 +
 testcases/kernel/fs/acl/acl_lib.h        | 507 +++++++++++++++++++++++
 testcases/kernel/fs/acl/acl_user_obj01.c | 127 ++++++
 5 files changed, 646 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..2d9cba46d
--- /dev/null
+++ b/testcases/kernel/fs/acl/Makefile
@@ -0,0 +1,8 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# Copyright (c) 2026 IBM
+
+top_srcdir		?= ../../../..
+
+include $(top_srcdir)/include/mk/testcases.mk
+
+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..7fe68aa59
--- /dev/null
+++ b/testcases/kernel/fs/acl/acl_lib.h
@@ -0,0 +1,507 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (c) 2026 IBM
+ * Original shell test by Kai Zhao (ltcd3@cn.ibm.com)
+ * Converted to C by Sachin Sant <sachinp@linux.ibm.com>
+ *
+ * Common library for ACL and extended attribute tests using xattr API
+ */
+
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE
+#endif
+
+#ifndef ACL_LIB_H
+#define ACL_LIB_H
+
+#include "config.h"
+
+#include <pwd.h>
+#include <grp.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <string.h>
+#include <stdint.h>
+#include <endian.h>
+#include <sys/fsuid.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+
+#ifdef HAVE_SYS_XATTR_H
+#include <sys/xattr.h>
+#endif
+#include "tst_test.h"
+#include "tst_safe_stdio.h"
+#include "tst_capability.h"
+
+#define MNTPOINT	"mntpoint"
+#define TESTDIR		MNTPOINT "/testdir"
+#define TESTFILE	TESTDIR "/testfile"
+#define TESTSYMLINK	TESTDIR "/testsymlink"
+#define XATTR_BACKUP_FILE MNTPOINT "/xattr_backup.txt"
+
+/* 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
+
+/*
+ * POSIX ACL xattr format definitions
+ * These match the kernel's internal representation
+ */
+#define POSIX_ACL_XATTR_VERSION	0x0002
+
+/* ACL entry tag types */
+#define ACL_UNDEFINED_TAG	0x00
+#define ACL_USER_OBJ		0x01
+#define ACL_USER		0x02
+#define ACL_GROUP_OBJ		0x04
+#define ACL_GROUP		0x08
+#define ACL_MASK		0x10
+#define ACL_OTHER		0x20
+
+/* ACL permissions */
+#define ACL_READ		0x04
+#define ACL_WRITE		0x02
+#define ACL_EXECUTE		0x01
+
+/* ACL xattr names */
+#define XATTR_NAME_POSIX_ACL_ACCESS	"system.posix_acl_access"
+#define XATTR_NAME_POSIX_ACL_DEFAULT	"system.posix_acl_default"
+
+/* ACL type for set/get operations */
+#define ACL_TYPE_ACCESS		1
+#define ACL_TYPE_DEFAULT	2
+
+/* Convert host to little-endian */
+#if __BYTE_ORDER == __LITTLE_ENDIAN
+#define cpu_to_le16(x) (x)
+#define cpu_to_le32(x) (x)
+#define le16_to_cpu(x) (x)
+#define le32_to_cpu(x) (x)
+#else
+#define cpu_to_le16(x) __builtin_bswap16(x)
+#define cpu_to_le32(x) __builtin_bswap32(x)
+#define le16_to_cpu(x) __builtin_bswap16(x)
+#define le32_to_cpu(x) __builtin_bswap32(x)
+#endif
+
+/*
+ * POSIX ACL xattr format as stored in kernel
+ * This is the on-disk/in-xattr representation
+ */
+struct posix_acl_xattr_header {
+	uint32_t a_version;
+};
+
+struct posix_acl_xattr_entry {
+	uint16_t e_tag;
+	uint16_t e_perm;
+	uint32_t e_id;
+};
+
+/*
+ * Combined xattr structure for ACL operations
+ */
+struct acl_xattr {
+	struct posix_acl_xattr_header header;
+	struct posix_acl_xattr_entry entries[];
+};
+
+/*
+ * In-memory ACL representation for building ACLs
+ */
+#define MAX_ACL_ENTRIES 32
+
+struct acl_entry {
+	uint16_t tag;
+	uint16_t perm;
+	uint32_t id;
+};
+
+struct acl {
+	int count;
+	struct acl_entry entries[MAX_ACL_ENTRIES];
+};
+
+/*
+ * SAFE_TRY_UNLINK - unlink a file, ignoring ENOENT
+ * Won't TBROK if file doesn't exist
+ */
+#define SAFE_TRY_UNLINK(path) do { \
+	if (unlink(path) == -1 && errno != ENOENT) \
+		tst_brk(TBROK | TERRNO, "unlink(%s) failed", path); \
+} while (0)
+
+/*
+ * SAFE_TRY_RMDIR - remove a directory, ignoring ENOENT
+ * Won't TBROK if directory doesn't exist
+ */
+#define SAFE_TRY_RMDIR(path) do { \
+	if (rmdir(path) == -1 && errno != ENOENT) \
+		tst_brk(TBROK | TERRNO, "rmdir(%s) failed", path); \
+} while (0)
+
+static inline void reset_test_path(void)
+{
+	SAFE_TRY_UNLINK(TESTSYMLINK);
+	SAFE_TRY_UNLINK(TESTFILE);
+	SAFE_TRY_RMDIR(TESTDIR);
+
+	SAFE_MKDIR(TESTDIR, 0755);
+}
+
+static inline void cleanup_testfile(void)
+{
+	SAFE_TRY_UNLINK(TESTFILE);
+}
+
+#ifdef HAVE_SYS_XATTR_H
+
+/*
+ * Initialize an empty ACL structure
+ */
+static inline struct acl *acl_init(void)
+{
+	struct acl *acl = SAFE_MALLOC(sizeof(struct acl));
+
+	acl->count = 0;
+	return acl;
+}
+
+/*
+ * Free an ACL structure
+ */
+static inline void acl_free(struct acl *acl)
+{
+	free(acl);
+}
+
+/*
+ * Add an ACL entry to the ACL structure
+ */
+static inline int acl_add_entry(struct acl *acl, uint16_t tag, uint16_t perm,
+				uint32_t id)
+{
+	if (acl->count >= MAX_ACL_ENTRIES) {
+		errno = ENOMEM;
+		return -1;
+	}
+
+	acl->entries[acl->count].tag = tag;
+	acl->entries[acl->count].perm = perm;
+	acl->entries[acl->count].id = id;
+	acl->count++;
+	return 0;
+}
+
+/*
+ * Convert ACL type to xattr name
+ */
+static inline const char *acl_type_to_name(int type)
+{
+	if (type == ACL_TYPE_ACCESS)
+		return XATTR_NAME_POSIX_ACL_ACCESS;
+	else if (type == ACL_TYPE_DEFAULT)
+		return XATTR_NAME_POSIX_ACL_DEFAULT;
+	else
+		return NULL;
+}
+
+/*
+ * Set ACL on a file using xattr.
+ *
+ * The kernel stores access ACLs only when they differ from the file mode.
+ * If the ACL is equivalent to st_mode, the xattr is removed and future
+ * getxattr() calls return ENODATA. Mirror libacl semantics by treating
+ * ENODATA as a valid minimal ACL derived from st_mode.
+ */
+static inline int acl_set_file(const char *path, int type, struct acl *acl)
+{
+	const char *xattr_name;
+	size_t size;
+	char *buf;
+	struct posix_acl_xattr_header *header;
+	struct posix_acl_xattr_entry *entries;
+	int i, ret;
+
+	xattr_name = acl_type_to_name(type);
+	if (!xattr_name) {
+		errno = EINVAL;
+		return -1;
+	}
+
+	size = sizeof(struct posix_acl_xattr_header) +
+	       acl->count * sizeof(struct posix_acl_xattr_entry);
+
+	buf = malloc(size);
+	if (!buf)
+		return -1;
+
+	header = (struct posix_acl_xattr_header *)buf;
+	header->a_version = cpu_to_le32(POSIX_ACL_XATTR_VERSION);
+
+	entries = (struct posix_acl_xattr_entry *)(buf + sizeof(*header));
+
+	for (i = 0; i < acl->count; i++) {
+		entries[i].e_tag = cpu_to_le16(acl->entries[i].tag);
+		entries[i].e_perm = cpu_to_le16(acl->entries[i].perm);
+		entries[i].e_id = cpu_to_le32(acl->entries[i].id);
+	}
+
+	ret = setxattr(path, xattr_name, buf, size, 0);
+	free(buf);
+
+	return ret;
+}
+
+static inline void acl_add_mode_entries(struct acl *acl, mode_t mode)
+{
+	acl_add_entry(acl, ACL_USER_OBJ, (mode >> 6) & 07, 0);
+	acl_add_entry(acl, ACL_GROUP_OBJ, (mode >> 3) & 07, 0);
+	acl_add_entry(acl, ACL_OTHER, mode & 07, 0);
+}
+
+/*
+ * Synthesize an ACL from file mode bits.
+ * Used when no xattr exists for an access ACL.
+ */
+static inline struct acl *acl_from_mode(const char *path)
+{
+	struct acl *acl;
+	struct stat st;
+
+	if (stat(path, &st) < 0)
+		return NULL;
+
+	acl = acl_init();
+	acl_add_mode_entries(acl, st.st_mode);
+
+	return acl;
+}
+
+/*
+ * Get ACL from a file using xattr.
+ *
+ * Access ACLs equivalent to file mode may not have a backing xattr at all.
+ * In that case synthesize the base ACL from st_mode so callers observe the
+ * same behavior as acl_get_file(3).
+ */
+static inline struct acl *acl_get_file(const char *path, int type)
+{
+	const char *xattr_name;
+	ssize_t size;
+	struct acl_xattr *ax;
+	struct acl *acl;
+	int i, count;
+
+	xattr_name = acl_type_to_name(type);
+	if (!xattr_name) {
+		errno = EINVAL;
+		return NULL;
+	}
+
+	size = getxattr(path, xattr_name, NULL, 0);
+	if (size < 0) {
+		if (errno != ENODATA || type != ACL_TYPE_ACCESS)
+			return NULL;
+
+		return acl_from_mode(path);
+	}
+
+	/* Handle race: xattr removed between size check and actual read */
+	if (size == 0)
+		return acl_from_mode(path);
+
+	ax = malloc(size);
+	if (!ax)
+		return NULL;
+
+	size = getxattr(path, xattr_name, ax, size);
+	if (size < 0) {
+		free(ax);
+		/* Handle race: xattr removed between size check and read */
+		if (errno == ENODATA && type == ACL_TYPE_ACCESS)
+			return acl_from_mode(path);
+		return NULL;
+	}
+
+	if (le32_to_cpu(ax->header.a_version) != POSIX_ACL_XATTR_VERSION) {
+		free(ax);
+		errno = EINVAL;
+		return NULL;
+	}
+
+	count = (size - sizeof(ax->header)) /
+		sizeof(struct posix_acl_xattr_entry);
+
+	acl = acl_init();
+	if (!acl) {
+		free(ax);
+		return NULL;
+	}
+
+	for (i = 0; i < count; i++) {
+		uint16_t tag = le16_to_cpu(ax->entries[i].e_tag);
+		uint16_t perm = le16_to_cpu(ax->entries[i].e_perm);
+		uint32_t id = le32_to_cpu(ax->entries[i].e_id);
+
+		if (acl_add_entry(acl, tag, perm, id) < 0) {
+			acl_free(acl);
+			free(ax);
+			return NULL;
+		}
+	}
+
+	free(ax);
+	return acl;
+}
+
+/*
+ * Check if an ACL entry has a specific permission
+ */
+static inline int acl_entry_has_perm(struct acl_entry *entry, uint16_t perm)
+{
+	return (entry->perm & perm) == perm;
+}
+
+/*
+ * Check if an ACL entry has all rwx permissions
+ */
+static inline int acl_entry_has_rwx(struct acl_entry *entry)
+{
+	return acl_entry_has_perm(entry,
+				  ACL_READ | ACL_WRITE | ACL_EXECUTE);
+}
+
+/*
+ * Find an ACL entry by tag type
+ */
+static inline struct acl_entry *acl_find_entry(struct acl *acl, uint16_t tag,
+					       uint32_t id)
+{
+	int i;
+
+	for (i = 0; i < acl->count; i++) {
+		if (acl->entries[i].tag == tag) {
+			if (tag == ACL_USER || tag == ACL_GROUP) {
+				if (acl->entries[i].id == id)
+					return &acl->entries[i];
+			} else {
+				return &acl->entries[i];
+			}
+		}
+	}
+
+	return NULL;
+}
+
+/*
+ * Update ACL mask permissions
+ */
+static inline int acl_set_mask_perms(struct acl *acl, uint16_t perm)
+{
+	struct acl_entry *mask_entry = acl_find_entry(acl, ACL_MASK, 0);
+
+	if (!mask_entry) {
+		errno = ENOENT;
+		return -1;
+	}
+
+	mask_entry->perm = perm;
+	return 0;
+}
+
+static inline void create_file_as(uid_t uid, gid_t gid, mode_t mode,
+				   int use_umask, mode_t mask, int exp_errno)
+{
+	pid_t pid;
+
+	pid = SAFE_FORK();
+	if (!pid) {
+		uid_t fsuid;
+		gid_t fsgid;
+		struct tst_cap_user_header hdr = {
+			.version = 0x20080522,
+			.pid = 0,
+		};
+		struct tst_cap_user_data data[2] = {};
+
+		SAFE_SETGROUPS(0, NULL);
+		SAFE_SETRESGID(gid, gid, gid);
+		SAFE_SETRESUID(uid, uid, uid);
+
+		/* Drop all capabilities to ensure DAC checks are enforced */
+		if (tst_capset(&hdr, data) < 0)
+			tst_brk(TBROK | TERRNO, "capset failed");
+
+		/*
+		 * setfsuid()/setfsgid() return the previous value, not -1 on
+		 * failure. Verify the effective filesystem credentials by
+		 * reading them back.
+		 */
+		setfsuid(uid);
+		fsuid = setfsuid((uid_t)-1);
+		if (fsuid != uid)
+			tst_brk(TBROK,
+				"setfsuid verification failed, expected %u got %u",
+				(unsigned int)uid, (unsigned int)fsuid);
+
+		setfsgid(gid);
+		fsgid = setfsgid((gid_t)-1);
+		if (fsgid != gid)
+			tst_brk(TBROK,
+				"setfsgid verification failed, expected %u got %u",
+				(unsigned int)gid, (unsigned int)fsgid);
+
+		if (use_umask)
+			umask(mask);
+
+		if (exp_errno) {
+			TST_EXP_FAIL2(open(TESTFILE, O_CREAT | O_WRONLY, mode),
+				      exp_errno, "open(%s)", TESTFILE);
+		} else {
+			TST_EXP_FD(open(TESTFILE, O_CREAT | O_WRONLY, mode),
+				   "open(%s)", TESTFILE);
+			if (TST_RET >= 0)
+				SAFE_CLOSE(TST_RET);
+		}
+
+		exit(0);
+	}
+
+	tst_reap_children();
+}
+
+static inline void try_create_as(uid_t uid, gid_t gid, mode_t mode,
+				  int exp_errno)
+{
+	create_file_as(uid, gid, mode, 0, 0, exp_errno);
+}
+
+static inline void create_with_umask_as(uid_t uid, gid_t gid, mode_t mode,
+					 mode_t mask, int exp_errno)
+{
+	create_file_as(uid, gid, mode, 1, mask, exp_errno);
+}
+
+static inline void cleanup_test_paths(void)
+{
+	SAFE_TRY_UNLINK(TESTSYMLINK);
+	SAFE_TRY_UNLINK(TESTFILE);
+	SAFE_TRY_RMDIR(TESTDIR);
+}
+
+#endif /* HAVE_SYS_XATTR_H */
+
+#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..89291d275
--- /dev/null
+++ b/testcases/kernel/fs/acl/acl_user_obj01.c
@@ -0,0 +1,127 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (c) 2026 IBM
+ *
+ * Original shell test by Kai Zhao (ltcd3@cn.ibm.com)
+ * Converted to C by Sachin Sant <sachinp@linux.ibm.com>
+ */
+
+/*\
+ * Test ACL_USER_OBJ permissions using direct xattr manipulation.
+ *
+ * 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 :manpage:`setxattr(2)` overrides
+ *   a previous :manpage:`chmod(2)` restriction
+ * - Owner permissions work independently of group and other permissions
+ *
+ * This test uses arbitrary UIDs without creating actual users, testing
+ * only the kernel ACL implementation.
+ */
+
+#include "acl_lib.h"
+
+#ifdef HAVE_SYS_XATTR_H
+
+#define TEST_UID 1000
+#define TEST_GID 1000
+
+/*
+ * Test permission bits deny access.
+ * Owner should be denied file creation when directory mode is 0555.
+ */
+static void test_deny_by_mode(void)
+{
+	tst_res(TINFO, "Testing permission bits deny access");
+	reset_test_path();
+
+	SAFE_CHOWN(TESTDIR, TEST_UID, TEST_GID);
+	SAFE_CHMOD(TESTDIR, 0555);
+
+	try_create_as(TEST_UID, TEST_GID, 0644, EACCES);
+}
+
+/*
+ * Test ACL_USER_OBJ grants access.
+ * Setting ACL_USER_OBJ=rwx should restore owner write permission and
+ * allow file creation after the restrictive mode baseline.
+ */
+static void test_grant_by_acl(void)
+{
+	struct acl *acl;
+
+	tst_res(TINFO, "Testing ACL_USER_OBJ grants access");
+	reset_test_path();
+
+	SAFE_CHOWN(TESTDIR, TEST_UID, TEST_GID);
+	SAFE_CHMOD(TESTDIR, 0555);
+
+	acl = acl_init();
+
+	acl_add_entry(acl, ACL_USER_OBJ,
+		      ACL_READ | ACL_WRITE | ACL_EXECUTE, 0);
+	acl_add_entry(acl, ACL_GROUP_OBJ, 0, 0);
+	acl_add_entry(acl, ACL_OTHER, 0, 0);
+
+	if (acl_set_file(TESTDIR, ACL_TYPE_ACCESS, acl) < 0) {
+		if (errno == EOPNOTSUPP) {
+			acl_free(acl);
+			tst_brk(TCONF | TERRNO, "ACL not supported");
+		}
+		acl_free(acl);
+		tst_brk(TBROK | TERRNO, "ACL setup failed");
+	}
+
+	acl_free(acl);
+
+	try_create_as(TEST_UID, TEST_GID, 0644, 0);
+
+	cleanup_testfile();
+}
+
+static void run(unsigned int n)
+{
+	switch (n) {
+	case 0:
+		test_deny_by_mode();
+		break;
+	case 1:
+		test_grant_by_acl();
+		break;
+	}
+}
+
+static void setup(void)
+{
+	reset_test_path();
+}
+
+static void cleanup(void)
+{
+	cleanup_test_paths();
+}
+
+static struct tst_test test = {
+	.test = run,
+	.tcnt = 2,
+	.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"},
+		{}
+	}
+};
+
+#else
+	TST_TEST_TCONF("sys/xattr.h is not available");
+#endif
-- 
2.39.1


-- 
Mailing list info: https://lists.linux.it/listinfo/ltp

^ permalink raw reply related	[flat|nested] 23+ messages in thread
* [LTP] [PATCH v8 1/8] fs/acl: Add ACL_USER_OBJ permission test
@ 2026-06-13  9:05 Sachin Sant
  2026-06-13 10:36 ` [LTP] " linuxtestproject.agent
  0 siblings, 1 reply; 23+ messages in thread
From: Sachin Sant @ 2026-06-13  9:05 UTC (permalink / raw)
  To: ltp

Add acl_user_obj01 test to validate ACL_USER_OBJ permissions:
- Owner permissions correctly control file/directory access
- ACL_USER_OBJ=rwx via setxattr() overrides chmod restrictions
- Owner permissions work independently of group/other permissions
- Tests use arbitrary UIDs without requiring actual user creation

The patch also adds acl_lib.h containing shared helpers for ACL
manipulation via xattr API, including:
- ACL structure management (acl_init, acl_free, acl_add_entry)
- ACL serialization/deserialization for kernel xattr format
- ACL get/set operations using getxattr/setxattr
- permission testing and file operations
- Support for both ACCESS and DEFAULT ACL types

The implementation uses direct xattr API (getxattr/setxattr) to
test kernel ACL behavior directly. Tests run on ext2/3/4,
XFS, and Btrfs filesystems with ACL support.

Suggested-by: Cyril Hrubis <chrubis@suse.cz>
Signed-off-by: Sachin Sant <sachinp@linux.ibm.com>
---
V8 changes:
- Removed no chown variant of reset_test_path
- Use RST manpage formatted documentation
- v7 link https://lore.kernel.org/ltp/20260612171930.11964-1-sachinp@linux.ibm.com/T/#t

V7 changes:
- Use SAFE_ variants of setresgid/setresuid
- Replace TST_EXP_FAIL with TST_EXP_FAIL2
- v6 link https://lore.kernel.org/ltp/20260612102502.24071-1-sachinp@linux.ibm.com/T/#t

V6 changes:
- Added HAVE_SYS_XATTR_H guards for systems without xattr support
- Removed unnecessary error paths and manual status checking
- Uses SAFE_MALLOC() and new helper macros
- Added acl_type_to_name() helper to eliminate duplication
- v5 link https://lore.kernel.org/ltp/20260608092200.92827-1-sachinp@linux.ibm.com/T/#t

V5 changes:
- Switch to kernel only test validation to remove dependency on libacl
  and useradd/del commands.
- v4 link https://lore.kernel.org/ltp/20260604065417.25924-1-sachinp@linux.ibm.com/T/#t

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/20260603140147.50738-1-sachinp@linux.ibm.com/T/#t

V3 changes:
- Updated copyright header as per LTP format.
- v2 link https://lore.kernel.org/ltp/20260603065744.47106-1-sachinp@linux.ibm.com/T/#t

V2 changes:
- Added no chown variant of reset_test_path 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/20260602121958.27494-1-sachinp@linux.ibm.com/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/477836fd-80c8-4168-bfe6-00b374bb2534@linux.ibm.com/T/#t

---
 runtest/fs                               |   3 +
 testcases/kernel/fs/acl/.gitignore       |   1 +
 testcases/kernel/fs/acl/Makefile         |   8 +
 testcases/kernel/fs/acl/acl_lib.h        | 507 +++++++++++++++++++++++
 testcases/kernel/fs/acl/acl_user_obj01.c | 126 ++++++
 5 files changed, 645 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..2d9cba46d
--- /dev/null
+++ b/testcases/kernel/fs/acl/Makefile
@@ -0,0 +1,8 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# Copyright (c) 2026 IBM
+
+top_srcdir		?= ../../../..
+
+include $(top_srcdir)/include/mk/testcases.mk
+
+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..7fe68aa59
--- /dev/null
+++ b/testcases/kernel/fs/acl/acl_lib.h
@@ -0,0 +1,507 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (c) 2026 IBM
+ * Original shell test by Kai Zhao (ltcd3@cn.ibm.com)
+ * Converted to C by Sachin Sant <sachinp@linux.ibm.com>
+ *
+ * Common library for ACL and extended attribute tests using xattr API
+ */
+
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE
+#endif
+
+#ifndef ACL_LIB_H
+#define ACL_LIB_H
+
+#include "config.h"
+
+#include <pwd.h>
+#include <grp.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <string.h>
+#include <stdint.h>
+#include <endian.h>
+#include <sys/fsuid.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+
+#ifdef HAVE_SYS_XATTR_H
+#include <sys/xattr.h>
+#endif
+#include "tst_test.h"
+#include "tst_safe_stdio.h"
+#include "tst_capability.h"
+
+#define MNTPOINT	"mntpoint"
+#define TESTDIR		MNTPOINT "/testdir"
+#define TESTFILE	TESTDIR "/testfile"
+#define TESTSYMLINK	TESTDIR "/testsymlink"
+#define XATTR_BACKUP_FILE MNTPOINT "/xattr_backup.txt"
+
+/* 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
+
+/*
+ * POSIX ACL xattr format definitions
+ * These match the kernel's internal representation
+ */
+#define POSIX_ACL_XATTR_VERSION	0x0002
+
+/* ACL entry tag types */
+#define ACL_UNDEFINED_TAG	0x00
+#define ACL_USER_OBJ		0x01
+#define ACL_USER		0x02
+#define ACL_GROUP_OBJ		0x04
+#define ACL_GROUP		0x08
+#define ACL_MASK		0x10
+#define ACL_OTHER		0x20
+
+/* ACL permissions */
+#define ACL_READ		0x04
+#define ACL_WRITE		0x02
+#define ACL_EXECUTE		0x01
+
+/* ACL xattr names */
+#define XATTR_NAME_POSIX_ACL_ACCESS	"system.posix_acl_access"
+#define XATTR_NAME_POSIX_ACL_DEFAULT	"system.posix_acl_default"
+
+/* ACL type for set/get operations */
+#define ACL_TYPE_ACCESS		1
+#define ACL_TYPE_DEFAULT	2
+
+/* Convert host to little-endian */
+#if __BYTE_ORDER == __LITTLE_ENDIAN
+#define cpu_to_le16(x) (x)
+#define cpu_to_le32(x) (x)
+#define le16_to_cpu(x) (x)
+#define le32_to_cpu(x) (x)
+#else
+#define cpu_to_le16(x) __builtin_bswap16(x)
+#define cpu_to_le32(x) __builtin_bswap32(x)
+#define le16_to_cpu(x) __builtin_bswap16(x)
+#define le32_to_cpu(x) __builtin_bswap32(x)
+#endif
+
+/*
+ * POSIX ACL xattr format as stored in kernel
+ * This is the on-disk/in-xattr representation
+ */
+struct posix_acl_xattr_header {
+	uint32_t a_version;
+};
+
+struct posix_acl_xattr_entry {
+	uint16_t e_tag;
+	uint16_t e_perm;
+	uint32_t e_id;
+};
+
+/*
+ * Combined xattr structure for ACL operations
+ */
+struct acl_xattr {
+	struct posix_acl_xattr_header header;
+	struct posix_acl_xattr_entry entries[];
+};
+
+/*
+ * In-memory ACL representation for building ACLs
+ */
+#define MAX_ACL_ENTRIES 32
+
+struct acl_entry {
+	uint16_t tag;
+	uint16_t perm;
+	uint32_t id;
+};
+
+struct acl {
+	int count;
+	struct acl_entry entries[MAX_ACL_ENTRIES];
+};
+
+/*
+ * SAFE_TRY_UNLINK - unlink a file, ignoring ENOENT
+ * Won't TBROK if file doesn't exist
+ */
+#define SAFE_TRY_UNLINK(path) do { \
+	if (unlink(path) == -1 && errno != ENOENT) \
+		tst_brk(TBROK | TERRNO, "unlink(%s) failed", path); \
+} while (0)
+
+/*
+ * SAFE_TRY_RMDIR - remove a directory, ignoring ENOENT
+ * Won't TBROK if directory doesn't exist
+ */
+#define SAFE_TRY_RMDIR(path) do { \
+	if (rmdir(path) == -1 && errno != ENOENT) \
+		tst_brk(TBROK | TERRNO, "rmdir(%s) failed", path); \
+} while (0)
+
+static inline void reset_test_path(void)
+{
+	SAFE_TRY_UNLINK(TESTSYMLINK);
+	SAFE_TRY_UNLINK(TESTFILE);
+	SAFE_TRY_RMDIR(TESTDIR);
+
+	SAFE_MKDIR(TESTDIR, 0755);
+}
+
+static inline void cleanup_testfile(void)
+{
+	SAFE_TRY_UNLINK(TESTFILE);
+}
+
+#ifdef HAVE_SYS_XATTR_H
+
+/*
+ * Initialize an empty ACL structure
+ */
+static inline struct acl *acl_init(void)
+{
+	struct acl *acl = SAFE_MALLOC(sizeof(struct acl));
+
+	acl->count = 0;
+	return acl;
+}
+
+/*
+ * Free an ACL structure
+ */
+static inline void acl_free(struct acl *acl)
+{
+	free(acl);
+}
+
+/*
+ * Add an ACL entry to the ACL structure
+ */
+static inline int acl_add_entry(struct acl *acl, uint16_t tag, uint16_t perm,
+				uint32_t id)
+{
+	if (acl->count >= MAX_ACL_ENTRIES) {
+		errno = ENOMEM;
+		return -1;
+	}
+
+	acl->entries[acl->count].tag = tag;
+	acl->entries[acl->count].perm = perm;
+	acl->entries[acl->count].id = id;
+	acl->count++;
+	return 0;
+}
+
+/*
+ * Convert ACL type to xattr name
+ */
+static inline const char *acl_type_to_name(int type)
+{
+	if (type == ACL_TYPE_ACCESS)
+		return XATTR_NAME_POSIX_ACL_ACCESS;
+	else if (type == ACL_TYPE_DEFAULT)
+		return XATTR_NAME_POSIX_ACL_DEFAULT;
+	else
+		return NULL;
+}
+
+/*
+ * Set ACL on a file using xattr.
+ *
+ * The kernel stores access ACLs only when they differ from the file mode.
+ * If the ACL is equivalent to st_mode, the xattr is removed and future
+ * getxattr() calls return ENODATA. Mirror libacl semantics by treating
+ * ENODATA as a valid minimal ACL derived from st_mode.
+ */
+static inline int acl_set_file(const char *path, int type, struct acl *acl)
+{
+	const char *xattr_name;
+	size_t size;
+	char *buf;
+	struct posix_acl_xattr_header *header;
+	struct posix_acl_xattr_entry *entries;
+	int i, ret;
+
+	xattr_name = acl_type_to_name(type);
+	if (!xattr_name) {
+		errno = EINVAL;
+		return -1;
+	}
+
+	size = sizeof(struct posix_acl_xattr_header) +
+	       acl->count * sizeof(struct posix_acl_xattr_entry);
+
+	buf = malloc(size);
+	if (!buf)
+		return -1;
+
+	header = (struct posix_acl_xattr_header *)buf;
+	header->a_version = cpu_to_le32(POSIX_ACL_XATTR_VERSION);
+
+	entries = (struct posix_acl_xattr_entry *)(buf + sizeof(*header));
+
+	for (i = 0; i < acl->count; i++) {
+		entries[i].e_tag = cpu_to_le16(acl->entries[i].tag);
+		entries[i].e_perm = cpu_to_le16(acl->entries[i].perm);
+		entries[i].e_id = cpu_to_le32(acl->entries[i].id);
+	}
+
+	ret = setxattr(path, xattr_name, buf, size, 0);
+	free(buf);
+
+	return ret;
+}
+
+static inline void acl_add_mode_entries(struct acl *acl, mode_t mode)
+{
+	acl_add_entry(acl, ACL_USER_OBJ, (mode >> 6) & 07, 0);
+	acl_add_entry(acl, ACL_GROUP_OBJ, (mode >> 3) & 07, 0);
+	acl_add_entry(acl, ACL_OTHER, mode & 07, 0);
+}
+
+/*
+ * Synthesize an ACL from file mode bits.
+ * Used when no xattr exists for an access ACL.
+ */
+static inline struct acl *acl_from_mode(const char *path)
+{
+	struct acl *acl;
+	struct stat st;
+
+	if (stat(path, &st) < 0)
+		return NULL;
+
+	acl = acl_init();
+	acl_add_mode_entries(acl, st.st_mode);
+
+	return acl;
+}
+
+/*
+ * Get ACL from a file using xattr.
+ *
+ * Access ACLs equivalent to file mode may not have a backing xattr at all.
+ * In that case synthesize the base ACL from st_mode so callers observe the
+ * same behavior as acl_get_file(3).
+ */
+static inline struct acl *acl_get_file(const char *path, int type)
+{
+	const char *xattr_name;
+	ssize_t size;
+	struct acl_xattr *ax;
+	struct acl *acl;
+	int i, count;
+
+	xattr_name = acl_type_to_name(type);
+	if (!xattr_name) {
+		errno = EINVAL;
+		return NULL;
+	}
+
+	size = getxattr(path, xattr_name, NULL, 0);
+	if (size < 0) {
+		if (errno != ENODATA || type != ACL_TYPE_ACCESS)
+			return NULL;
+
+		return acl_from_mode(path);
+	}
+
+	/* Handle race: xattr removed between size check and actual read */
+	if (size == 0)
+		return acl_from_mode(path);
+
+	ax = malloc(size);
+	if (!ax)
+		return NULL;
+
+	size = getxattr(path, xattr_name, ax, size);
+	if (size < 0) {
+		free(ax);
+		/* Handle race: xattr removed between size check and read */
+		if (errno == ENODATA && type == ACL_TYPE_ACCESS)
+			return acl_from_mode(path);
+		return NULL;
+	}
+
+	if (le32_to_cpu(ax->header.a_version) != POSIX_ACL_XATTR_VERSION) {
+		free(ax);
+		errno = EINVAL;
+		return NULL;
+	}
+
+	count = (size - sizeof(ax->header)) /
+		sizeof(struct posix_acl_xattr_entry);
+
+	acl = acl_init();
+	if (!acl) {
+		free(ax);
+		return NULL;
+	}
+
+	for (i = 0; i < count; i++) {
+		uint16_t tag = le16_to_cpu(ax->entries[i].e_tag);
+		uint16_t perm = le16_to_cpu(ax->entries[i].e_perm);
+		uint32_t id = le32_to_cpu(ax->entries[i].e_id);
+
+		if (acl_add_entry(acl, tag, perm, id) < 0) {
+			acl_free(acl);
+			free(ax);
+			return NULL;
+		}
+	}
+
+	free(ax);
+	return acl;
+}
+
+/*
+ * Check if an ACL entry has a specific permission
+ */
+static inline int acl_entry_has_perm(struct acl_entry *entry, uint16_t perm)
+{
+	return (entry->perm & perm) == perm;
+}
+
+/*
+ * Check if an ACL entry has all rwx permissions
+ */
+static inline int acl_entry_has_rwx(struct acl_entry *entry)
+{
+	return acl_entry_has_perm(entry,
+				  ACL_READ | ACL_WRITE | ACL_EXECUTE);
+}
+
+/*
+ * Find an ACL entry by tag type
+ */
+static inline struct acl_entry *acl_find_entry(struct acl *acl, uint16_t tag,
+					       uint32_t id)
+{
+	int i;
+
+	for (i = 0; i < acl->count; i++) {
+		if (acl->entries[i].tag == tag) {
+			if (tag == ACL_USER || tag == ACL_GROUP) {
+				if (acl->entries[i].id == id)
+					return &acl->entries[i];
+			} else {
+				return &acl->entries[i];
+			}
+		}
+	}
+
+	return NULL;
+}
+
+/*
+ * Update ACL mask permissions
+ */
+static inline int acl_set_mask_perms(struct acl *acl, uint16_t perm)
+{
+	struct acl_entry *mask_entry = acl_find_entry(acl, ACL_MASK, 0);
+
+	if (!mask_entry) {
+		errno = ENOENT;
+		return -1;
+	}
+
+	mask_entry->perm = perm;
+	return 0;
+}
+
+static inline void create_file_as(uid_t uid, gid_t gid, mode_t mode,
+				   int use_umask, mode_t mask, int exp_errno)
+{
+	pid_t pid;
+
+	pid = SAFE_FORK();
+	if (!pid) {
+		uid_t fsuid;
+		gid_t fsgid;
+		struct tst_cap_user_header hdr = {
+			.version = 0x20080522,
+			.pid = 0,
+		};
+		struct tst_cap_user_data data[2] = {};
+
+		SAFE_SETGROUPS(0, NULL);
+		SAFE_SETRESGID(gid, gid, gid);
+		SAFE_SETRESUID(uid, uid, uid);
+
+		/* Drop all capabilities to ensure DAC checks are enforced */
+		if (tst_capset(&hdr, data) < 0)
+			tst_brk(TBROK | TERRNO, "capset failed");
+
+		/*
+		 * setfsuid()/setfsgid() return the previous value, not -1 on
+		 * failure. Verify the effective filesystem credentials by
+		 * reading them back.
+		 */
+		setfsuid(uid);
+		fsuid = setfsuid((uid_t)-1);
+		if (fsuid != uid)
+			tst_brk(TBROK,
+				"setfsuid verification failed, expected %u got %u",
+				(unsigned int)uid, (unsigned int)fsuid);
+
+		setfsgid(gid);
+		fsgid = setfsgid((gid_t)-1);
+		if (fsgid != gid)
+			tst_brk(TBROK,
+				"setfsgid verification failed, expected %u got %u",
+				(unsigned int)gid, (unsigned int)fsgid);
+
+		if (use_umask)
+			umask(mask);
+
+		if (exp_errno) {
+			TST_EXP_FAIL2(open(TESTFILE, O_CREAT | O_WRONLY, mode),
+				      exp_errno, "open(%s)", TESTFILE);
+		} else {
+			TST_EXP_FD(open(TESTFILE, O_CREAT | O_WRONLY, mode),
+				   "open(%s)", TESTFILE);
+			if (TST_RET >= 0)
+				SAFE_CLOSE(TST_RET);
+		}
+
+		exit(0);
+	}
+
+	tst_reap_children();
+}
+
+static inline void try_create_as(uid_t uid, gid_t gid, mode_t mode,
+				  int exp_errno)
+{
+	create_file_as(uid, gid, mode, 0, 0, exp_errno);
+}
+
+static inline void create_with_umask_as(uid_t uid, gid_t gid, mode_t mode,
+					 mode_t mask, int exp_errno)
+{
+	create_file_as(uid, gid, mode, 1, mask, exp_errno);
+}
+
+static inline void cleanup_test_paths(void)
+{
+	SAFE_TRY_UNLINK(TESTSYMLINK);
+	SAFE_TRY_UNLINK(TESTFILE);
+	SAFE_TRY_RMDIR(TESTDIR);
+}
+
+#endif /* HAVE_SYS_XATTR_H */
+
+#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..a442f1131
--- /dev/null
+++ b/testcases/kernel/fs/acl/acl_user_obj01.c
@@ -0,0 +1,126 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (c) 2026 IBM
+ *
+ * Original shell test by Kai Zhao (ltcd3@cn.ibm.com)
+ * Converted to C by Sachin Sant <sachinp@linux.ibm.com>
+ */
+
+/*\
+ * Test ACL_USER_OBJ permissions using direct xattr manipulation.
+ *
+ * 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 :manpage:`setxattr(2)` overrides 
+ *   a previous :manpage:`chmod(2)` restriction
+ * - Owner permissions work independently of group and other permissions
+ *
+ * This test uses arbitrary UIDs without creating actual users, testing
+ * only the kernel ACL implementation.
+ */
+
+#include "acl_lib.h"
+
+#ifdef HAVE_SYS_XATTR_H
+
+#define TEST_UID 1000
+#define TEST_GID 1000
+
+/*
+ * Test permission bits deny access.
+ * Owner should be denied file creation when directory mode is 0555.
+ */
+static void test_deny_by_mode(void)
+{
+	tst_res(TINFO, "Testing permission bits deny access");
+	reset_test_path();
+
+	SAFE_CHOWN(TESTDIR, TEST_UID, TEST_GID);
+	SAFE_CHMOD(TESTDIR, 0555);
+
+	try_create_as(TEST_UID, TEST_GID, 0644, EACCES);
+}
+
+/*
+ * Test ACL_USER_OBJ grants access.
+ * Setting ACL_USER_OBJ=rwx should restore owner write permission and
+ * allow file creation after the restrictive mode baseline.
+ */
+static void test_grant_by_acl(void)
+{
+	struct acl *acl;
+
+	tst_res(TINFO, "Testing ACL_USER_OBJ grants access");
+	reset_test_path();
+
+	SAFE_CHOWN(TESTDIR, TEST_UID, TEST_GID);
+	SAFE_CHMOD(TESTDIR, 0555);
+
+	acl = acl_init();
+
+	acl_add_entry(acl, ACL_USER_OBJ,
+		      ACL_READ | ACL_WRITE | ACL_EXECUTE, 0);
+	acl_add_entry(acl, ACL_GROUP_OBJ, 0, 0);
+	acl_add_entry(acl, ACL_OTHER, 0, 0);
+
+	if (acl_set_file(TESTDIR, ACL_TYPE_ACCESS, acl) < 0) {
+		if (errno == EOPNOTSUPP) {
+			acl_free(acl);
+			tst_brk(TCONF | TERRNO, "ACL not supported");
+		}
+		acl_free(acl);
+		tst_brk(TBROK | TERRNO, "ACL setup failed");
+	}
+
+	acl_free(acl);
+
+	try_create_as(TEST_UID, TEST_GID, 0644, 0);
+
+	cleanup_testfile();
+}
+
+static void run(unsigned int n)
+{
+	switch (n) {
+	case 0:
+		test_deny_by_mode();
+		break;
+	case 1:
+		test_grant_by_acl();
+		break;
+	}
+}
+
+static void setup(void)
+{
+	reset_test_path();
+}
+
+static void cleanup(void)
+{
+	cleanup_test_paths();
+}
+
+static struct tst_test test = {
+	.test = run,
+	.tcnt = 2,
+	.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"},
+		{}
+	}
+};
+
+#else
+	TST_TEST_TCONF("sys/xattr.h is not available");
+#endif
-- 
2.39.1


-- 
Mailing list info: https://lists.linux.it/listinfo/ltp

^ permalink raw reply related	[flat|nested] 23+ messages in thread
* [LTP] [PATCH v7 1/8] fs/acl: Add ACL_USER_OBJ permission test
@ 2026-06-12 17:19 Sachin Sant
  2026-06-12 19:13 ` [LTP] " linuxtestproject.agent
  0 siblings, 1 reply; 23+ messages in thread
From: Sachin Sant @ 2026-06-12 17:19 UTC (permalink / raw)
  To: ltp

Add acl_user_obj01 test to validate ACL_USER_OBJ permissions:
- Owner permissions correctly control file/directory access
- ACL_USER_OBJ=rwx via setxattr() overrides chmod restrictions
- Owner permissions work independently of group/other permissions
- Tests use arbitrary UIDs without requiring actual user creation

The patch also adds acl_lib.h containing shared helpers for ACL
manipulation via xattr API, including:
- ACL structure management (acl_init, acl_free, acl_add_entry)
- ACL serialization/deserialization for kernel xattr format
- ACL get/set operations using getxattr/setxattr
- permission testing and file operations
- Support for both ACCESS and DEFAULT ACL types

The implementation uses direct xattr API (getxattr/setxattr) to
test kernel ACL behavior directly. Tests run on ext2/3/4,
XFS, and Btrfs filesystems with ACL support.

Suggested-by: Cyril Hrubis <chrubis@suse.cz>
Signed-off-by: Sachin Sant <sachinp@linux.ibm.com>
---
V7 changes:
- Use SAFE_ variants of setresgid/setresuid
- Replace TST_EXP_FAIL with TST_EXP_FAIL2
- v6 link https://lore.kernel.org/ltp/20260612102502.24071-1-sachinp@linux.ibm.com/T/#t

V6 changes:
- Added HAVE_SYS_XATTR_H guards for systems without xattr support
- Removed unnecessary error paths and manual status checking
- Uses SAFE_MALLOC() and new helper macros
- Added acl_type_to_name() helper to eliminate duplication
- v5 link https://lore.kernel.org/ltp/20260608092200.92827-1-sachinp@linux.ibm.com/T/#t

V5 changes:
- Switch to kernel only test validation to remove dependency on libacl
  and useradd/del commands.
- v4 link https://lore.kernel.org/ltp/20260604065417.25924-1-sachinp@linux.ibm.com/T/#t

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/20260603140147.50738-1-sachinp@linux.ibm.com/T/#t

V3 changes:
- Updated copyright header as per LTP format.
- v2 link https://lore.kernel.org/ltp/20260603065744.47106-1-sachinp@linux.ibm.com/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/20260602121958.27494-1-sachinp@linux.ibm.com/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/477836fd-80c8-4168-bfe6-00b374bb2534@linux.ibm.com/T/#t

---
 runtest/fs                               |   3 +
 testcases/kernel/fs/acl/.gitignore       |   1 +
 testcases/kernel/fs/acl/Makefile         |   8 +
 testcases/kernel/fs/acl/acl_lib.h        | 512 +++++++++++++++++++++++
 testcases/kernel/fs/acl/acl_user_obj01.c | 126 ++++++
 5 files changed, 650 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..2d9cba46d
--- /dev/null
+++ b/testcases/kernel/fs/acl/Makefile
@@ -0,0 +1,8 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# Copyright (c) 2026 IBM
+
+top_srcdir		?= ../../../..
+
+include $(top_srcdir)/include/mk/testcases.mk
+
+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..550f2ebf9
--- /dev/null
+++ b/testcases/kernel/fs/acl/acl_lib.h
@@ -0,0 +1,512 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (c) 2026 IBM
+ * Original shell test by Kai Zhao (ltcd3@cn.ibm.com)
+ * Converted to C by Sachin Sant <sachinp@linux.ibm.com>
+ *
+ * Common library for ACL and extended attribute tests using xattr API
+ */
+
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE
+#endif
+
+#ifndef ACL_LIB_H
+#define ACL_LIB_H
+
+#include "config.h"
+
+#include <pwd.h>
+#include <grp.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <string.h>
+#include <stdint.h>
+#include <endian.h>
+#include <sys/fsuid.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+
+#ifdef HAVE_SYS_XATTR_H
+#include <sys/xattr.h>
+#endif
+#include "tst_test.h"
+#include "tst_safe_stdio.h"
+#include "tst_capability.h"
+
+#define MNTPOINT	"mntpoint"
+#define TESTDIR		MNTPOINT "/testdir"
+#define TESTFILE	TESTDIR "/testfile"
+#define TESTSYMLINK	TESTDIR "/testsymlink"
+#define XATTR_BACKUP_FILE MNTPOINT "/xattr_backup.txt"
+
+/* 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
+
+/*
+ * POSIX ACL xattr format definitions
+ * These match the kernel's internal representation
+ */
+#define POSIX_ACL_XATTR_VERSION	0x0002
+
+/* ACL entry tag types */
+#define ACL_UNDEFINED_TAG	0x00
+#define ACL_USER_OBJ		0x01
+#define ACL_USER		0x02
+#define ACL_GROUP_OBJ		0x04
+#define ACL_GROUP		0x08
+#define ACL_MASK		0x10
+#define ACL_OTHER		0x20
+
+/* ACL permissions */
+#define ACL_READ		0x04
+#define ACL_WRITE		0x02
+#define ACL_EXECUTE		0x01
+
+/* ACL xattr names */
+#define XATTR_NAME_POSIX_ACL_ACCESS	"system.posix_acl_access"
+#define XATTR_NAME_POSIX_ACL_DEFAULT	"system.posix_acl_default"
+
+/* ACL type for set/get operations */
+#define ACL_TYPE_ACCESS		1
+#define ACL_TYPE_DEFAULT	2
+
+/* Convert host to little-endian */
+#if __BYTE_ORDER == __LITTLE_ENDIAN
+#define cpu_to_le16(x) (x)
+#define cpu_to_le32(x) (x)
+#define le16_to_cpu(x) (x)
+#define le32_to_cpu(x) (x)
+#else
+#define cpu_to_le16(x) __builtin_bswap16(x)
+#define cpu_to_le32(x) __builtin_bswap32(x)
+#define le16_to_cpu(x) __builtin_bswap16(x)
+#define le32_to_cpu(x) __builtin_bswap32(x)
+#endif
+
+/*
+ * POSIX ACL xattr format as stored in kernel
+ * This is the on-disk/in-xattr representation
+ */
+struct posix_acl_xattr_header {
+	uint32_t a_version;
+};
+
+struct posix_acl_xattr_entry {
+	uint16_t e_tag;
+	uint16_t e_perm;
+	uint32_t e_id;
+};
+
+/*
+ * Combined xattr structure for ACL operations
+ */
+struct acl_xattr {
+	struct posix_acl_xattr_header header;
+	struct posix_acl_xattr_entry entries[];
+};
+
+/*
+ * In-memory ACL representation for building ACLs
+ */
+#define MAX_ACL_ENTRIES 32
+
+struct acl_entry {
+	uint16_t tag;
+	uint16_t perm;
+	uint32_t id;
+};
+
+struct acl {
+	int count;
+	struct acl_entry entries[MAX_ACL_ENTRIES];
+};
+
+/*
+ * SAFE_TRY_UNLINK - unlink a file, ignoring ENOENT
+ * Won't TBROK if file doesn't exist
+ */
+#define SAFE_TRY_UNLINK(path) do { \
+	if (unlink(path) == -1 && errno != ENOENT) \
+		tst_brk(TBROK | TERRNO, "unlink(%s) failed", path); \
+} while (0)
+
+/*
+ * SAFE_TRY_RMDIR - remove a directory, ignoring ENOENT
+ * Won't TBROK if directory doesn't exist
+ */
+#define SAFE_TRY_RMDIR(path) do { \
+	if (rmdir(path) == -1 && errno != ENOENT) \
+		tst_brk(TBROK | TERRNO, "rmdir(%s) failed", path); \
+} while (0)
+
+static inline void reset_test_path_no_chown(void)
+{
+	SAFE_TRY_UNLINK(TESTSYMLINK);
+	SAFE_TRY_UNLINK(TESTFILE);
+	SAFE_TRY_RMDIR(TESTDIR);
+
+	SAFE_MKDIR(TESTDIR, 0755);
+}
+
+static inline void reset_test_path(void)
+{
+	reset_test_path_no_chown();
+}
+
+static inline void cleanup_testfile(void)
+{
+	SAFE_TRY_UNLINK(TESTFILE);
+}
+
+#ifdef HAVE_SYS_XATTR_H
+
+/*
+ * Initialize an empty ACL structure
+ */
+static inline struct acl *acl_init(void)
+{
+	struct acl *acl = SAFE_MALLOC(sizeof(struct acl));
+
+	acl->count = 0;
+	return acl;
+}
+
+/*
+ * Free an ACL structure
+ */
+static inline void acl_free(struct acl *acl)
+{
+	free(acl);
+}
+
+/*
+ * Add an ACL entry to the ACL structure
+ */
+static inline int acl_add_entry(struct acl *acl, uint16_t tag, uint16_t perm,
+				uint32_t id)
+{
+	if (acl->count >= MAX_ACL_ENTRIES) {
+		errno = ENOMEM;
+		return -1;
+	}
+
+	acl->entries[acl->count].tag = tag;
+	acl->entries[acl->count].perm = perm;
+	acl->entries[acl->count].id = id;
+	acl->count++;
+	return 0;
+}
+
+/*
+ * Convert ACL type to xattr name
+ */
+static inline const char *acl_type_to_name(int type)
+{
+	if (type == ACL_TYPE_ACCESS)
+		return XATTR_NAME_POSIX_ACL_ACCESS;
+	else if (type == ACL_TYPE_DEFAULT)
+		return XATTR_NAME_POSIX_ACL_DEFAULT;
+	else
+		return NULL;
+}
+
+/*
+ * Set ACL on a file using xattr.
+ *
+ * The kernel stores access ACLs only when they differ from the file mode.
+ * If the ACL is equivalent to st_mode, the xattr is removed and future
+ * getxattr() calls return ENODATA. Mirror libacl semantics by treating
+ * ENODATA as a valid minimal ACL derived from st_mode.
+ */
+static inline int acl_set_file(const char *path, int type, struct acl *acl)
+{
+	const char *xattr_name;
+	size_t size;
+	char *buf;
+	struct posix_acl_xattr_header *header;
+	struct posix_acl_xattr_entry *entries;
+	int i, ret;
+
+	xattr_name = acl_type_to_name(type);
+	if (!xattr_name) {
+		errno = EINVAL;
+		return -1;
+	}
+
+	size = sizeof(struct posix_acl_xattr_header) +
+	       acl->count * sizeof(struct posix_acl_xattr_entry);
+
+	buf = malloc(size);
+	if (!buf)
+		return -1;
+
+	header = (struct posix_acl_xattr_header *)buf;
+	header->a_version = cpu_to_le32(POSIX_ACL_XATTR_VERSION);
+
+	entries = (struct posix_acl_xattr_entry *)(buf + sizeof(*header));
+
+	for (i = 0; i < acl->count; i++) {
+		entries[i].e_tag = cpu_to_le16(acl->entries[i].tag);
+		entries[i].e_perm = cpu_to_le16(acl->entries[i].perm);
+		entries[i].e_id = cpu_to_le32(acl->entries[i].id);
+	}
+
+	ret = setxattr(path, xattr_name, buf, size, 0);
+	free(buf);
+
+	return ret;
+}
+
+static inline void acl_add_mode_entries(struct acl *acl, mode_t mode)
+{
+	acl_add_entry(acl, ACL_USER_OBJ, (mode >> 6) & 07, 0);
+	acl_add_entry(acl, ACL_GROUP_OBJ, (mode >> 3) & 07, 0);
+	acl_add_entry(acl, ACL_OTHER, mode & 07, 0);
+}
+
+/*
+ * Synthesize an ACL from file mode bits.
+ * Used when no xattr exists for an access ACL.
+ */
+static inline struct acl *acl_from_mode(const char *path)
+{
+	struct acl *acl;
+	struct stat st;
+
+	if (stat(path, &st) < 0)
+		return NULL;
+
+	acl = acl_init();
+	acl_add_mode_entries(acl, st.st_mode);
+
+	return acl;
+}
+
+/*
+ * Get ACL from a file using xattr.
+ *
+ * Access ACLs equivalent to file mode may not have a backing xattr at all.
+ * In that case synthesize the base ACL from st_mode so callers observe the
+ * same behavior as acl_get_file(3).
+ */
+static inline struct acl *acl_get_file(const char *path, int type)
+{
+	const char *xattr_name;
+	ssize_t size;
+	struct acl_xattr *ax;
+	struct acl *acl;
+	int i, count;
+
+	xattr_name = acl_type_to_name(type);
+	if (!xattr_name) {
+		errno = EINVAL;
+		return NULL;
+	}
+
+	size = getxattr(path, xattr_name, NULL, 0);
+	if (size < 0) {
+		if (errno != ENODATA || type != ACL_TYPE_ACCESS)
+			return NULL;
+
+		return acl_from_mode(path);
+	}
+
+	/* Handle race: xattr removed between size check and actual read */
+	if (size == 0)
+		return acl_from_mode(path);
+
+	ax = malloc(size);
+	if (!ax)
+		return NULL;
+
+	size = getxattr(path, xattr_name, ax, size);
+	if (size < 0) {
+		free(ax);
+		/* Handle race: xattr removed between size check and read */
+		if (errno == ENODATA && type == ACL_TYPE_ACCESS)
+			return acl_from_mode(path);
+		return NULL;
+	}
+
+	if (le32_to_cpu(ax->header.a_version) != POSIX_ACL_XATTR_VERSION) {
+		free(ax);
+		errno = EINVAL;
+		return NULL;
+	}
+
+	count = (size - sizeof(ax->header)) /
+		sizeof(struct posix_acl_xattr_entry);
+
+	acl = acl_init();
+	if (!acl) {
+		free(ax);
+		return NULL;
+	}
+
+	for (i = 0; i < count; i++) {
+		uint16_t tag = le16_to_cpu(ax->entries[i].e_tag);
+		uint16_t perm = le16_to_cpu(ax->entries[i].e_perm);
+		uint32_t id = le32_to_cpu(ax->entries[i].e_id);
+
+		if (acl_add_entry(acl, tag, perm, id) < 0) {
+			acl_free(acl);
+			free(ax);
+			return NULL;
+		}
+	}
+
+	free(ax);
+	return acl;
+}
+
+/*
+ * Check if an ACL entry has a specific permission
+ */
+static inline int acl_entry_has_perm(struct acl_entry *entry, uint16_t perm)
+{
+	return (entry->perm & perm) == perm;
+}
+
+/*
+ * Check if an ACL entry has all rwx permissions
+ */
+static inline int acl_entry_has_rwx(struct acl_entry *entry)
+{
+	return acl_entry_has_perm(entry,
+				  ACL_READ | ACL_WRITE | ACL_EXECUTE);
+}
+
+/*
+ * Find an ACL entry by tag type
+ */
+static inline struct acl_entry *acl_find_entry(struct acl *acl, uint16_t tag,
+					       uint32_t id)
+{
+	int i;
+
+	for (i = 0; i < acl->count; i++) {
+		if (acl->entries[i].tag == tag) {
+			if (tag == ACL_USER || tag == ACL_GROUP) {
+				if (acl->entries[i].id == id)
+					return &acl->entries[i];
+			} else {
+				return &acl->entries[i];
+			}
+		}
+	}
+
+	return NULL;
+}
+
+/*
+ * Update ACL mask permissions
+ */
+static inline int acl_set_mask_perms(struct acl *acl, uint16_t perm)
+{
+	struct acl_entry *mask_entry = acl_find_entry(acl, ACL_MASK, 0);
+
+	if (!mask_entry) {
+		errno = ENOENT;
+		return -1;
+	}
+
+	mask_entry->perm = perm;
+	return 0;
+}
+
+static inline void create_file_as(uid_t uid, gid_t gid, mode_t mode,
+				   int use_umask, mode_t mask, int exp_errno)
+{
+	pid_t pid;
+
+	pid = SAFE_FORK();
+	if (!pid) {
+		uid_t fsuid;
+		gid_t fsgid;
+		struct tst_cap_user_header hdr = {
+			.version = 0x20080522,
+			.pid = 0,
+		};
+		struct tst_cap_user_data data[2] = {};
+
+		SAFE_SETGROUPS(0, NULL);
+		SAFE_SETRESGID(gid, gid, gid);
+		SAFE_SETRESUID(uid, uid, uid);
+
+		/* Drop all capabilities to ensure DAC checks are enforced */
+		if (tst_capset(&hdr, data) < 0)
+			tst_brk(TBROK | TERRNO, "capset failed");
+
+		/*
+		 * setfsuid()/setfsgid() return the previous value, not -1 on
+		 * failure. Verify the effective filesystem credentials by
+		 * reading them back.
+		 */
+		setfsuid(uid);
+		fsuid = setfsuid((uid_t)-1);
+		if (fsuid != uid)
+			tst_brk(TBROK,
+				"setfsuid verification failed, expected %u got %u",
+				(unsigned int)uid, (unsigned int)fsuid);
+
+		setfsgid(gid);
+		fsgid = setfsgid((gid_t)-1);
+		if (fsgid != gid)
+			tst_brk(TBROK,
+				"setfsgid verification failed, expected %u got %u",
+				(unsigned int)gid, (unsigned int)fsgid);
+
+		if (use_umask)
+			umask(mask);
+
+		if (exp_errno) {
+			TST_EXP_FAIL2(open(TESTFILE, O_CREAT | O_WRONLY, mode),
+				      exp_errno, "open(%s)", TESTFILE);
+		} else {
+			TST_EXP_FD(open(TESTFILE, O_CREAT | O_WRONLY, mode),
+				   "open(%s)", TESTFILE);
+			if (TST_RET >= 0)
+				SAFE_CLOSE(TST_RET);
+		}
+
+		exit(0);
+	}
+
+	tst_reap_children();
+}
+
+static inline void try_create_as(uid_t uid, gid_t gid, mode_t mode,
+				  int exp_errno)
+{
+	create_file_as(uid, gid, mode, 0, 0, exp_errno);
+}
+
+static inline void create_with_umask_as(uid_t uid, gid_t gid, mode_t mode,
+					 mode_t mask, int exp_errno)
+{
+	create_file_as(uid, gid, mode, 1, mask, exp_errno);
+}
+
+static inline void cleanup_test_paths(void)
+{
+	SAFE_TRY_UNLINK(TESTSYMLINK);
+	SAFE_TRY_UNLINK(TESTFILE);
+	SAFE_TRY_RMDIR(TESTDIR);
+}
+
+#endif /* HAVE_SYS_XATTR_H */
+
+#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..f82950a2b
--- /dev/null
+++ b/testcases/kernel/fs/acl/acl_user_obj01.c
@@ -0,0 +1,126 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (c) 2026 IBM
+ *
+ * Original shell test by Kai Zhao (ltcd3@cn.ibm.com)
+ * Converted to C by Sachin Sant <sachinp@linux.ibm.com>
+ */
+
+/*\
+ * Test ACL_USER_OBJ permissions using direct xattr manipulation.
+ *
+ * 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 setxattr() overrides a previous
+ *   chmod restriction
+ * - Owner permissions work independently of group and other permissions
+ *
+ * This test uses arbitrary UIDs without creating actual users, testing
+ * only the kernel ACL implementation.
+ */
+
+#include "acl_lib.h"
+
+#ifdef HAVE_SYS_XATTR_H
+
+#define TEST_UID 1000
+#define TEST_GID 1000
+
+/*
+ * Test permission bits deny access.
+ * Owner should be denied file creation when directory mode is 0555.
+ */
+static void test_deny_by_mode(void)
+{
+	tst_res(TINFO, "Testing permission bits deny access");
+	reset_test_path();
+
+	SAFE_CHOWN(TESTDIR, TEST_UID, TEST_GID);
+	SAFE_CHMOD(TESTDIR, 0555);
+
+	try_create_as(TEST_UID, TEST_GID, 0644, EACCES);
+}
+
+/*
+ * Test ACL_USER_OBJ grants access.
+ * Setting ACL_USER_OBJ=rwx should restore owner write permission and
+ * allow file creation after the restrictive mode baseline.
+ */
+static void test_grant_by_acl(void)
+{
+	struct acl *acl;
+
+	tst_res(TINFO, "Testing ACL_USER_OBJ grants access");
+	reset_test_path();
+
+	SAFE_CHOWN(TESTDIR, TEST_UID, TEST_GID);
+	SAFE_CHMOD(TESTDIR, 0555);
+
+	acl = acl_init();
+
+	acl_add_entry(acl, ACL_USER_OBJ,
+		      ACL_READ | ACL_WRITE | ACL_EXECUTE, 0);
+	acl_add_entry(acl, ACL_GROUP_OBJ, 0, 0);
+	acl_add_entry(acl, ACL_OTHER, 0, 0);
+
+	if (acl_set_file(TESTDIR, ACL_TYPE_ACCESS, acl) < 0) {
+		if (errno == EOPNOTSUPP) {
+			acl_free(acl);
+			tst_brk(TCONF | TERRNO, "ACL not supported");
+		}
+		acl_free(acl);
+		tst_brk(TBROK | TERRNO, "ACL setup failed");
+	}
+
+	acl_free(acl);
+
+	try_create_as(TEST_UID, TEST_GID, 0644, 0);
+
+	cleanup_testfile();
+}
+
+static void run(unsigned int n)
+{
+	switch (n) {
+	case 0:
+		test_deny_by_mode();
+		break;
+	case 1:
+		test_grant_by_acl();
+		break;
+	}
+}
+
+static void setup(void)
+{
+	reset_test_path();
+}
+
+static void cleanup(void)
+{
+	cleanup_test_paths();
+}
+
+static struct tst_test test = {
+	.test = run,
+	.tcnt = 2,
+	.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"},
+		{}
+	}
+};
+
+#else
+	TST_TEST_TCONF("sys/xattr.h is not available");
+#endif
-- 
2.39.1


-- 
Mailing list info: https://lists.linux.it/listinfo/ltp

^ permalink raw reply related	[flat|nested] 23+ messages in thread
* [LTP] [PATCH v6 1/8] fs/acl: Add ACL_USER_OBJ permission test
@ 2026-06-12 10:24 Sachin Sant
  2026-06-12 12:27 ` [LTP] " linuxtestproject.agent
  0 siblings, 1 reply; 23+ messages in thread
From: Sachin Sant @ 2026-06-12 10:24 UTC (permalink / raw)
  To: ltp

Add acl_user_obj01 test to validate ACL_USER_OBJ permissions:
- Owner permissions correctly control file/directory access
- ACL_USER_OBJ=rwx via setxattr() overrides chmod restrictions
- Owner permissions work independently of group/other permissions
- Tests use arbitrary UIDs without requiring actual user creation

The patch also adds acl_lib.h containing shared helpers for ACL
manipulation via xattr API, including:
- ACL structure management (acl_init, acl_free, acl_add_entry)
- ACL serialization/deserialization for kernel xattr format
- ACL get/set operations using getxattr/setxattr
- permission testing and file operations
- Support for both ACCESS and DEFAULT ACL types

The implementation uses direct xattr API (getxattr/setxattr) to
test kernel ACL behavior directly. Tests run on ext2/3/4,
XFS, and Btrfs filesystems with ACL support.

Suggested-by: Cyril Hrubis <chrubis@suse.cz>
Signed-off-by: Sachin Sant <sachinp@linux.ibm.com>
---
V6 changes:
- Added HAVE_SYS_XATTR_H guards for systems without xattr support
- Removed unnecessary error paths and manual status checking
- Uses SAFE_MALLOC() and new helper macros
- Added acl_type_to_name() helper to eliminate duplication
- v5 link https://lore.kernel.org/ltp/20260608092200.92827-1-sachinp@linux.ibm.com/T/#t

V5 changes:
- Switch to kernel only test validation to remove dependency on libacl
  and useradd/del commands.
- v4 link https://lore.kernel.org/ltp/20260604065417.25924-1-sachinp@linux.ibm.com/T/#t

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/20260603140147.50738-1-sachinp@linux.ibm.com/T/#t

V3 changes:
- Updated copyright header as per LTP format.
- v2 link https://lore.kernel.org/ltp/20260603065744.47106-1-sachinp@linux.ibm.com/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/20260602121958.27494-1-sachinp@linux.ibm.com/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/477836fd-80c8-4168-bfe6-00b374bb2534@linux.ibm.com/T/#t

---
 runtest/fs                               |   3 +
 testcases/kernel/fs/acl/.gitignore       |   1 +
 testcases/kernel/fs/acl/Makefile         |   8 +
 testcases/kernel/fs/acl/acl_lib.h        | 518 +++++++++++++++++++++++
 testcases/kernel/fs/acl/acl_user_obj01.c | 126 ++++++
 5 files changed, 656 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..2d9cba46d
--- /dev/null
+++ b/testcases/kernel/fs/acl/Makefile
@@ -0,0 +1,8 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# Copyright (c) 2026 IBM
+
+top_srcdir		?= ../../../..
+
+include $(top_srcdir)/include/mk/testcases.mk
+
+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..d57662a93
--- /dev/null
+++ b/testcases/kernel/fs/acl/acl_lib.h
@@ -0,0 +1,518 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (c) 2026 IBM
+ * Original shell test by Kai Zhao (ltcd3@cn.ibm.com)
+ * Converted to C by Sachin Sant <sachinp@linux.ibm.com>
+ *
+ * Common library for ACL and extended attribute tests using xattr API
+ */
+
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE
+#endif
+
+#ifndef ACL_LIB_H
+#define ACL_LIB_H
+
+#include "config.h"
+
+#include <pwd.h>
+#include <grp.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <string.h>
+#include <stdint.h>
+#include <endian.h>
+#include <sys/fsuid.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+
+#ifdef HAVE_SYS_XATTR_H
+#include <sys/xattr.h>
+#endif
+#include "tst_test.h"
+#include "tst_safe_stdio.h"
+#include "tst_capability.h"
+
+#define MNTPOINT	"mntpoint"
+#define TESTDIR		MNTPOINT "/testdir"
+#define TESTFILE	TESTDIR "/testfile"
+#define TESTSYMLINK	TESTDIR "/testsymlink"
+#define XATTR_BACKUP_FILE MNTPOINT "/xattr_backup.txt"
+
+/* 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
+
+/*
+ * POSIX ACL xattr format definitions
+ * These match the kernel's internal representation
+ */
+#define POSIX_ACL_XATTR_VERSION	0x0002
+
+/* ACL entry tag types */
+#define ACL_UNDEFINED_TAG	0x00
+#define ACL_USER_OBJ		0x01
+#define ACL_USER		0x02
+#define ACL_GROUP_OBJ		0x04
+#define ACL_GROUP		0x08
+#define ACL_MASK		0x10
+#define ACL_OTHER		0x20
+
+/* ACL permissions */
+#define ACL_READ		0x04
+#define ACL_WRITE		0x02
+#define ACL_EXECUTE		0x01
+
+/* ACL xattr names */
+#define XATTR_NAME_POSIX_ACL_ACCESS	"system.posix_acl_access"
+#define XATTR_NAME_POSIX_ACL_DEFAULT	"system.posix_acl_default"
+
+/* ACL type for set/get operations */
+#define ACL_TYPE_ACCESS		1
+#define ACL_TYPE_DEFAULT	2
+
+/* Convert host to little-endian */
+#if __BYTE_ORDER == __LITTLE_ENDIAN
+#define cpu_to_le16(x) (x)
+#define cpu_to_le32(x) (x)
+#define le16_to_cpu(x) (x)
+#define le32_to_cpu(x) (x)
+#else
+#define cpu_to_le16(x) __builtin_bswap16(x)
+#define cpu_to_le32(x) __builtin_bswap32(x)
+#define le16_to_cpu(x) __builtin_bswap16(x)
+#define le32_to_cpu(x) __builtin_bswap32(x)
+#endif
+
+/*
+ * POSIX ACL xattr format as stored in kernel
+ * This is the on-disk/in-xattr representation
+ */
+struct posix_acl_xattr_header {
+	uint32_t a_version;
+};
+
+struct posix_acl_xattr_entry {
+	uint16_t e_tag;
+	uint16_t e_perm;
+	uint32_t e_id;
+};
+
+/*
+ * Combined xattr structure for ACL operations
+ */
+struct acl_xattr {
+	struct posix_acl_xattr_header header;
+	struct posix_acl_xattr_entry entries[];
+};
+
+/*
+ * In-memory ACL representation for building ACLs
+ */
+#define MAX_ACL_ENTRIES 32
+
+struct acl_entry {
+	uint16_t tag;
+	uint16_t perm;
+	uint32_t id;
+};
+
+struct acl {
+	int count;
+	struct acl_entry entries[MAX_ACL_ENTRIES];
+};
+
+/*
+ * SAFE_TRY_UNLINK - unlink a file, ignoring ENOENT
+ * Won't TBROK if file doesn't exist
+ */
+#define SAFE_TRY_UNLINK(path) do { \
+	if (unlink(path) == -1 && errno != ENOENT) \
+		tst_brk(TBROK | TERRNO, "unlink(%s) failed", path); \
+} while (0)
+
+/*
+ * SAFE_TRY_RMDIR - remove a directory, ignoring ENOENT
+ * Won't TBROK if directory doesn't exist
+ */
+#define SAFE_TRY_RMDIR(path) do { \
+	if (rmdir(path) == -1 && errno != ENOENT) \
+		tst_brk(TBROK | TERRNO, "rmdir(%s) failed", path); \
+} while (0)
+
+static inline void reset_test_path_no_chown(void)
+{
+	SAFE_TRY_UNLINK(TESTSYMLINK);
+	SAFE_TRY_UNLINK(TESTFILE);
+	SAFE_TRY_RMDIR(TESTDIR);
+
+	SAFE_MKDIR(TESTDIR, 0755);
+}
+
+static inline void reset_test_path(void)
+{
+	reset_test_path_no_chown();
+}
+
+static inline void cleanup_testfile(void)
+{
+	SAFE_TRY_UNLINK(TESTFILE);
+}
+
+#ifdef HAVE_SYS_XATTR_H
+
+/*
+ * Initialize an empty ACL structure
+ */
+static inline struct acl *acl_init(void)
+{
+	struct acl *acl = SAFE_MALLOC(sizeof(struct acl));
+
+	acl->count = 0;
+	return acl;
+}
+
+/*
+ * Free an ACL structure
+ */
+static inline void acl_free(struct acl *acl)
+{
+	free(acl);
+}
+
+/*
+ * Add an ACL entry to the ACL structure
+ */
+static inline int acl_add_entry(struct acl *acl, uint16_t tag, uint16_t perm,
+				uint32_t id)
+{
+	if (acl->count >= MAX_ACL_ENTRIES) {
+		errno = ENOMEM;
+		return -1;
+	}
+
+	acl->entries[acl->count].tag = tag;
+	acl->entries[acl->count].perm = perm;
+	acl->entries[acl->count].id = id;
+	acl->count++;
+	return 0;
+}
+
+/*
+ * Convert ACL type to xattr name
+ */
+static inline const char *acl_type_to_name(int type)
+{
+	if (type == ACL_TYPE_ACCESS)
+		return XATTR_NAME_POSIX_ACL_ACCESS;
+	else if (type == ACL_TYPE_DEFAULT)
+		return XATTR_NAME_POSIX_ACL_DEFAULT;
+	else
+		return NULL;
+}
+
+/*
+ * Set ACL on a file using xattr.
+ *
+ * The kernel stores access ACLs only when they differ from the file mode.
+ * If the ACL is equivalent to st_mode, the xattr is removed and future
+ * getxattr() calls return ENODATA. Mirror libacl semantics by treating
+ * ENODATA as a valid minimal ACL derived from st_mode.
+ */
+static inline int acl_set_file(const char *path, int type, struct acl *acl)
+{
+	const char *xattr_name;
+	size_t size;
+	char *buf;
+	struct posix_acl_xattr_header *header;
+	struct posix_acl_xattr_entry *entries;
+	int i, ret;
+
+	xattr_name = acl_type_to_name(type);
+	if (!xattr_name) {
+		errno = EINVAL;
+		return -1;
+	}
+
+	size = sizeof(struct posix_acl_xattr_header) +
+	       acl->count * sizeof(struct posix_acl_xattr_entry);
+
+	buf = malloc(size);
+	if (!buf)
+		return -1;
+
+	header = (struct posix_acl_xattr_header *)buf;
+	header->a_version = cpu_to_le32(POSIX_ACL_XATTR_VERSION);
+
+	entries = (struct posix_acl_xattr_entry *)(buf + sizeof(*header));
+
+	for (i = 0; i < acl->count; i++) {
+		entries[i].e_tag = cpu_to_le16(acl->entries[i].tag);
+		entries[i].e_perm = cpu_to_le16(acl->entries[i].perm);
+		entries[i].e_id = cpu_to_le32(acl->entries[i].id);
+	}
+
+	ret = setxattr(path, xattr_name, buf, size, 0);
+	free(buf);
+
+	return ret;
+}
+
+static inline void acl_add_mode_entries(struct acl *acl, mode_t mode)
+{
+	acl_add_entry(acl, ACL_USER_OBJ, (mode >> 6) & 07, 0);
+	acl_add_entry(acl, ACL_GROUP_OBJ, (mode >> 3) & 07, 0);
+	acl_add_entry(acl, ACL_OTHER, mode & 07, 0);
+}
+
+/*
+ * Synthesize an ACL from file mode bits.
+ * Used when no xattr exists for an access ACL.
+ */
+static inline struct acl *acl_from_mode(const char *path)
+{
+	struct acl *acl;
+	struct stat st;
+
+	if (stat(path, &st) < 0)
+		return NULL;
+
+	acl = acl_init();
+	acl_add_mode_entries(acl, st.st_mode);
+
+	return acl;
+}
+
+/*
+ * Get ACL from a file using xattr.
+ *
+ * Access ACLs equivalent to file mode may not have a backing xattr at all.
+ * In that case synthesize the base ACL from st_mode so callers observe the
+ * same behavior as acl_get_file(3).
+ */
+static inline struct acl *acl_get_file(const char *path, int type)
+{
+	const char *xattr_name;
+	ssize_t size;
+	struct acl_xattr *ax;
+	struct acl *acl;
+	int i, count;
+
+	xattr_name = acl_type_to_name(type);
+	if (!xattr_name) {
+		errno = EINVAL;
+		return NULL;
+	}
+
+	size = getxattr(path, xattr_name, NULL, 0);
+	if (size < 0) {
+		if (errno != ENODATA || type != ACL_TYPE_ACCESS)
+			return NULL;
+
+		return acl_from_mode(path);
+	}
+
+	/* Handle race: xattr removed between size check and actual read */
+	if (size == 0)
+		return acl_from_mode(path);
+
+	ax = malloc(size);
+	if (!ax)
+		return NULL;
+
+	size = getxattr(path, xattr_name, ax, size);
+	if (size < 0) {
+		free(ax);
+		/* Handle race: xattr removed between size check and read */
+		if (errno == ENODATA && type == ACL_TYPE_ACCESS)
+			return acl_from_mode(path);
+		return NULL;
+	}
+
+	if (le32_to_cpu(ax->header.a_version) != POSIX_ACL_XATTR_VERSION) {
+		free(ax);
+		errno = EINVAL;
+		return NULL;
+	}
+
+	count = (size - sizeof(ax->header)) /
+		sizeof(struct posix_acl_xattr_entry);
+
+	acl = acl_init();
+	if (!acl) {
+		free(ax);
+		return NULL;
+	}
+
+	for (i = 0; i < count; i++) {
+		uint16_t tag = le16_to_cpu(ax->entries[i].e_tag);
+		uint16_t perm = le16_to_cpu(ax->entries[i].e_perm);
+		uint32_t id = le32_to_cpu(ax->entries[i].e_id);
+
+		if (acl_add_entry(acl, tag, perm, id) < 0) {
+			acl_free(acl);
+			free(ax);
+			return NULL;
+		}
+	}
+
+	free(ax);
+	return acl;
+}
+
+/*
+ * Check if an ACL entry has a specific permission
+ */
+static inline int acl_entry_has_perm(struct acl_entry *entry, uint16_t perm)
+{
+	return (entry->perm & perm) == perm;
+}
+
+/*
+ * Check if an ACL entry has all rwx permissions
+ */
+static inline int acl_entry_has_rwx(struct acl_entry *entry)
+{
+	return acl_entry_has_perm(entry,
+				  ACL_READ | ACL_WRITE | ACL_EXECUTE);
+}
+
+/*
+ * Find an ACL entry by tag type
+ */
+static inline struct acl_entry *acl_find_entry(struct acl *acl, uint16_t tag,
+					       uint32_t id)
+{
+	int i;
+
+	for (i = 0; i < acl->count; i++) {
+		if (acl->entries[i].tag == tag) {
+			if (tag == ACL_USER || tag == ACL_GROUP) {
+				if (acl->entries[i].id == id)
+					return &acl->entries[i];
+			} else {
+				return &acl->entries[i];
+			}
+		}
+	}
+
+	return NULL;
+}
+
+/*
+ * Update ACL mask permissions
+ */
+static inline int acl_set_mask_perms(struct acl *acl, uint16_t perm)
+{
+	struct acl_entry *mask_entry = acl_find_entry(acl, ACL_MASK, 0);
+
+	if (!mask_entry) {
+		errno = ENOENT;
+		return -1;
+	}
+
+	mask_entry->perm = perm;
+	return 0;
+}
+
+static inline void create_file_as(uid_t uid, gid_t gid, mode_t mode,
+				   int use_umask, mode_t mask, int exp_errno)
+{
+	pid_t pid;
+
+	pid = SAFE_FORK();
+	if (!pid) {
+		uid_t fsuid;
+		gid_t fsgid;
+		struct tst_cap_user_header hdr = {
+			.version = 0x20080522,
+			.pid = 0,
+		};
+		struct tst_cap_user_data data[2] = {};
+
+		SAFE_SETGROUPS(0, NULL);
+
+		if (setresgid(gid, gid, gid) < 0)
+			tst_brk(TBROK | TERRNO, "setresgid(%u) failed",
+				(unsigned int)gid);
+
+		if (setresuid(uid, uid, uid) < 0)
+			tst_brk(TBROK | TERRNO, "setresuid(%u) failed",
+				(unsigned int)uid);
+
+		/* Drop all capabilities to ensure DAC checks are enforced */
+		if (tst_capset(&hdr, data) < 0)
+			tst_brk(TBROK | TERRNO, "capset failed");
+
+		/*
+		 * setfsuid()/setfsgid() return the previous value, not -1 on
+		 * failure. Verify the effective filesystem credentials by
+		 * reading them back.
+		 */
+		setfsuid(uid);
+		fsuid = setfsuid((uid_t)-1);
+		if (fsuid != uid)
+			tst_brk(TBROK,
+				"setfsuid verification failed, expected %u got %u",
+				(unsigned int)uid, (unsigned int)fsuid);
+
+		setfsgid(gid);
+		fsgid = setfsgid((gid_t)-1);
+		if (fsgid != gid)
+			tst_brk(TBROK,
+				"setfsgid verification failed, expected %u got %u",
+				(unsigned int)gid, (unsigned int)fsgid);
+
+		if (use_umask)
+			umask(mask);
+
+		if (exp_errno) {
+			TST_EXP_FAIL(open(TESTFILE, O_CREAT | O_WRONLY, mode),
+				     exp_errno, "open(%s)", TESTFILE);
+		} else {
+			TST_EXP_FD(open(TESTFILE, O_CREAT | O_WRONLY, mode),
+				   "open(%s)", TESTFILE);
+			if (TST_RET >= 0)
+				SAFE_CLOSE(TST_RET);
+		}
+
+		exit(0);
+	}
+
+	tst_reap_children();
+}
+
+static inline void try_create_as(uid_t uid, gid_t gid, mode_t mode,
+				  int exp_errno)
+{
+	create_file_as(uid, gid, mode, 0, 0, exp_errno);
+}
+
+static inline void create_with_umask_as(uid_t uid, gid_t gid, mode_t mode,
+					 mode_t mask, int exp_errno)
+{
+	create_file_as(uid, gid, mode, 1, mask, exp_errno);
+}
+
+static inline void cleanup_test_paths(void)
+{
+	SAFE_TRY_UNLINK(TESTSYMLINK);
+	SAFE_TRY_UNLINK(TESTFILE);
+	SAFE_TRY_RMDIR(TESTDIR);
+}
+
+#endif /* HAVE_SYS_XATTR_H */
+
+#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..f82950a2b
--- /dev/null
+++ b/testcases/kernel/fs/acl/acl_user_obj01.c
@@ -0,0 +1,126 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (c) 2026 IBM
+ *
+ * Original shell test by Kai Zhao (ltcd3@cn.ibm.com)
+ * Converted to C by Sachin Sant <sachinp@linux.ibm.com>
+ */
+
+/*\
+ * Test ACL_USER_OBJ permissions using direct xattr manipulation.
+ *
+ * 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 setxattr() overrides a previous
+ *   chmod restriction
+ * - Owner permissions work independently of group and other permissions
+ *
+ * This test uses arbitrary UIDs without creating actual users, testing
+ * only the kernel ACL implementation.
+ */
+
+#include "acl_lib.h"
+
+#ifdef HAVE_SYS_XATTR_H
+
+#define TEST_UID 1000
+#define TEST_GID 1000
+
+/*
+ * Test permission bits deny access.
+ * Owner should be denied file creation when directory mode is 0555.
+ */
+static void test_deny_by_mode(void)
+{
+	tst_res(TINFO, "Testing permission bits deny access");
+	reset_test_path();
+
+	SAFE_CHOWN(TESTDIR, TEST_UID, TEST_GID);
+	SAFE_CHMOD(TESTDIR, 0555);
+
+	try_create_as(TEST_UID, TEST_GID, 0644, EACCES);
+}
+
+/*
+ * Test ACL_USER_OBJ grants access.
+ * Setting ACL_USER_OBJ=rwx should restore owner write permission and
+ * allow file creation after the restrictive mode baseline.
+ */
+static void test_grant_by_acl(void)
+{
+	struct acl *acl;
+
+	tst_res(TINFO, "Testing ACL_USER_OBJ grants access");
+	reset_test_path();
+
+	SAFE_CHOWN(TESTDIR, TEST_UID, TEST_GID);
+	SAFE_CHMOD(TESTDIR, 0555);
+
+	acl = acl_init();
+
+	acl_add_entry(acl, ACL_USER_OBJ,
+		      ACL_READ | ACL_WRITE | ACL_EXECUTE, 0);
+	acl_add_entry(acl, ACL_GROUP_OBJ, 0, 0);
+	acl_add_entry(acl, ACL_OTHER, 0, 0);
+
+	if (acl_set_file(TESTDIR, ACL_TYPE_ACCESS, acl) < 0) {
+		if (errno == EOPNOTSUPP) {
+			acl_free(acl);
+			tst_brk(TCONF | TERRNO, "ACL not supported");
+		}
+		acl_free(acl);
+		tst_brk(TBROK | TERRNO, "ACL setup failed");
+	}
+
+	acl_free(acl);
+
+	try_create_as(TEST_UID, TEST_GID, 0644, 0);
+
+	cleanup_testfile();
+}
+
+static void run(unsigned int n)
+{
+	switch (n) {
+	case 0:
+		test_deny_by_mode();
+		break;
+	case 1:
+		test_grant_by_acl();
+		break;
+	}
+}
+
+static void setup(void)
+{
+	reset_test_path();
+}
+
+static void cleanup(void)
+{
+	cleanup_test_paths();
+}
+
+static struct tst_test test = {
+	.test = run,
+	.tcnt = 2,
+	.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"},
+		{}
+	}
+};
+
+#else
+	TST_TEST_TCONF("sys/xattr.h is not available");
+#endif
-- 
2.39.1


-- 
Mailing list info: https://lists.linux.it/listinfo/ltp

^ permalink raw reply related	[flat|nested] 23+ messages in thread
* [LTP] [PATCH v5 1/8] fs/acl: Add ACL_USER_OBJ permission test
@ 2026-06-08  9:21 Sachin Sant
  2026-06-08 11:01 ` [LTP] " linuxtestproject.agent
  0 siblings, 1 reply; 23+ messages in thread
From: Sachin Sant @ 2026-06-08  9:21 UTC (permalink / raw)
  To: ltp

Add acl_user_obj01 test to validate ACL_USER_OBJ permissions:
- Owner permissions correctly control file/directory access
- ACL_USER_OBJ=rwx via setxattr() overrides chmod restrictions
- Owner permissions work independently of group/other permissions
- Tests use arbitrary UIDs without requiring actual user creation

The patch also adds acl_lib.h containing shared helpers for ACL
manipulation via xattr API, including:
- ACL structure management (acl_init, acl_free, acl_add_entry)
- ACL serialization/deserialization for kernel xattr format
- ACL get/set operations using getxattr/setxattr
- permission testing and file operations
- Support for both ACCESS and DEFAULT ACL types

The implementation uses direct xattr API (getxattr/setxattr) to
test kernel ACL behavior directly. Tests run on ext2/3/4,
XFS, and Btrfs filesystems with ACL support.

Suggested-by: Cyril Hrubis <chrubis@suse.cz>
Signed-off-by: Sachin Sant <sachinp@linux.ibm.com>
---
V5 changes:
- Switch to kernel only test validation to remove dependency on libacl
  and useradd/del commands.
- v4 link https://lore.kernel.org/ltp/20260604065417.25924-1-sachinp@linux.ibm.com/T/#t

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/20260603140147.50738-1-sachinp@linux.ibm.com/T/#t

V3 changes:
- Updated copyright header as per LTP format.
- v2 link https://lore.kernel.org/ltp/20260603065744.47106-1-sachinp@linux.ibm.com/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/20260602121958.27494-1-sachinp@linux.ibm.com/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/477836fd-80c8-4168-bfe6-00b374bb2534@linux.ibm.com/T/#t

---
 runtest/fs                               |   3 +
 testcases/kernel/fs/acl/.gitignore       |   1 +
 testcases/kernel/fs/acl/Makefile         |   8 +
 testcases/kernel/fs/acl/acl_lib.h        | 483 +++++++++++++++++++++++
 testcases/kernel/fs/acl/acl_user_obj01.c | 154 ++++++++
 5 files changed, 649 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..2d9cba46d
--- /dev/null
+++ b/testcases/kernel/fs/acl/Makefile
@@ -0,0 +1,8 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# Copyright (c) 2026 IBM
+
+top_srcdir		?= ../../../..
+
+include $(top_srcdir)/include/mk/testcases.mk
+
+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..717c9ff1e
--- /dev/null
+++ b/testcases/kernel/fs/acl/acl_lib.h
@@ -0,0 +1,483 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (c) 2026 IBM
+ * Original shell test by Kai Zhao (ltcd3@cn.ibm.com)
+ * Converted to C by Sachin Sant <sachinp@linux.ibm.com>
+ *
+ * Common library for ACL and extended attribute tests using xattr API
+ */
+
+#ifndef ACL_LIB_H
+#define ACL_LIB_H
+
+#include <pwd.h>
+#include <grp.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <string.h>
+#include <stdint.h>
+#include <endian.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"
+
+#define MNTPOINT	"mntpoint"
+#define TESTDIR		MNTPOINT "/testdir"
+#define TESTFILE	TESTDIR "/testfile"
+#define TESTSYMLINK	TESTDIR "/testsymlink"
+#define XATTR_BACKUP_FILE MNTPOINT "/xattr_backup.txt"
+
+/* 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
+
+/*
+ * POSIX ACL xattr format definitions
+ * These match the kernel's internal representation
+ */
+#define POSIX_ACL_XATTR_VERSION	0x0002
+
+/* ACL entry tag types */
+#define ACL_UNDEFINED_TAG	0x00
+#define ACL_USER_OBJ		0x01
+#define ACL_USER		0x02
+#define ACL_GROUP_OBJ		0x04
+#define ACL_GROUP		0x08
+#define ACL_MASK		0x10
+#define ACL_OTHER		0x20
+
+/* ACL permissions */
+#define ACL_READ		0x04
+#define ACL_WRITE		0x02
+#define ACL_EXECUTE		0x01
+
+/* ACL xattr names */
+#define XATTR_NAME_POSIX_ACL_ACCESS	"system.posix_acl_access"
+#define XATTR_NAME_POSIX_ACL_DEFAULT	"system.posix_acl_default"
+
+/* ACL type for set/get operations */
+#define ACL_TYPE_ACCESS		1
+#define ACL_TYPE_DEFAULT	2
+
+/* Convert host to little-endian */
+#if __BYTE_ORDER == __LITTLE_ENDIAN
+#define cpu_to_le16(x) (x)
+#define cpu_to_le32(x) (x)
+#define le16_to_cpu(x) (x)
+#define le32_to_cpu(x) (x)
+#else
+#define cpu_to_le16(x) __builtin_bswap16(x)
+#define cpu_to_le32(x) __builtin_bswap32(x)
+#define le16_to_cpu(x) __builtin_bswap16(x)
+#define le32_to_cpu(x) __builtin_bswap32(x)
+#endif
+
+/*
+ * POSIX ACL xattr format as stored in kernel
+ * This is the on-disk/in-xattr representation
+ */
+struct posix_acl_xattr_header {
+	uint32_t a_version;
+};
+
+struct posix_acl_xattr_entry {
+	uint16_t e_tag;
+	uint16_t e_perm;
+	uint32_t e_id;
+};
+
+/*
+ * In-memory ACL representation for building ACLs
+ */
+#define MAX_ACL_ENTRIES 32
+
+struct acl_entry {
+	uint16_t tag;
+	uint16_t perm;
+	uint32_t id;
+};
+
+struct acl {
+	int count;
+	struct acl_entry entries[MAX_ACL_ENTRIES];
+};
+
+/* 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();
+}
+
+static inline void cleanup_testfile(void)
+{
+	if (unlink(TESTFILE) == -1 && errno != ENOENT)
+		tst_res(TWARN | TERRNO, "unlink(%s) failed", TESTFILE);
+}
+
+/*
+ * Initialize an empty ACL structure
+ */
+static inline struct acl *acl_init(void)
+{
+	struct acl *acl = malloc(sizeof(struct acl));
+
+	if (!acl)
+		return NULL;
+
+	acl->count = 0;
+	return acl;
+}
+
+/*
+ * Free an ACL structure
+ */
+static inline void acl_free(struct acl *acl)
+{
+	free(acl);
+}
+
+/*
+ * Add an ACL entry to the ACL structure
+ */
+static inline int acl_add_entry(struct acl *acl, uint16_t tag, uint16_t perm,
+				uint32_t id)
+{
+	if (acl->count >= MAX_ACL_ENTRIES) {
+		errno = ENOMEM;
+		return -1;
+	}
+
+	acl->entries[acl->count].tag = tag;
+	acl->entries[acl->count].perm = perm;
+	acl->entries[acl->count].id = id;
+	acl->count++;
+
+	return 0;
+}
+
+/*
+ * Set ACL on a file using xattr.
+ *
+ * The kernel stores access ACLs only when they differ from the file mode.
+ * If the ACL is equivalent to st_mode, the xattr is removed and future
+ * getxattr() calls return ENODATA. Mirror libacl semantics by treating
+ * ENODATA as a valid minimal ACL derived from st_mode.
+ */
+static inline int acl_set_file(const char *path, int type, struct acl *acl)
+{
+	const char *xattr_name;
+	size_t size;
+	char *buf;
+	struct posix_acl_xattr_header *header;
+	struct posix_acl_xattr_entry *entries;
+	int i, ret;
+
+	if (type == ACL_TYPE_ACCESS)
+		xattr_name = XATTR_NAME_POSIX_ACL_ACCESS;
+	else if (type == ACL_TYPE_DEFAULT)
+		xattr_name = XATTR_NAME_POSIX_ACL_DEFAULT;
+	else {
+		errno = EINVAL;
+		return -1;
+	}
+
+	size = sizeof(struct posix_acl_xattr_header) +
+	       acl->count * sizeof(struct posix_acl_xattr_entry);
+
+	buf = malloc(size);
+	if (!buf)
+		return -1;
+
+	header = (struct posix_acl_xattr_header *)buf;
+	header->a_version = cpu_to_le32(POSIX_ACL_XATTR_VERSION);
+
+	entries = (struct posix_acl_xattr_entry *)(buf + sizeof(*header));
+
+	for (i = 0; i < acl->count; i++) {
+		entries[i].e_tag = cpu_to_le16(acl->entries[i].tag);
+		entries[i].e_perm = cpu_to_le16(acl->entries[i].perm);
+		entries[i].e_id = cpu_to_le32(acl->entries[i].id);
+	}
+
+	ret = setxattr(path, xattr_name, buf, size, 0);
+	free(buf);
+
+	return ret;
+}
+
+static inline int acl_add_mode_entries(struct acl *acl, mode_t mode)
+{
+	if (acl_add_entry(acl, ACL_USER_OBJ, (mode >> 6) & 07, 0) < 0)
+		return -1;
+
+	if (acl_add_entry(acl, ACL_GROUP_OBJ, (mode >> 3) & 07, 0) < 0)
+		return -1;
+
+	if (acl_add_entry(acl, ACL_OTHER, mode & 07, 0) < 0)
+		return -1;
+
+	return 0;
+}
+
+/*
+ * Synthesize an ACL from file mode bits.
+ * Used when no xattr exists for an access ACL.
+ */
+static inline struct acl *acl_from_mode(const char *path)
+{
+	struct acl *acl;
+	struct stat st;
+
+	if (stat(path, &st) < 0)
+		return NULL;
+
+	acl = acl_init();
+	if (!acl)
+		return NULL;
+
+	if (acl_add_mode_entries(acl, st.st_mode) < 0) {
+		acl_free(acl);
+		return NULL;
+	}
+
+	return acl;
+}
+
+/*
+ * Get ACL from a file using xattr.
+ *
+ * Access ACLs equivalent to file mode may not have a backing xattr at all.
+ * In that case synthesize the base ACL from st_mode so callers observe the
+ * same behavior as acl_get_file(3).
+ */
+static inline struct acl *acl_get_file(const char *path, int type)
+{
+	const char *xattr_name;
+	ssize_t size;
+	char *buf;
+	struct posix_acl_xattr_header *header;
+	struct posix_acl_xattr_entry *entries;
+	struct acl *acl;
+	int i, count;
+
+	if (type == ACL_TYPE_ACCESS)
+		xattr_name = XATTR_NAME_POSIX_ACL_ACCESS;
+	else if (type == ACL_TYPE_DEFAULT)
+		xattr_name = XATTR_NAME_POSIX_ACL_DEFAULT;
+	else {
+		errno = EINVAL;
+		return NULL;
+	}
+
+	size = getxattr(path, xattr_name, NULL, 0);
+	if (size < 0) {
+		if (errno != ENODATA || type != ACL_TYPE_ACCESS)
+			return NULL;
+
+		return acl_from_mode(path);
+	}
+
+	/* Handle race: xattr removed between size check and actual read */
+	if (size == 0)
+		return acl_from_mode(path);
+
+	buf = malloc(size);
+	if (!buf)
+		return NULL;
+
+	size = getxattr(path, xattr_name, buf, size);
+	if (size < 0) {
+		free(buf);
+		/* Handle race: xattr removed between size check and read */
+		if (errno == ENODATA && type == ACL_TYPE_ACCESS)
+			return acl_from_mode(path);
+		return NULL;
+	}
+
+	header = (struct posix_acl_xattr_header *)buf;
+	if (le32_to_cpu(header->a_version) != POSIX_ACL_XATTR_VERSION) {
+		free(buf);
+		errno = EINVAL;
+		return NULL;
+	}
+
+	count = (size - sizeof(*header)) / sizeof(struct posix_acl_xattr_entry);
+	entries = (struct posix_acl_xattr_entry *)(buf + sizeof(*header));
+
+	acl = acl_init();
+	if (!acl) {
+		free(buf);
+		return NULL;
+	}
+
+	for (i = 0; i < count; i++) {
+		uint16_t tag = le16_to_cpu(entries[i].e_tag);
+		uint16_t perm = le16_to_cpu(entries[i].e_perm);
+		uint32_t id = le32_to_cpu(entries[i].e_id);
+
+		if (acl_add_entry(acl, tag, perm, id) < 0) {
+			acl_free(acl);
+			free(buf);
+			return NULL;
+		}
+	}
+
+	free(buf);
+	return acl;
+}
+
+/*
+ * Check if an ACL entry has a specific permission
+ */
+static inline int acl_entry_has_perm(struct acl_entry *entry, uint16_t perm)
+{
+	return (entry->perm & perm) == perm;
+}
+
+/*
+ * Check if an ACL entry has all rwx permissions
+ */
+static inline int acl_entry_has_rwx(struct acl_entry *entry)
+{
+	return acl_entry_has_perm(entry, ACL_READ) &&
+	       acl_entry_has_perm(entry, ACL_WRITE) &&
+	       acl_entry_has_perm(entry, ACL_EXECUTE);
+}
+
+/*
+ * Find an ACL entry by tag type
+ */
+static inline struct acl_entry *acl_find_entry(struct acl *acl, uint16_t tag,
+					       uint32_t id)
+{
+	int i;
+
+	for (i = 0; i < acl->count; i++) {
+		if (acl->entries[i].tag == tag) {
+			if (tag == ACL_USER || tag == ACL_GROUP) {
+				if (acl->entries[i].id == id)
+					return &acl->entries[i];
+			} else {
+				return &acl->entries[i];
+			}
+		}
+	}
+
+	return NULL;
+}
+
+/*
+ * Update ACL mask permissions
+ */
+static inline int acl_set_mask_perms(struct acl *acl, uint16_t perm)
+{
+	struct acl_entry *mask_entry = acl_find_entry(acl, ACL_MASK, 0);
+
+	if (!mask_entry) {
+		errno = ENOENT;
+		return -1;
+	}
+
+	mask_entry->perm = perm;
+	return 0;
+}
+
+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 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 (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..01abfbf4e
--- /dev/null
+++ b/testcases/kernel/fs/acl/acl_user_obj01.c
@@ -0,0 +1,154 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (c) 2026 IBM
+ *
+ * Original shell test by Kai Zhao (ltcd3@cn.ibm.com)
+ * Converted to C by Sachin Sant <sachinp@linux.ibm.com>
+ */
+
+/*\
+ * Test ACL_USER_OBJ permissions using direct xattr manipulation.
+ *
+ * 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 setxattr() overrides a previous
+ *   chmod restriction
+ * - Owner permissions work independently of group and other permissions
+ *
+ * This test uses arbitrary UIDs without creating actual users, testing
+ * only the kernel ACL implementation.
+ */
+
+#include "acl_lib.h"
+
+#define TEST_UID 1000
+#define TEST_GID 1000
+
+/*
+ * Test permission bits deny access.
+ * Owner should be denied write access when mode is 0500.
+ */
+static void test_deny_by_mode(void)
+{
+	int err;
+
+	tst_res(TINFO, "Testing permission bits deny access");
+	reset_test_path();
+
+	SAFE_CHOWN(TESTDIR, TEST_UID, TEST_GID);
+	SAFE_CHMOD(TESTDIR, 0500);
+
+	err = try_create_as(TEST_UID, TEST_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 ACL_USER_OBJ grants access.
+ * Setting ACL_USER_OBJ=rwx should override previous chmod restriction.
+ */
+static void test_grant_by_acl(void)
+{
+	struct acl *acl = NULL;
+	int err;
+
+	tst_res(TINFO, "Testing ACL_USER_OBJ grants access");
+	reset_test_path();
+
+	SAFE_CHOWN(TESTDIR, TEST_UID, TEST_GID);
+	SAFE_CHMOD(TESTDIR, 0500);
+
+	acl = acl_init();
+	if (!acl)
+		tst_brk(TBROK | TERRNO, "acl_init failed");
+
+	if (acl_add_entry(acl, ACL_USER_OBJ,
+			  ACL_READ | ACL_WRITE | ACL_EXECUTE, 0) < 0)
+		goto cleanup_acl;
+
+	if (acl_add_entry(acl, ACL_GROUP_OBJ, 0, 0) < 0)
+		goto cleanup_acl;
+
+	if (acl_add_entry(acl, ACL_OTHER, 0, 0) < 0)
+		goto cleanup_acl;
+
+	if (acl_set_file(TESTDIR, ACL_TYPE_ACCESS, acl) < 0) {
+		if (errno == EOPNOTSUPP) {
+			acl_free(acl);
+			tst_brk(TCONF | TERRNO, "ACL not supported");
+		}
+		goto cleanup_acl;
+	}
+
+	acl_free(acl);
+	acl = NULL;
+
+	err = try_create_as(TEST_UID, TEST_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");
+	return;
+
+cleanup_acl:
+	acl_free(acl);
+	tst_brk(TBROK | TERRNO, "ACL setup failed");
+}
+
+static void run(unsigned int n)
+{
+	switch (n) {
+	case 0:
+		test_deny_by_mode();
+		break;
+	case 1:
+		test_grant_by_acl();
+		break;
+	}
+}
+
+static void setup(void)
+{
+	reset_test_path();
+}
+
+static void cleanup(void)
+{
+	cleanup_test_paths();
+}
+
+static struct tst_test test = {
+	.test = run,
+	.tcnt = 2,
+	.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"},
+		{}
+	}
+};
-- 
2.39.1


-- 
Mailing list info: https://lists.linux.it/listinfo/ltp

^ permalink raw reply related	[flat|nested] 23+ messages in thread

end of thread, other threads:[~2026-07-29 13:52 UTC | newest]

Thread overview: 23+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-06-16  4:54 [LTP] [PATCH v12 0/8] Convert shell-based ACL test (tacl_xattr.sh) to C Sachin Sant
2026-06-16  4:54 ` [LTP] [PATCH v12 1/8] fs/acl: Add ACL_USER_OBJ permission test Sachin Sant
2026-06-16  8:39   ` [LTP] " linuxtestproject.agent
2026-07-29 12:24   ` [LTP] [PATCH v12 1/8] " Cyril Hrubis
2026-07-29 13:52     ` Sachin Sant
2026-06-16  4:54 ` [LTP] [PATCH v12 2/8] fs/acl: Add ACL mask interaction tests Sachin Sant
2026-06-16  4:54 ` [LTP] [PATCH v12 3/8] fs/acl: Add ACL_OTHER permission test Sachin Sant
2026-06-16  4:54 ` [LTP] [PATCH v12 4/8] fs/acl: Add default ACL inheritance test Sachin Sant
2026-06-16  4:54 ` [LTP] [PATCH v12 5/8] fs/acl: Add chmod/chown ACL interaction tests Sachin Sant
2026-06-16  4:54 ` [LTP] [PATCH v12 6/8] fs/acl: Add ACL symlink operations test Sachin Sant
2026-06-16  4:54 ` [LTP] [PATCH v12 7/8] fs/acl: Add extended attributes test Sachin Sant
2026-06-16  4:54 ` [LTP] [PATCH v12 8/8] fs/acl: Remove old shell-based ACL test Sachin Sant
  -- strict thread matches above, loose matches on Subject: below --
2026-06-15 15:15 [LTP] [PATCH v11 1/8] fs/acl: Add ACL_USER_OBJ permission test Sachin Sant
2026-06-15 18:35 ` [LTP] " linuxtestproject.agent
2026-06-15  7:25 [LTP] [PATCH v10 1/8] " Sachin Sant
2026-06-15  8:33 ` [LTP] " linuxtestproject.agent
2026-06-15  9:04   ` Andrea Cervesato via ltp
2026-06-15  9:29     ` Andrea Cervesato via ltp
2026-06-15 11:25     ` Sachin Sant
2026-06-15 12:07       ` Andrea Cervesato via ltp
2026-06-15  5:29 [LTP] [PATCH v9 1/8] " Sachin Sant
2026-06-15  6:32 ` [LTP] " linuxtestproject.agent
2026-06-13  9:05 [LTP] [PATCH v8 1/8] " Sachin Sant
2026-06-13 10:36 ` [LTP] " linuxtestproject.agent
2026-06-12 17:19 [LTP] [PATCH v7 1/8] " Sachin Sant
2026-06-12 19:13 ` [LTP] " linuxtestproject.agent
2026-06-12 10:24 [LTP] [PATCH v6 1/8] " Sachin Sant
2026-06-12 12:27 ` [LTP] " linuxtestproject.agent
2026-06-08  9:21 [LTP] [PATCH v5 1/8] " Sachin Sant
2026-06-08 11:01 ` [LTP] " linuxtestproject.agent

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.