[PATCH v12 1/8] fs/acl: Add ACL_USER_OBJ permission test
Sachin Sant <[email protected]>
| Newsgroups | gmane.linux.ltp |
|---|---|
| Message-ID | <[email protected]> |
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 <[email protected]> Signed-off-by: Sachin Sant <[email protected]> --- No changes in v12 V11 changes: - Converted SAFE_TRY_* functions to inline. - v10 link https://lore.kernel.org/ltp/[email protected]/T/#t V10 changes: - No change V9 changes: - Use RST manpage formatted documentation - v8 link https://lore.kernel.org/ltp/[email protected]/T/#t V8 changes: - Removed no chown variant of reset_test_path - Use RST manpage formatted documentation - v7 link https://lore.kernel.org/ltp/[email protected]/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/[email protected]/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/[email protected]/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/[email protected]/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/[email protected]/T/#t V3 changes: - Updated copyright header as per LTP format. - v2 link https://lore.kernel.org/ltp/[email protected]/T/#t V2 changes: - Added 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/[email protected]/T/#t V1 changes: - Use ACL_LIBS variable instead of hardcoded -lacl in Makefile - Move ACL header includes inside feature guards in acl_lib.h - Use HAVE_LIBACL guards in .c code - Report TCONF when libacl is not available - rfc link https://lore.kernel.org/ltp/[email protected]/T/#t --- runtest/fs | 3 + testcases/kernel/fs/acl/.gitignore | 1 + testcases/kernel/fs/acl/Makefile | 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 ([email protected]) + * Converted to C by Sachin Sant <[email protected]> + * + * 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 ([email protected]) + * Converted to C by Sachin Sant <[email protected]> + */ + +/*\ + * 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