diff options
| author | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-04-02 18:29:12 +0300 |
|---|---|---|
| committer | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-04-02 18:29:12 +0300 |
| commit | 96be0a182e0b889a9daf43c04a0b835aa604d280 (patch) | |
| tree | 06d0746e47d5bb2b34d393d34c9aa8b4cd6c2289 /corebinutils | |
| parent | 115756d3f1b14d01db40e07035cf96069b07a320 (diff) | |
| parent | a4ca535a68ee1a8f0d906cf633344d67238d133f (diff) | |
| download | Project-Tick-96be0a182e0b889a9daf43c04a0b835aa604d280.tar.gz Project-Tick-96be0a182e0b889a9daf43c04a0b835aa604d280.zip | |
Add 'corebinutils/setfacl/' from commit 'a4ca535a68ee1a8f0d906cf633344d67238d133f'
git-subtree-dir: corebinutils/setfacl
git-subtree-mainline: 115756d3f1b14d01db40e07035cf96069b07a320
git-subtree-split: a4ca535a68ee1a8f0d906cf633344d67238d133f
Diffstat (limited to 'corebinutils')
| -rw-r--r-- | corebinutils/setfacl/.gitignore | 25 | ||||
| -rw-r--r-- | corebinutils/setfacl/GNUmakefile | 35 | ||||
| -rw-r--r-- | corebinutils/setfacl/LICENSE | 24 | ||||
| -rw-r--r-- | corebinutils/setfacl/README.md | 43 | ||||
| -rw-r--r-- | corebinutils/setfacl/setfacl.1 | 515 | ||||
| -rw-r--r-- | corebinutils/setfacl/setfacl.c | 2080 | ||||
| -rw-r--r-- | corebinutils/setfacl/tests/test.sh | 257 |
7 files changed, 2979 insertions, 0 deletions
diff --git a/corebinutils/setfacl/.gitignore b/corebinutils/setfacl/.gitignore new file mode 100644 index 0000000000..a74d30b48c --- /dev/null +++ b/corebinutils/setfacl/.gitignore @@ -0,0 +1,25 @@ +*.a +*.core +*.lo +*.nossppico +*.o +*.orig +*.pico +*.pieo +*.po +*.rej +*.so +*.so.[0-9]* +*.sw[nop] +*~ +.*DS_Store +.cache +.clangd +.ccls-cache +.depend* +compile_commands.json +compile_commands.events.json +tags +build/ +out/ +.linux-obj/ diff --git a/corebinutils/setfacl/GNUmakefile b/corebinutils/setfacl/GNUmakefile new file mode 100644 index 0000000000..d2fca64a74 --- /dev/null +++ b/corebinutils/setfacl/GNUmakefile @@ -0,0 +1,35 @@ +.DEFAULT_GOAL := all + +CC ?= cc +CPPFLAGS += -D_POSIX_C_SOURCE=200809L +CFLAGS ?= -O2 +CFLAGS += -std=c17 -g -Wall -Wextra -Wpedantic +LDFLAGS ?= +LDLIBS ?= + +OBJDIR := $(CURDIR)/build +OUTDIR := $(CURDIR)/out +TARGET := $(OUTDIR)/setfacl +OBJS := $(OBJDIR)/setfacl.o + +.PHONY: all clean dirs test status + +all: $(TARGET) + +dirs: + @mkdir -p "$(OBJDIR)" "$(OUTDIR)" + +$(TARGET): $(OBJS) | dirs + $(CC) $(LDFLAGS) -o "$@" $(OBJS) $(LDLIBS) + +$(OBJDIR)/setfacl.o: $(CURDIR)/setfacl.c | dirs + $(CC) $(CPPFLAGS) $(CFLAGS) -c "$(CURDIR)/setfacl.c" -o "$@" + +test: $(TARGET) + SETFACL_BIN="$(TARGET)" sh "$(CURDIR)/tests/test.sh" + +status: + @printf '%s\n' "$(TARGET)" + +clean: + @rm -rf "$(CURDIR)/build" "$(CURDIR)/out" "$(CURDIR)/.tmp-tests" diff --git a/corebinutils/setfacl/LICENSE b/corebinutils/setfacl/LICENSE new file mode 100644 index 0000000000..739b27676f --- /dev/null +++ b/corebinutils/setfacl/LICENSE @@ -0,0 +1,24 @@ +Copyright (c) 2001 Chris D. Faulhaber. All rights reserved. + +Copyright (c) 2026 Project Tick. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. diff --git a/corebinutils/setfacl/README.md b/corebinutils/setfacl/README.md new file mode 100644 index 0000000000..f08f55a689 --- /dev/null +++ b/corebinutils/setfacl/README.md @@ -0,0 +1,43 @@ +# setfacl + +Standalone musl-libc-friendly Linux port of FreeBSD `setfacl` for Project Tick BSD/Linux Distribution. + +## Build + +```sh +gmake -f GNUmakefile +gmake -f GNUmakefile CC=musl-gcc +``` + +## Test + +```sh +gmake -f GNUmakefile test +gmake -f GNUmakefile test CC=musl-gcc +``` + +## Port strategy + +- FreeBSD `sys/acl.h`, `acl_*_np()` helpers, and `fts(3)` traversal were removed. +- Linux access/default ACLs are decoded from and encoded to `system.posix_acl_access` / `system.posix_acl_default` with `getxattr(2)`, `setxattr(2)`, `removexattr(2)`, and their `l*` variants. +- Access ACLs without an xattr start from file mode bits; default ACLs do not get synthesized, matching the FreeBSD manpage expectation that callers create mandatory default entries explicitly. +- Base-only access ACL updates are applied with `chmod(2)` plus access-xattr cleanup so trivial ACL operations still work cleanly on filesystems that have no POSIX ACL xattr. +- Recursive walking is Linux-native `opendir(3)` / `readdir(3)` based and implements `-P`, `-L`, and `-H` without relying on GNU or glibc-only traversal APIs. + +## Supported semantics on Linux + +- POSIX access ACL modify/remove operations with `-m`, `-M`, `-x`, `-X`, `-b`, `-k`, `-d`, `-n`, `-R`, `-H`, `-L`, `-P`, and `-h`. +- `-M -` / `-X -` ACL files with comments and whitespace ignored per `setfacl(1)`. +- Path operands from standard input when no path operands are given or the sole operand is `-`. +- Explicit mask handling and recalculation that follows the FreeBSD manpage ordering model instead of silently deferring kernel-side failures. + +## Unsupported semantics on Linux + +- `-a` and NFSv4 ACL entry syntax (`owner@`, `allow`, inheritance flags, and similar) fail with an explicit error because this port targets Linux POSIX ACLs only. +- `-h` on a symbolic link fails with an explicit error because Linux does not support POSIX ACLs on symlink inodes. +- Creating a default ACL from only named entries is rejected; callers must provide the mandatory `user::`, `group::`, and `other::` entries, as documented in `setfacl.1`. + +## Notes + +- The parser is strict: malformed entries, duplicate ACL members, missing required entries, and impossible `-n` mask states are rejected before any file is modified. +- Verified with `gmake -f GNUmakefile clean test` and `gmake -f GNUmakefile clean test CC=musl-gcc`. diff --git a/corebinutils/setfacl/setfacl.1 b/corebinutils/setfacl/setfacl.1 new file mode 100644 index 0000000000..b021f85091 --- /dev/null +++ b/corebinutils/setfacl/setfacl.1 @@ -0,0 +1,515 @@ +.\"- +.\" Copyright (c) 2001 Chris D. Faulhaber +.\" Copyright (c) 2011 Edward Tomasz NapieraĆa +.\" All rights reserved. +.\" +.\" Redistribution and use in source and binary forms, with or without +.\" modification, are permitted provided that the following conditions +.\" are met: +.\" 1. Redistributions of source code must retain the above copyright +.\" notice, this list of conditions and the following disclaimer. +.\" 2. Redistributions in binary form must reproduce the above copyright +.\" notice, this list of conditions and the following disclaimer in the +.\" documentation and/or other materials provided with the distribution. +.\" +.\" THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +.\" ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +.\" IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +.\" ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +.\" FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +.\" DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +.\" OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +.\" HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +.\" LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +.\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +.\" SUCH DAMAGE. +.\" +.Dd April 29, 2023 +.Dt SETFACL 1 +.Os +.Sh NAME +.Nm setfacl +.Nd set ACL information +.Sh SYNOPSIS +.Nm +.Op Fl R Op Fl H | L | P +.Op Fl bdhkn +.Op Fl a Ar position entries +.Op Fl m Ar entries +.Op Fl M Ar file +.Op Fl x Ar entries | position +.Op Fl X Ar file +.Op Ar +.Sh DESCRIPTION +The +.Nm +utility sets discretionary access control information on +the specified file(s). +If no files are specified, or the list consists of the only +.Sq Fl , +the file names are taken from the standard input. +.Pp +The following options are available: +.Bl -tag -width indent +.It Fl a Ar position entries +Modify the ACL on the specified files by inserting new +ACL entries +specified in +.Ar entries , +starting at position +.Ar position , +counting from zero. +This option is only applicable to NFSv4 ACLs. +.It Fl b +Remove all ACL entries except for the ones synthesized +from the file mode - the three mandatory entries in case +of POSIX.1e ACL. +If the POSIX.1e ACL contains a +.Dq Li mask +entry, the permissions of the +.Dq Li group +entry in the resulting ACL will be set to the permission +associated with both the +.Dq Li group +and +.Dq Li mask +entries of the current ACL. +.It Fl d +The operations apply to the default ACL entries instead of +access ACL entries. +Currently only directories may have +default ACL's. +This option is not applicable to NFSv4 ACLs. +.It Fl h +If the target of the operation is a symbolic link, perform the operation +on the symbolic link itself, rather than following the link. +.It Fl H +If the +.Fl R +option is specified, symbolic links on the command line are followed +and hence unaffected by the command. +(Symbolic links encountered during tree traversal are not followed.) +.It Fl k +Delete any default ACL entries on the specified files. +It +is not considered an error if the specified files do not have +any default ACL entries. +An error will be reported if any of +the specified files cannot have a default entry (i.e., +non-directories). +This option is not applicable to NFSv4 ACLs. +.It Fl L +If the +.Fl R +option is specified, all symbolic links are followed. +.It Fl m Ar entries +Modify the ACL on the specified file. +New entries will be added, and existing entries will be modified +according to the +.Ar entries +argument. +For NFSv4 ACLs, it is recommended to use the +.Fl a +and +.Fl x +options instead. +.It Fl M Ar file +Modify the ACL entries on the specified files by adding new +ACL entries and modifying existing ACL entries with the ACL +entries specified in the file +.Ar file . +If +.Ar file +is +.Fl , +the input is taken from stdin. +.It Fl n +Do not recalculate the permissions associated with the ACL +mask entry. +This option is not applicable to NFSv4 ACLs. +.It Fl P +If the +.Fl R +option is specified, no symbolic links are followed. +This is the default. +.It Fl R +Perform the action recursively on any specified directories. +When modifying or adding NFSv4 ACL entries, inheritance flags +are applied only to directories. +.It Fl x Ar entries | position +If +.Ar entries +is specified, remove the ACL entries specified there +from the access or default ACL of the specified files. +Otherwise, remove entry at index +.Ar position , +counting from zero. +.It Fl X Ar file +Remove the ACL entries specified in the file +.Ar file +from the access or default ACL of the specified files. +.El +.Pp +The above options are evaluated in the order specified +on the command-line. +.Sh POSIX.1e ACL ENTRIES +A POSIX.1E ACL entry contains three colon-separated fields: +an ACL tag, an ACL qualifier, and discretionary access +permissions: +.Bl -tag -width indent +.It Ar "ACL tag" +The ACL tag specifies the ACL entry type and consists of +one of the following: +.Dq Li user +or +.Ql u +specifying the access +granted to the owner of the file or a specified user; +.Dq Li group +or +.Ql g +specifying the access granted to the file owning group +or a specified group; +.Dq Li other +or +.Ql o +specifying the access +granted to any process that does not match any user or group +ACL entry; +.Dq Li mask +or +.Ql m +specifying the maximum access +granted to any ACL entry except the +.Dq Li user +ACL entry for the file owner and the +.Dq Li other +ACL entry. +.It Ar "ACL qualifier" +The ACL qualifier field describes the user or group associated with +the ACL entry. +It may consist of one of the following: uid or +user name, gid or group name, or empty. +For +.Dq Li user +ACL entries, an empty field specifies access granted to the +file owner. +For +.Dq Li group +ACL entries, an empty field specifies access granted to the +file owning group. +.Dq Li mask +and +.Dq Li other +ACL entries do not use this field. +.It Ar "access permissions" +The access permissions field contains up to one of each of +the following: +.Ql r , +.Ql w , +and +.Ql x +to set read, write, and +execute permissions, respectively. +Each of these may be excluded +or replaced with a +.Ql - +character to indicate no access. +.El +.Pp +A +.Dq Li mask +ACL entry is required on a file with any ACL entries other than +the default +.Dq Li user , +.Dq Li group , +and +.Dq Li other +ACL entries. +If the +.Fl n +option is not specified and no +.Dq Li mask +ACL entry was specified, the +.Nm +utility +will apply a +.Dq Li mask +ACL entry consisting of the union of the permissions associated +with all +.Dq Li group +ACL entries in the resulting ACL. +.Pp +Traditional POSIX interfaces acting on file system object modes have +modified semantics in the presence of POSIX.1e extended ACLs. +When a mask entry is present on the access ACL of an object, the mask +entry is substituted for the group bits; this occurs in programs such +as +.Xr stat 1 +or +.Xr ls 1 . +When the mode is modified on an object that has a mask entry, the +changes applied to the group bits will actually be applied to the +mask entry. +These semantics provide for greater application compatibility: +applications modifying the mode instead of the ACL will see +conservative behavior, limiting the effective rights granted by all +of the additional user and group entries; this occurs in programs +such as +.Xr chmod 1 . +.Pp +ACL entries applied from a file using the +.Fl M +or +.Fl X +options shall be of the following form: one ACL entry per line, as +previously specified; whitespace is ignored; any text after a +.Ql # +is ignored (comments). +.Pp +When POSIX.1e ACL entries are evaluated, the access check algorithm checks +the ACL entries in the following order: file owner, +.Dq Li user +ACL entries, file owning group, +.Dq Li group +ACL entries, and +.Dq Li other +ACL entry. +.Pp +Multiple ACL entries specified on the command line are +separated by commas. +.Pp +It is possible for files and directories to inherit ACL entries from their +parent directory. +This is accomplished through the use of the default ACL. +It should be noted that before you can specify a default ACL, the mandatory +ACL entries for user, group, other and mask must be set. +For more details see the examples below. +Default ACLs can be created by using +.Fl d . +.Sh NFSv4 ACL ENTRIES +An NFSv4 ACL entry contains four or five colon-separated fields: an ACL tag, +an ACL qualifier (only for +.Dq Li user +and +.Dq Li group +tags), discretionary access permissions, ACL inheritance flags, and ACL type: +.Bl -tag -width indent +.It Ar "ACL tag" +The ACL tag specifies the ACL entry type and consists of +one of the following: +.Dq Li user +or +.Ql u +specifying the access +granted to the specified user; +.Dq Li group +or +.Ql g +specifying the access granted to the specified group; +.Dq Li owner@ +specifying the access granted to the owner of the file; +.Dq Li group@ +specifying the access granted to the file owning group; +.Dq Li everyone@ +specifying everyone. +Note that +.Dq Li everyone@ +is not the same as traditional Unix +.Dq Li other +- it means, +literally, everyone, including file owner and owning group. +.It Ar "ACL qualifier" +The ACL qualifier field describes the user or group associated with +the ACL entry. +It may consist of one of the following: uid or +user name, or gid or group name. +In entries whose tag type is one of +.Dq Li owner@ , +.Dq Li group@ , +or +.Dq Li everyone@ , +this field is omitted altogether, including the trailing colon. +.It Ar "access permissions" +Access permissions may be specified in either short or long form. +Short and long forms may not be mixed. +Permissions in long form are separated by the +.Ql / +character; in short form, they are concatenated together. +Valid permissions are: +.Bl -tag -width ".Dv modify_set" +.It Short +Long +.It r +read_data +.It w +write_data +.It x +execute +.It p +append_data +.It D +delete_child +.It d +delete +.It a +read_attributes +.It A +write_attributes +.It R +read_xattr +.It W +write_xattr +.It c +read_acl +.It C +write_acl +.It o +write_owner +.It s +synchronize +.El +.Pp +In addition, the following permission sets may be used: +.Bl -tag -width ".Dv modify_set" +.It Set +Permissions +.It full_set +all permissions, as shown above +.It modify_set +all permissions except write_acl and write_owner +.It read_set +read_data, read_attributes, read_xattr and read_acl +.It write_set +write_data, append_data, write_attributes and write_xattr +.El +.It Ar "ACL inheritance flags" +Inheritance flags may be specified in either short or long form. +Short and long forms may not be mixed. +Access flags in long form are separated by the +.Ql / +character; in short form, they are concatenated together. +Valid inheritance flags are: +.Bl -tag -width ".Dv short" +.It Short +Long +.It f +file_inherit +.It d +dir_inherit +.It i +inherit_only +.It n +no_propagate +.It I +inherited +.El +.Pp +Other than the "inherited" flag, inheritance flags may be only set on directories. +.It Ar "ACL type" +The ACL type field is either +.Dq Li allow +or +.Dq Li deny . +.El +.Pp +ACL entries applied from a file using the +.Fl M +or +.Fl X +options shall be of the following form: one ACL entry per line, as +previously specified; whitespace is ignored; any text after a +.Ql # +is ignored (comments). +.Pp +NFSv4 ACL entries are evaluated in their visible order. +.Pp +Multiple ACL entries specified on the command line are +separated by commas. +.Pp +Note that the file owner is always granted the read_acl, write_acl, +read_attributes, and write_attributes permissions, even if the ACL +would deny it. +.Sh EXIT STATUS +.Ex -std +.Sh EXAMPLES +.Dl setfacl -d -m u::rwx,g::rx,o::rx,mask::rwx dir +.Dl setfacl -d -m g:admins:rwx dir +.Pp +The first command sets the mandatory elements of the POSIX.1e default ACL. +The second command specifies that users in group admins can have read, write, and execute +permissions for directory named "dir". +It should be noted that any files or directories created underneath "dir" will +inherit these default ACLs upon creation. +.Pp +.Dl setfacl -m u::rwx,g:mail:rw file +.Pp +Sets read, write, and execute permissions for the +.Pa file +owner's POSIX.1e ACL entry and read and write permissions for group mail on +.Pa file . +.Pp +.Dl setfacl -m owner@:rwxp::allow,g:mail:rwp::allow file +.Pp +Semantically equal to the example above, but for NFSv4 ACL. +.Pp +.Dl setfacl -M file1 file2 +.Pp +Sets/updates the ACL entries contained in +.Pa file1 +on +.Pa file2 . +.Pp +.Dl setfacl -x g:mail:rw file +.Pp +Remove the group mail POSIX.1e ACL entry containing read/write permissions +from +.Pa file . +.Pp +.Dl setfacl -x0 file +.Pp +Remove the first entry from the NFSv4 ACL from +.Pa file . +.Pp +.Dl setfacl -bn file +.Pp +Remove all +.Dq Li access +ACL entries except for the three required from +.Pa file . +.Pp +.Dl getfacl file1 | setfacl -b -n -M - file2 +.Pp +Copy ACL entries from +.Pa file1 +to +.Pa file2 . +.Sh SEE ALSO +.Xr getfacl 1 , +.Xr acl 3 , +.Xr getextattr 8 , +.Xr setextattr 8 , +.Xr acl 9 , +.Xr extattr 9 +.Sh STANDARDS +The +.Nm +utility is expected to be +.Tn IEEE +Std 1003.2c compliant. +.Sh HISTORY +Extended Attribute and Access Control List support was developed +as part of the +.Tn TrustedBSD +Project and introduced in +.Fx 5.0 . +NFSv4 ACL support was introduced in +.Fx 8.1 . +.Sh AUTHORS +.An -nosplit +The +.Nm +utility was written by +.An Chris D. Faulhaber Aq Mt jedgar@fxp.org . +NFSv4 ACL support was implemented by +.An Edward Tomasz Napierala Aq Mt trasz@FreeBSD.org . diff --git a/corebinutils/setfacl/setfacl.c b/corebinutils/setfacl/setfacl.c new file mode 100644 index 0000000000..5e3aae3639 --- /dev/null +++ b/corebinutils/setfacl/setfacl.c @@ -0,0 +1,2080 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2001 Chris D. Faulhaber + * All rights reserved. + * + * Copyright (c) 2026 + * Project Tick. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ +/* + * Linux-native setfacl implementation. + * + * FreeBSD's original utility depends on sys/acl.h, acl_*_np(), and FTS. + * This port applies POSIX ACLs by reading and writing Linux ACL xattrs + * directly: + * - system.posix_acl_access + * - system.posix_acl_default + * + * NFSv4 ACL manipulation has no Linux POSIX ACL equivalent here and is + * rejected explicitly instead of emulating partial behavior. + */ + +#include <sys/stat.h> +#include <sys/types.h> +#include <sys/xattr.h> + +#include <ctype.h> +#include <dirent.h> +#include <endian.h> +#include <errno.h> +#include <getopt.h> +#include <grp.h> +#include <inttypes.h> +#include <limits.h> +#include <pwd.h> +#include <stdarg.h> +#include <stdbool.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#ifndef le16toh +#if __BYTE_ORDER == __LITTLE_ENDIAN +#define le16toh(x) (x) +#define le32toh(x) (x) +#define htole16(x) (x) +#define htole32(x) (x) +#else +#define le16toh(x) __builtin_bswap16(x) +#define le32toh(x) __builtin_bswap32(x) +#define htole16(x) __builtin_bswap16(x) +#define htole32(x) __builtin_bswap32(x) +#endif +#endif + +#ifndef ENOATTR +#define ENOATTR ENODATA +#endif + +#define ACL_XATTR_ACCESS "system.posix_acl_access" +#define ACL_XATTR_DEFAULT "system.posix_acl_default" + +#define POSIX_ACL_XATTR_VERSION 0x0002U +#define ACL_UNDEFINED_ID ((uint32_t)-1) + +#define ACL_READ 0x04U +#define ACL_WRITE 0x02U +#define ACL_EXECUTE 0x01U + +#define ACL_USER_OBJ 0x01U +#define ACL_USER 0x02U +#define ACL_GROUP_OBJ 0x04U +#define ACL_GROUP 0x08U +#define ACL_MASK 0x10U +#define ACL_OTHER 0x20U + +struct posix_acl_xattr_entry_linux { + uint16_t e_tag; + uint16_t e_perm; + uint32_t e_id; +}; + +struct posix_acl_xattr_header_linux { + uint32_t a_version; +}; + +enum acl_kind { + ACL_KIND_ACCESS, + ACL_KIND_DEFAULT, +}; + +enum op_type { + OP_MODIFY, + OP_REMOVE_SPEC, + OP_REMOVE_INDEX, + OP_REMOVE_ALL, + OP_REMOVE_DEFAULT, +}; + +enum walk_mode { + WALK_PHYSICAL, + WALK_LOGICAL, + WALK_HYBRID, +}; + +struct acl_entry_linux { + uint16_t tag; + uint16_t perm; + uint32_t id; +}; + +struct acl_list { + struct acl_entry_linux *entries; + size_t count; + size_t cap; +}; + +struct acl_spec { + struct acl_list acl; + bool explicit_mask; +}; + +struct operation { + enum op_type type; + enum acl_kind kind; + struct acl_spec spec; + size_t index; +}; + +struct operation_list { + struct operation *ops; + size_t count; + size_t cap; +}; + +struct options { + bool no_mask; + bool recursive; + bool no_follow; + enum walk_mode walk_mode; +}; + +struct parser_state { + enum acl_kind current_kind; + bool stdin_reserved; + bool stdin_path_error; +}; + +struct visited_dir { + dev_t dev; + ino_t ino; +}; + +struct visited_set { + struct visited_dir *items; + size_t count; + size_t cap; +}; + +struct file_state { + const char *path; + bool follow; + bool is_dir; + struct stat st; + bool access_loaded; + bool access_dirty; + struct acl_list access_acl; + bool default_loaded; + bool default_dirty; + struct acl_list default_acl; +}; + +static const struct option long_options[] = { + { NULL, 0, NULL, 0 }, +}; + +static void usage(void) __attribute__((noreturn)); + +static void *xcalloc(size_t nmemb, size_t size); +static void *xreallocarray(void *ptr, size_t nmemb, size_t size); +static char *xstrdup(const char *s); +static void report_error(const char *fmt, ...); +static void report_errno(const char *path); +static void report_path_error(const char *path, const char *fmt, ...); + +static void acl_list_init(struct acl_list *acl); +static void acl_list_free(struct acl_list *acl); +static void acl_list_clear(struct acl_list *acl); +static void acl_list_push(struct acl_list *acl, + const struct acl_entry_linux *entry); +static void acl_list_remove_at(struct acl_list *acl, size_t index); +static void op_list_init(struct operation_list *ops); +static void op_list_free(struct operation_list *ops); +static void op_list_push(struct operation_list *ops, const struct operation *op); + +static void visited_init(struct visited_set *visited); +static void visited_free(struct visited_set *visited); +static bool visited_contains(const struct visited_set *visited, dev_t dev, + ino_t ino); +static void visited_add(struct visited_set *visited, dev_t dev, ino_t ino); + +static const char *xattr_name(enum acl_kind kind); +static int tag_sort_rank(uint16_t tag); +static int compare_acl_entries(const void *lhs, const void *rhs); +static void acl_sort(struct acl_list *acl); +static bool acl_has_named_entries(const struct acl_list *acl); +static bool acl_has_mask(const struct acl_list *acl); +static bool acl_is_base_only(const struct acl_list *acl); +static struct acl_entry_linux *acl_find_entry(struct acl_list *acl, uint16_t tag, + uint32_t id); +static const struct acl_entry_linux *acl_find_entry_const(const struct acl_list *acl, + uint16_t tag, uint32_t id); +static struct acl_entry_linux *acl_find_single(struct acl_list *acl, uint16_t tag); +static const struct acl_entry_linux *acl_find_single_const(const struct acl_list *acl, + uint16_t tag); + +static int validate_acl(const struct acl_list *acl, enum acl_kind kind, + char *errbuf, size_t errbufsz); +static void synthesize_access_acl(mode_t mode, struct acl_list *acl); +static mode_t access_acl_to_mode(mode_t existing_mode, const struct acl_list *acl); +static void strip_access_acl(struct acl_list *acl); +static void recalculate_mask(struct acl_list *acl); + +static char *trim_whitespace(char *s); +static bool looks_like_nfs4_acl(const char *text); +static int parse_perm_string(const char *text, uint16_t *perm_out, + char *errbuf, size_t errbufsz); +static int resolve_user(const char *text, uint32_t *id_out, char *errbuf, + size_t errbufsz); +static int resolve_group(const char *text, uint32_t *id_out, char *errbuf, + size_t errbufsz); +static int parse_acl_entry(const char *text, enum acl_kind kind, bool for_remove, + struct acl_entry_linux *entry_out, bool *has_perm_out, char *errbuf, + size_t errbufsz); +static int parse_acl_text_list(const char *text, enum acl_kind kind, bool for_remove, + struct acl_spec *spec, char *errbuf, size_t errbufsz); +static int parse_acl_file(const char *filename, enum acl_kind kind, bool for_remove, + struct parser_state *parser, struct acl_spec *spec, char *errbuf, + size_t errbufsz); + +static int stat_path(const char *path, bool follow, struct stat *st); +static int load_xattr_blob(const char *path, enum acl_kind kind, bool follow, + void **buf_out, size_t *size_out); +static int parse_acl_blob(const void *buf, size_t size, enum acl_kind kind, + struct acl_list *acl, char *errbuf, size_t errbufsz); +static int encode_acl_blob(const struct acl_list *acl, void **buf_out, + size_t *size_out); + +static int load_acl_kind(struct file_state *state, enum acl_kind kind, + char *errbuf, size_t errbufsz); +static int persist_access_acl(struct file_state *state, char *errbuf, + size_t errbufsz); +static int persist_default_acl(struct file_state *state, char *errbuf, + size_t errbufsz); +static int persist_file_state(struct file_state *state, char *errbuf, + size_t errbufsz); + +static int apply_modify(struct acl_list *acl, const struct acl_spec *spec); +static int apply_remove_spec(struct acl_list *acl, const struct acl_spec *spec, + char *errbuf, size_t errbufsz); +static int apply_remove_index(struct acl_list *acl, size_t index, char *errbuf, + size_t errbufsz); +static int finalize_acl_after_change(struct acl_list *acl, enum acl_kind kind, + bool explicit_mask, bool no_mask, char *errbuf, size_t errbufsz); +static int apply_operation(struct file_state *state, const struct operation *op, + const struct options *opts, char *errbuf, size_t errbufsz); +static int process_single_path(const char *path, bool follow, + const struct operation_list *ops, const struct options *opts); +static int process_path_recursive(const char *path, bool follow_root, + bool follow_children, const struct operation_list *ops, + const struct options *opts, struct visited_set *visited); + +static char **read_path_operands_from_stdin(struct parser_state *parser); +static int process_operand(const char *path, const struct operation_list *ops, + const struct options *opts, struct visited_set *visited); + +static void +usage(void) +{ + + fprintf(stderr, + "usage: setfacl [-R [-H | -L | -P]] [-bdhkn] " + "[-a position entries] [-m entries] [-M file] " + "[-x entries | position] [-X file] [file ...]\n"); + exit(1); +} + +static void * +xcalloc(size_t nmemb, size_t size) +{ + void *ptr; + + if (nmemb != 0 && size > SIZE_MAX / nmemb) { + report_error("setfacl: allocation overflow"); + exit(1); + } + ptr = calloc(nmemb, size); + if (ptr == NULL) { + report_errno("calloc"); + exit(1); + } + return (ptr); +} + +static void * +xreallocarray(void *ptr, size_t nmemb, size_t size) +{ + void *newptr; + + if (nmemb != 0 && size > SIZE_MAX / nmemb) { + report_error("setfacl: allocation overflow"); + exit(1); + } + newptr = realloc(ptr, nmemb * size); + if (newptr == NULL) { + report_errno("realloc"); + exit(1); + } + return (newptr); +} + +static char * +xstrdup(const char *s) +{ + char *copy; + + copy = strdup(s); + if (copy == NULL) { + report_errno("strdup"); + exit(1); + } + return (copy); +} + +static void +report_error(const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + fputc('\n', stderr); +} + +static void +report_errno(const char *path) +{ + + fprintf(stderr, "setfacl: %s: %s\n", path, strerror(errno)); +} + +static void +report_path_error(const char *path, const char *fmt, ...) +{ + va_list ap; + + fprintf(stderr, "setfacl: %s: ", path); + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + fputc('\n', stderr); +} + +static void +acl_list_init(struct acl_list *acl) +{ + + acl->entries = NULL; + acl->count = 0; + acl->cap = 0; +} + +static void +acl_list_free(struct acl_list *acl) +{ + + free(acl->entries); + acl->entries = NULL; + acl->count = 0; + acl->cap = 0; +} + +static void +acl_list_clear(struct acl_list *acl) +{ + + acl->count = 0; +} + +static void +acl_list_push(struct acl_list *acl, const struct acl_entry_linux *entry) +{ + + if (acl->count == acl->cap) { + size_t newcap; + + newcap = (acl->cap == 0) ? 8 : acl->cap * 2; + acl->entries = xreallocarray(acl->entries, newcap, + sizeof(*acl->entries)); + acl->cap = newcap; + } + acl->entries[acl->count++] = *entry; +} + +static void +acl_list_remove_at(struct acl_list *acl, size_t index) +{ + + if (index + 1 < acl->count) { + memmove(&acl->entries[index], &acl->entries[index + 1], + (acl->count - index - 1) * sizeof(*acl->entries)); + } + acl->count--; +} + +static void +op_list_init(struct operation_list *ops) +{ + + ops->ops = NULL; + ops->count = 0; + ops->cap = 0; +} + +static void +op_list_free(struct operation_list *ops) +{ + size_t i; + + for (i = 0; i < ops->count; i++) + acl_list_free(&ops->ops[i].spec.acl); + free(ops->ops); + ops->ops = NULL; + ops->count = 0; + ops->cap = 0; +} + +static void +op_list_push(struct operation_list *ops, const struct operation *op) +{ + + if (ops->count == ops->cap) { + size_t newcap; + + newcap = (ops->cap == 0) ? 8 : ops->cap * 2; + ops->ops = xreallocarray(ops->ops, newcap, sizeof(*ops->ops)); + ops->cap = newcap; + } + ops->ops[ops->count++] = *op; +} + +static void +visited_init(struct visited_set *visited) +{ + + visited->items = NULL; + visited->count = 0; + visited->cap = 0; +} + +static void +visited_free(struct visited_set *visited) +{ + + free(visited->items); + visited->items = NULL; + visited->count = 0; + visited->cap = 0; +} + +static bool +visited_contains(const struct visited_set *visited, dev_t dev, ino_t ino) +{ + size_t i; + + for (i = 0; i < visited->count; i++) { + if (visited->items[i].dev == dev && visited->items[i].ino == ino) + return (true); + } + return (false); +} + +static void +visited_add(struct visited_set *visited, dev_t dev, ino_t ino) +{ + + if (visited->count == visited->cap) { + size_t newcap; + + newcap = (visited->cap == 0) ? 16 : visited->cap * 2; + visited->items = xreallocarray(visited->items, newcap, + sizeof(*visited->items)); + visited->cap = newcap; + } + visited->items[visited->count].dev = dev; + visited->items[visited->count].ino = ino; + visited->count++; +} + +static const char * +xattr_name(enum acl_kind kind) +{ + + return (kind == ACL_KIND_DEFAULT) ? ACL_XATTR_DEFAULT : ACL_XATTR_ACCESS; +} + +static int +tag_sort_rank(uint16_t tag) +{ + + switch (tag) { + case ACL_USER_OBJ: + return (0); + case ACL_USER: + return (1); + case ACL_GROUP_OBJ: + return (2); + case ACL_GROUP: + return (3); + case ACL_MASK: + return (4); + case ACL_OTHER: + return (5); + default: + return (6); + } +} + +static int +compare_acl_entries(const void *lhs, const void *rhs) +{ + const struct acl_entry_linux *a; + const struct acl_entry_linux *b; + int rank_a; + int rank_b; + + a = lhs; + b = rhs; + rank_a = tag_sort_rank(a->tag); + rank_b = tag_sort_rank(b->tag); + if (rank_a != rank_b) + return (rank_a < rank_b ? -1 : 1); + if (a->id < b->id) + return (-1); + if (a->id > b->id) + return (1); + return (0); +} + +static void +acl_sort(struct acl_list *acl) +{ + + if (acl->count > 1) + qsort(acl->entries, acl->count, sizeof(*acl->entries), + compare_acl_entries); +} + +static bool +acl_has_named_entries(const struct acl_list *acl) +{ + size_t i; + + for (i = 0; i < acl->count; i++) { + if (acl->entries[i].tag == ACL_USER || acl->entries[i].tag == ACL_GROUP) + return (true); + } + return (false); +} + +static bool +acl_has_mask(const struct acl_list *acl) +{ + + return (acl_find_single_const(acl, ACL_MASK) != NULL); +} + +static bool +acl_is_base_only(const struct acl_list *acl) +{ + size_t i; + + for (i = 0; i < acl->count; i++) { + switch (acl->entries[i].tag) { + case ACL_USER_OBJ: + case ACL_GROUP_OBJ: + case ACL_OTHER: + break; + default: + return (false); + } + } + return (true); +} + +static struct acl_entry_linux * +acl_find_entry(struct acl_list *acl, uint16_t tag, uint32_t id) +{ + size_t i; + + for (i = 0; i < acl->count; i++) { + if (acl->entries[i].tag == tag && acl->entries[i].id == id) + return (&acl->entries[i]); + } + return (NULL); +} + +static const struct acl_entry_linux * +acl_find_entry_const(const struct acl_list *acl, uint16_t tag, uint32_t id) +{ + size_t i; + + for (i = 0; i < acl->count; i++) { + if (acl->entries[i].tag == tag && acl->entries[i].id == id) + return (&acl->entries[i]); + } + return (NULL); +} + +static struct acl_entry_linux * +acl_find_single(struct acl_list *acl, uint16_t tag) +{ + + return (acl_find_entry(acl, tag, ACL_UNDEFINED_ID)); +} + +static const struct acl_entry_linux * +acl_find_single_const(const struct acl_list *acl, uint16_t tag) +{ + + return (acl_find_entry_const(acl, tag, ACL_UNDEFINED_ID)); +} + +static int +validate_acl(const struct acl_list *acl, enum acl_kind kind, char *errbuf, + size_t errbufsz) +{ + size_t i; + bool saw_user_obj; + bool saw_group_obj; + bool saw_other; + bool saw_mask; + bool saw_named_user; + bool saw_named_group; + + if (kind == ACL_KIND_DEFAULT && acl->count == 0) + return (0); + if (kind == ACL_KIND_ACCESS && acl->count == 0) { + snprintf(errbuf, errbufsz, + "access ACL cannot be empty"); + return (-1); + } + + saw_user_obj = false; + saw_group_obj = false; + saw_other = false; + saw_mask = false; + saw_named_user = false; + saw_named_group = false; + + for (i = 0; i < acl->count; i++) { + const struct acl_entry_linux *entry; + + entry = &acl->entries[i]; + if ((entry->perm & ~(ACL_READ | ACL_WRITE | ACL_EXECUTE)) != 0) { + snprintf(errbuf, errbufsz, + "ACL contains invalid permission bits"); + return (-1); + } + if (i > 0 && + compare_acl_entries(&acl->entries[i - 1], &acl->entries[i]) == 0) { + snprintf(errbuf, errbufsz, + "ACL contains duplicate entries"); + return (-1); + } + + switch (entry->tag) { + case ACL_USER_OBJ: + saw_user_obj = true; + break; + case ACL_USER: + saw_named_user = true; + break; + case ACL_GROUP_OBJ: + saw_group_obj = true; + break; + case ACL_GROUP: + saw_named_group = true; + break; + case ACL_MASK: + saw_mask = true; + break; + case ACL_OTHER: + saw_other = true; + break; + default: + snprintf(errbuf, errbufsz, + "ACL contains unsupported tag 0x%x", entry->tag); + return (-1); + } + } + + if (!saw_user_obj) { + snprintf(errbuf, errbufsz, "ACL is missing required user:: entry"); + return (-1); + } + if (!saw_group_obj) { + snprintf(errbuf, errbufsz, "ACL is missing required group:: entry"); + return (-1); + } + if (!saw_other) { + snprintf(errbuf, errbufsz, "ACL is missing required other:: entry"); + return (-1); + } + if ((saw_named_user || saw_named_group) && !saw_mask) { + snprintf(errbuf, errbufsz, + "ACL with named user/group entries requires a mask entry"); + return (-1); + } + + return (0); +} + +static void +synthesize_access_acl(mode_t mode, struct acl_list *acl) +{ + struct acl_entry_linux entry; + + acl_list_clear(acl); + + entry.tag = ACL_USER_OBJ; + entry.id = ACL_UNDEFINED_ID; + entry.perm = ((mode & S_IRUSR) ? ACL_READ : 0) | + ((mode & S_IWUSR) ? ACL_WRITE : 0) | + ((mode & S_IXUSR) ? ACL_EXECUTE : 0); + acl_list_push(acl, &entry); + + entry.tag = ACL_GROUP_OBJ; + entry.perm = ((mode & S_IRGRP) ? ACL_READ : 0) | + ((mode & S_IWGRP) ? ACL_WRITE : 0) | + ((mode & S_IXGRP) ? ACL_EXECUTE : 0); + acl_list_push(acl, &entry); + + entry.tag = ACL_OTHER; + entry.perm = ((mode & S_IROTH) ? ACL_READ : 0) | + ((mode & S_IWOTH) ? ACL_WRITE : 0) | + ((mode & S_IXOTH) ? ACL_EXECUTE : 0); + acl_list_push(acl, &entry); +} + +static mode_t +access_acl_to_mode(mode_t existing_mode, const struct acl_list *acl) +{ + const struct acl_entry_linux *user_obj; + const struct acl_entry_linux *group_obj; + const struct acl_entry_linux *other; + mode_t mode; + + user_obj = acl_find_single_const(acl, ACL_USER_OBJ); + group_obj = acl_find_single_const(acl, ACL_GROUP_OBJ); + other = acl_find_single_const(acl, ACL_OTHER); + + mode = existing_mode & ~0777; + if (user_obj->perm & ACL_READ) + mode |= S_IRUSR; + if (user_obj->perm & ACL_WRITE) + mode |= S_IWUSR; + if (user_obj->perm & ACL_EXECUTE) + mode |= S_IXUSR; + if (group_obj->perm & ACL_READ) + mode |= S_IRGRP; + if (group_obj->perm & ACL_WRITE) + mode |= S_IWGRP; + if (group_obj->perm & ACL_EXECUTE) + mode |= S_IXGRP; + if (other->perm & ACL_READ) + mode |= S_IROTH; + if (other->perm & ACL_WRITE) + mode |= S_IWOTH; + if (other->perm & ACL_EXECUTE) + mode |= S_IXOTH; + return (mode); +} + +static void +strip_access_acl(struct acl_list *acl) +{ + struct acl_entry_linux *group_obj; + const struct acl_entry_linux *mask; + uint16_t group_perm; + size_t i; + + group_obj = acl_find_single(acl, ACL_GROUP_OBJ); + mask = acl_find_single_const(acl, ACL_MASK); + group_perm = (group_obj != NULL) ? group_obj->perm : 0; + if (mask != NULL) + group_perm &= mask->perm; + + for (i = 0; i < acl->count;) { + switch (acl->entries[i].tag) { + case ACL_USER_OBJ: + case ACL_GROUP_OBJ: + case ACL_OTHER: + i++; + break; + default: + acl_list_remove_at(acl, i); + break; + } + } + group_obj = acl_find_single(acl, ACL_GROUP_OBJ); + if (group_obj != NULL) + group_obj->perm = group_perm; + acl_sort(acl); +} + +static void +recalculate_mask(struct acl_list *acl) +{ + struct acl_entry_linux *mask; + const struct acl_entry_linux *group_obj; + uint16_t union_perm; + size_t i; + + group_obj = acl_find_single_const(acl, ACL_GROUP_OBJ); + if (group_obj == NULL) + return; + + union_perm = group_obj->perm; + for (i = 0; i < acl->count; i++) { + if (acl->entries[i].tag == ACL_USER || acl->entries[i].tag == ACL_GROUP) + union_perm |= acl->entries[i].perm; + } + + mask = acl_find_single(acl, ACL_MASK); + if (mask == NULL) { + struct acl_entry_linux entry; + + entry.tag = ACL_MASK; + entry.id = ACL_UNDEFINED_ID; + entry.perm = union_perm; + acl_list_push(acl, &entry); + } else { + mask->perm = union_perm; + } + acl_sort(acl); +} + +static char * +trim_whitespace(char *s) +{ + char *end; + + while (*s != '\0' && isspace((unsigned char)*s)) + s++; + end = s + strlen(s); + while (end > s && isspace((unsigned char)end[-1])) + end--; + *end = '\0'; + return (s); +} + +static bool +looks_like_nfs4_acl(const char *text) +{ + size_t colon_count; + const char *p; + + if (strstr(text, ":allow") != NULL || strstr(text, ":deny") != NULL) + return (true); + if (strchr(text, '@') != NULL) + return (true); + colon_count = 0; + for (p = text; *p != '\0'; p++) { + if (*p == ':') + colon_count++; + } + return (colon_count > 2); +} + +static int +parse_perm_string(const char *text, uint16_t *perm_out, char *errbuf, + size_t errbufsz) +{ + uint16_t perm; + size_t i; + bool saw_r; + bool saw_w; + bool saw_x; + + if (*text == '\0') { + snprintf(errbuf, errbufsz, "missing ACL permissions"); + return (-1); + } + + perm = 0; + saw_r = false; + saw_w = false; + saw_x = false; + for (i = 0; text[i] != '\0'; i++) { + switch (text[i]) { + case 'r': + if (saw_r) { + snprintf(errbuf, errbufsz, + "duplicate ACL permission 'r'"); + return (-1); + } + saw_r = true; + perm |= ACL_READ; + break; + case 'w': + if (saw_w) { + snprintf(errbuf, errbufsz, + "duplicate ACL permission 'w'"); + return (-1); + } + saw_w = true; + perm |= ACL_WRITE; + break; + case 'x': + if (saw_x) { + snprintf(errbuf, errbufsz, + "duplicate ACL permission 'x'"); + return (-1); + } + saw_x = true; + perm |= ACL_EXECUTE; + break; + case '-': + break; + default: + snprintf(errbuf, errbufsz, + "invalid ACL permission character '%c'", text[i]); + return (-1); + } + } + + *perm_out = perm; + return (0); +} + +static int +resolve_user(const char *text, uint32_t *id_out, char *errbuf, size_t errbufsz) +{ + struct passwd *pwd; + char *end; + unsigned long value; + + errno = 0; + value = strtoul(text, &end, 10); + if (*text != '\0' && *end == '\0' && errno == 0 && value <= UINT32_MAX) { + *id_out = (uint32_t)value; + return (0); + } + + pwd = getpwnam(text); + if (pwd == NULL) { + snprintf(errbuf, errbufsz, "unknown user '%s'", text); + return (-1); + } + *id_out = (uint32_t)pwd->pw_uid; + return (0); +} + +static int +resolve_group(const char *text, uint32_t *id_out, char *errbuf, size_t errbufsz) +{ + struct group *grp; + char *end; + unsigned long value; + + errno = 0; + value = strtoul(text, &end, 10); + if (*text != '\0' && *end == '\0' && errno == 0 && value <= UINT32_MAX) { + *id_out = (uint32_t)value; + return (0); + } + + grp = getgrnam(text); + if (grp == NULL) { + snprintf(errbuf, errbufsz, "unknown group '%s'", text); + return (-1); + } + *id_out = (uint32_t)grp->gr_gid; + return (0); +} + +static int +parse_acl_entry(const char *text, enum acl_kind kind, bool for_remove, + struct acl_entry_linux *entry_out, bool *has_perm_out, char *errbuf, + size_t errbufsz) +{ + char *copy; + char *body; + char *first; + char *second; + char *third; + char *tag_text; + char *qual_text; + char *perm_text; + int ret; + + copy = xstrdup(text); + body = trim_whitespace(copy); + if (strncmp(body, "default:", 8) == 0) { + if (kind != ACL_KIND_DEFAULT) { + snprintf(errbuf, errbufsz, + "default ACL entry requires -d"); + free(copy); + return (-1); + } + body += 8; + body = trim_whitespace(body); + } + + if (looks_like_nfs4_acl(body)) { + snprintf(errbuf, errbufsz, + "NFSv4 ACL entries are not supported on Linux"); + free(copy); + return (-1); + } + + first = strchr(body, ':'); + if (first == NULL) { + snprintf(errbuf, errbufsz, "ACL entry '%s' is missing ':' fields", + body); + free(copy); + return (-1); + } + second = strchr(first + 1, ':'); + if (second == NULL && !for_remove) { + snprintf(errbuf, errbufsz, "ACL entry '%s' is missing permission field", + body); + free(copy); + return (-1); + } + third = (second != NULL) ? strchr(second + 1, ':') : NULL; + if (third != NULL) { + snprintf(errbuf, errbufsz, + "NFSv4 ACL entries are not supported on Linux"); + free(copy); + return (-1); + } + + *first = '\0'; + if (second != NULL) + *second = '\0'; + tag_text = trim_whitespace(body); + qual_text = trim_whitespace(first + 1); + perm_text = (second != NULL) ? trim_whitespace(second + 1) : (char *)""; + + if (strcmp(tag_text, "u") == 0 || strcmp(tag_text, "user") == 0) { + if (*qual_text == '\0') { + entry_out->tag = ACL_USER_OBJ; + entry_out->id = ACL_UNDEFINED_ID; + } else { + entry_out->tag = ACL_USER; + if (resolve_user(qual_text, &entry_out->id, errbuf, errbufsz) != 0) { + free(copy); + return (-1); + } + } + } else if (strcmp(tag_text, "g") == 0 || strcmp(tag_text, "group") == 0) { + if (*qual_text == '\0') { + entry_out->tag = ACL_GROUP_OBJ; + entry_out->id = ACL_UNDEFINED_ID; + } else { + entry_out->tag = ACL_GROUP; + if (resolve_group(qual_text, &entry_out->id, errbuf, errbufsz) != 0) { + free(copy); + return (-1); + } + } + } else if (strcmp(tag_text, "o") == 0 || strcmp(tag_text, "other") == 0) { + if (*qual_text != '\0') { + snprintf(errbuf, errbufsz, + "other:: ACL entry does not accept a qualifier"); + free(copy); + return (-1); + } + entry_out->tag = ACL_OTHER; + entry_out->id = ACL_UNDEFINED_ID; + } else if (strcmp(tag_text, "m") == 0 || strcmp(tag_text, "mask") == 0) { + if (*qual_text != '\0') { + snprintf(errbuf, errbufsz, + "mask:: ACL entry does not accept a qualifier"); + free(copy); + return (-1); + } + entry_out->tag = ACL_MASK; + entry_out->id = ACL_UNDEFINED_ID; + } else { + snprintf(errbuf, errbufsz, "unsupported ACL tag '%s'", tag_text); + free(copy); + return (-1); + } + + if (*perm_text == '\0') { + if (!for_remove) { + snprintf(errbuf, errbufsz, + "ACL entry '%s' is missing permissions", text); + free(copy); + return (-1); + } + *has_perm_out = false; + entry_out->perm = 0; + free(copy); + return (0); + } + + ret = parse_perm_string(perm_text, &entry_out->perm, errbuf, errbufsz); + free(copy); + if (ret != 0) + return (-1); + *has_perm_out = true; + return (0); +} + +static int +parse_acl_text_list(const char *text, enum acl_kind kind, bool for_remove, + struct acl_spec *spec, char *errbuf, size_t errbufsz) +{ + char *copy; + char *cursor; + + copy = xstrdup(text); + cursor = copy; + while (*cursor != '\0') { + char *next; + char *token; + struct acl_entry_linux entry; + bool has_perm; + + next = strchr(cursor, ','); + if (next != NULL) + *next = '\0'; + token = trim_whitespace(cursor); + if (*token == '\0') { + snprintf(errbuf, errbufsz, "empty ACL entry"); + free(copy); + return (-1); + } + if (parse_acl_entry(token, kind, for_remove, &entry, &has_perm, + errbuf, errbufsz) != 0) { + free(copy); + return (-1); + } + acl_list_push(&spec->acl, &entry); + if (entry.tag == ACL_MASK) + spec->explicit_mask = true; + if (next == NULL) + break; + cursor = next + 1; + } + free(copy); + return (0); +} + +static int +parse_acl_file(const char *filename, enum acl_kind kind, bool for_remove, + struct parser_state *parser, struct acl_spec *spec, char *errbuf, + size_t errbufsz) +{ + FILE *fp; + char *line; + size_t linecap; + ssize_t linelen; + + if (strcmp(filename, "-") == 0) { + if (parser->stdin_reserved) { + snprintf(errbuf, errbufsz, + "cannot specify more than one stdin source"); + return (-1); + } + parser->stdin_reserved = true; + fp = stdin; + } else { + fp = fopen(filename, "r"); + if (fp == NULL) { + snprintf(errbuf, errbufsz, "%s: %s", filename, strerror(errno)); + return (-1); + } + } + + line = NULL; + linecap = 0; + while ((linelen = getline(&line, &linecap, fp)) != -1) { + char *comment; + char *text; + + (void)linelen; + comment = strchr(line, '#'); + if (comment != NULL) + *comment = '\0'; + text = trim_whitespace(line); + if (*text == '\0') + continue; + if (parse_acl_text_list(text, kind, for_remove, spec, errbuf, + errbufsz) != 0) { + free(line); + if (fp != stdin) + fclose(fp); + return (-1); + } + } + + if (ferror(fp) != 0) { + snprintf(errbuf, errbufsz, "%s: %s", filename, strerror(errno)); + free(line); + if (fp != stdin) + fclose(fp); + return (-1); + } + + free(line); + if (fp != stdin) + fclose(fp); + return (0); +} + +static int +stat_path(const char *path, bool follow, struct stat *st) +{ + + if (follow) + return (stat(path, st)); + return (lstat(path, st)); +} + +static int +load_xattr_blob(const char *path, enum acl_kind kind, bool follow, void **buf_out, + size_t *size_out) +{ + const char *name; + ssize_t size; + void *buf; + + *buf_out = NULL; + *size_out = 0; + name = xattr_name(kind); + + for (;;) { + if (follow) + size = getxattr(path, name, NULL, 0); + else + size = lgetxattr(path, name, NULL, 0); + if (size >= 0) + break; + if (errno == ENODATA || errno == ENOATTR || errno == ENOTSUP || + errno == EOPNOTSUPP) + return (0); + return (-1); + } + if (size == 0) + return (0); + + buf = xcalloc((size_t)size, 1); + for (;;) { + ssize_t nread; + + if (follow) + nread = getxattr(path, name, buf, (size_t)size); + else + nread = lgetxattr(path, name, buf, (size_t)size); + if (nread >= 0) { + *buf_out = buf; + *size_out = (size_t)nread; + return (1); + } + if (errno != ERANGE) { + free(buf); + if (errno == ENODATA || errno == ENOATTR || errno == ENOTSUP || + errno == EOPNOTSUPP) + return (0); + return (-1); + } + free(buf); + if (follow) + size = getxattr(path, name, NULL, 0); + else + size = lgetxattr(path, name, NULL, 0); + if (size <= 0) + return (0); + buf = xcalloc((size_t)size, 1); + } +} + +static int +parse_acl_blob(const void *buf, size_t size, enum acl_kind kind, + struct acl_list *acl, char *errbuf, size_t errbufsz) +{ + const struct posix_acl_xattr_header_linux *header; + const struct posix_acl_xattr_entry_linux *src; + size_t count; + size_t i; + + if (size < sizeof(*header)) { + snprintf(errbuf, errbufsz, "ACL xattr is truncated"); + return (-1); + } + if ((size - sizeof(*header)) % sizeof(*src) != 0) { + snprintf(errbuf, errbufsz, "ACL xattr has invalid length"); + return (-1); + } + + header = buf; + if (le32toh(header->a_version) != POSIX_ACL_XATTR_VERSION) { + snprintf(errbuf, errbufsz, "unsupported ACL xattr version %" PRIu32, + le32toh(header->a_version)); + return (-1); + } + + count = (size - sizeof(*header)) / sizeof(*src); + acl_list_clear(acl); + src = (const struct posix_acl_xattr_entry_linux *)((const char *)buf + + sizeof(*header)); + for (i = 0; i < count; i++) { + struct acl_entry_linux entry; + + entry.tag = le16toh(src[i].e_tag); + entry.perm = le16toh(src[i].e_perm); + entry.id = le32toh(src[i].e_id); + if (entry.tag != ACL_USER && entry.tag != ACL_GROUP) + entry.id = ACL_UNDEFINED_ID; + acl_list_push(acl, &entry); + } + acl_sort(acl); + return (validate_acl(acl, kind, errbuf, errbufsz)); +} + +static int +encode_acl_blob(const struct acl_list *acl, void **buf_out, size_t *size_out) +{ + struct posix_acl_xattr_header_linux *header; + struct posix_acl_xattr_entry_linux *dst; + void *buf; + size_t size; + size_t i; + + size = sizeof(*header) + acl->count * sizeof(*dst); + buf = xcalloc(size, 1); + header = buf; + header->a_version = htole32(POSIX_ACL_XATTR_VERSION); + dst = (struct posix_acl_xattr_entry_linux *)((char *)buf + sizeof(*header)); + for (i = 0; i < acl->count; i++) { + dst[i].e_tag = htole16(acl->entries[i].tag); + dst[i].e_perm = htole16(acl->entries[i].perm); + dst[i].e_id = htole32(acl->entries[i].id); + } + *buf_out = buf; + *size_out = size; + return (0); +} + +static int +load_acl_kind(struct file_state *state, enum acl_kind kind, char *errbuf, + size_t errbufsz) +{ + struct acl_list *acl; + bool *loaded; + void *buf; + size_t size; + int xattr_state; + + if (kind == ACL_KIND_DEFAULT) { + loaded = &state->default_loaded; + acl = &state->default_acl; + } else { + loaded = &state->access_loaded; + acl = &state->access_acl; + } + if (*loaded) + return (0); + + buf = NULL; + size = 0; + xattr_state = load_xattr_blob(state->path, kind, state->follow, &buf, &size); + if (xattr_state < 0) { + snprintf(errbuf, errbufsz, "%s", strerror(errno)); + return (-1); + } + if (xattr_state == 0) { + if (kind == ACL_KIND_ACCESS) + synthesize_access_acl(state->st.st_mode, acl); + else + acl_list_clear(acl); + *loaded = true; + return (0); + } + + if (parse_acl_blob(buf, size, kind, acl, errbuf, errbufsz) != 0) { + free(buf); + return (-1); + } + free(buf); + *loaded = true; + return (0); +} + +static int +persist_access_acl(struct file_state *state, char *errbuf, size_t errbufsz) +{ + const char *name; + mode_t new_mode; + + name = xattr_name(ACL_KIND_ACCESS); + if (acl_is_base_only(&state->access_acl)) { + new_mode = access_acl_to_mode(state->st.st_mode, &state->access_acl); + if (chmod(state->path, new_mode) != 0) { + snprintf(errbuf, errbufsz, "%s", strerror(errno)); + return (-1); + } + state->st.st_mode = (state->st.st_mode & ~0777) | (new_mode & 0777); + if (state->follow) { + if (removexattr(state->path, name) != 0 && + errno != ENODATA && errno != ENOATTR && + errno != ENOTSUP && errno != EOPNOTSUPP) { + snprintf(errbuf, errbufsz, "%s", strerror(errno)); + return (-1); + } + } else { + if (lremovexattr(state->path, name) != 0 && + errno != ENODATA && errno != ENOATTR && + errno != ENOTSUP && errno != EOPNOTSUPP) { + snprintf(errbuf, errbufsz, "%s", strerror(errno)); + return (-1); + } + } + return (0); + } + + { + void *buf; + size_t size; + int ret; + + buf = NULL; + size = 0; + encode_acl_blob(&state->access_acl, &buf, &size); + if (state->follow) + ret = setxattr(state->path, name, buf, size, 0); + else + ret = lsetxattr(state->path, name, buf, size, 0); + free(buf); + if (ret != 0) { + snprintf(errbuf, errbufsz, "%s", strerror(errno)); + return (-1); + } + } + + return (0); +} + +static int +persist_default_acl(struct file_state *state, char *errbuf, size_t errbufsz) +{ + const char *name; + + name = xattr_name(ACL_KIND_DEFAULT); + if (state->default_acl.count == 0) { + int ret; + + if (state->follow) + ret = removexattr(state->path, name); + else + ret = lremovexattr(state->path, name); + if (ret != 0 && errno != ENODATA && errno != ENOATTR && + errno != ENOTSUP && errno != EOPNOTSUPP) { + snprintf(errbuf, errbufsz, "%s", strerror(errno)); + return (-1); + } + return (0); + } + + { + void *buf; + size_t size; + int ret; + + buf = NULL; + size = 0; + encode_acl_blob(&state->default_acl, &buf, &size); + if (state->follow) + ret = setxattr(state->path, name, buf, size, 0); + else + ret = lsetxattr(state->path, name, buf, size, 0); + free(buf); + if (ret != 0) { + snprintf(errbuf, errbufsz, "%s", strerror(errno)); + return (-1); + } + } + + return (0); +} + +static int +persist_file_state(struct file_state *state, char *errbuf, size_t errbufsz) +{ + + if (state->access_dirty && + persist_access_acl(state, errbuf, errbufsz) != 0) + return (-1); + if (state->default_dirty && + persist_default_acl(state, errbuf, errbufsz) != 0) + return (-1); + return (0); +} + +static int +apply_modify(struct acl_list *acl, const struct acl_spec *spec) +{ + size_t i; + + for (i = 0; i < spec->acl.count; i++) { + const struct acl_entry_linux *entry; + struct acl_entry_linux *existing; + + entry = &spec->acl.entries[i]; + existing = acl_find_entry(acl, entry->tag, entry->id); + if (existing != NULL) + *existing = *entry; + else + acl_list_push(acl, entry); + } + acl_sort(acl); + return (0); +} + +static int +apply_remove_spec(struct acl_list *acl, const struct acl_spec *spec, char *errbuf, + size_t errbufsz) +{ + size_t i; + + for (i = 0; i < spec->acl.count; i++) { + const struct acl_entry_linux *entry; + size_t j; + bool found; + + entry = &spec->acl.entries[i]; + found = false; + for (j = 0; j < acl->count; j++) { + if (acl->entries[j].tag == entry->tag && + acl->entries[j].id == entry->id) { + acl_list_remove_at(acl, j); + found = true; + break; + } + } + if (!found) { + snprintf(errbuf, errbufsz, + "cannot remove non-existent ACL entry"); + return (-1); + } + } + acl_sort(acl); + return (0); +} + +static int +apply_remove_index(struct acl_list *acl, size_t index, char *errbuf, + size_t errbufsz) +{ + + if (index >= acl->count) { + snprintf(errbuf, errbufsz, "ACL entry index %zu is out of range", + index); + return (-1); + } + acl_list_remove_at(acl, index); + return (0); +} + +static int +finalize_acl_after_change(struct acl_list *acl, enum acl_kind kind, + bool explicit_mask, bool no_mask, char *errbuf, size_t errbufsz) +{ + + acl_sort(acl); + if (kind == ACL_KIND_DEFAULT && acl->count == 0) + return (0); + if (!explicit_mask && !no_mask && + (acl_has_named_entries(acl) || acl_has_mask(acl))) + recalculate_mask(acl); + if (no_mask && acl_has_named_entries(acl) && !acl_has_mask(acl)) { + snprintf(errbuf, errbufsz, + "ACL requires a mask entry when -n is used"); + return (-1); + } + acl_sort(acl); + return (validate_acl(acl, kind, errbuf, errbufsz)); +} + +static int +apply_operation(struct file_state *state, const struct operation *op, + const struct options *opts, char *errbuf, size_t errbufsz) +{ + struct acl_list *acl; + + switch (op->type) { + case OP_REMOVE_DEFAULT: + if (!state->is_dir) { + snprintf(errbuf, errbufsz, + "default ACL may only be set on a directory"); + return (-1); + } + if (load_acl_kind(state, ACL_KIND_DEFAULT, errbuf, errbufsz) != 0) + return (-1); + acl_list_clear(&state->default_acl); + state->default_dirty = true; + return (0); + case OP_REMOVE_ALL: + if (op->kind == ACL_KIND_DEFAULT) { + if (!state->is_dir) { + snprintf(errbuf, errbufsz, + "default ACL may only be set on a directory"); + return (-1); + } + if (load_acl_kind(state, ACL_KIND_DEFAULT, errbuf, errbufsz) != 0) + return (-1); + acl_list_clear(&state->default_acl); + state->default_dirty = true; + return (0); + } + if (load_acl_kind(state, ACL_KIND_ACCESS, errbuf, errbufsz) != 0) + return (-1); + strip_access_acl(&state->access_acl); + if (finalize_acl_after_change(&state->access_acl, ACL_KIND_ACCESS, + false, opts->no_mask, errbuf, errbufsz) != 0) + return (-1); + state->access_dirty = true; + return (0); + default: + break; + } + + if (op->kind == ACL_KIND_DEFAULT) { + if (!state->is_dir) { + snprintf(errbuf, errbufsz, + "default ACL may only be set on a directory"); + return (-1); + } + if (load_acl_kind(state, ACL_KIND_DEFAULT, errbuf, errbufsz) != 0) + return (-1); + acl = &state->default_acl; + } else { + if (load_acl_kind(state, ACL_KIND_ACCESS, errbuf, errbufsz) != 0) + return (-1); + acl = &state->access_acl; + } + + switch (op->type) { + case OP_MODIFY: + apply_modify(acl, &op->spec); + if (finalize_acl_after_change(acl, op->kind, op->spec.explicit_mask, + opts->no_mask, errbuf, errbufsz) != 0) + return (-1); + break; + case OP_REMOVE_SPEC: + if (apply_remove_spec(acl, &op->spec, errbuf, errbufsz) != 0) + return (-1); + if (finalize_acl_after_change(acl, op->kind, false, opts->no_mask, + errbuf, errbufsz) != 0) + return (-1); + break; + case OP_REMOVE_INDEX: + if (apply_remove_index(acl, op->index, errbuf, errbufsz) != 0) + return (-1); + if (finalize_acl_after_change(acl, op->kind, false, opts->no_mask, + errbuf, errbufsz) != 0) + return (-1); + break; + case OP_REMOVE_ALL: + case OP_REMOVE_DEFAULT: + break; + } + + if (op->kind == ACL_KIND_DEFAULT) + state->default_dirty = true; + else + state->access_dirty = true; + return (0); +} + +static int +process_single_path(const char *path, bool follow, const struct operation_list *ops, + const struct options *opts) +{ + struct file_state state; + char errbuf[256]; + size_t i; + + memset(&state, 0, sizeof(state)); + state.path = path; + state.follow = follow; + acl_list_init(&state.access_acl); + acl_list_init(&state.default_acl); + + if (stat_path(path, follow, &state.st) != 0) { + report_errno(path); + goto fail; + } + state.is_dir = S_ISDIR(state.st.st_mode); + if (!follow && S_ISLNK(state.st.st_mode)) { + report_path_error(path, + "symbolic link ACLs are not supported on Linux"); + goto fail; + } + + for (i = 0; i < ops->count; i++) { + if (apply_operation(&state, &ops->ops[i], opts, errbuf, + sizeof(errbuf)) != 0) { + report_path_error(path, "%s", errbuf); + goto fail; + } + } + + if (persist_file_state(&state, errbuf, sizeof(errbuf)) != 0) { + report_path_error(path, "%s", errbuf); + goto fail; + } + + acl_list_free(&state.access_acl); + acl_list_free(&state.default_acl); + return (0); + +fail: + acl_list_free(&state.access_acl); + acl_list_free(&state.default_acl); + return (1); +} + +static int +process_path_recursive(const char *path, bool follow_root, bool follow_children, + const struct operation_list *ops, const struct options *opts, + struct visited_set *visited) +{ + struct stat st; + int errors; + + errors = 0; + if (stat_path(path, follow_root, &st) != 0) { + report_errno(path); + return (1); + } + + if (visited_contains(visited, st.st_dev, st.st_ino)) { + report_path_error(path, "recursive directory cycle detected"); + return (1); + } + if (S_ISDIR(st.st_mode)) + visited_add(visited, st.st_dev, st.st_ino); + + errors += process_single_path(path, follow_root, ops, opts); + if (!S_ISDIR(st.st_mode)) + return (errors); + + { + DIR *dir; + struct dirent *de; + + dir = opendir(path); + if (dir == NULL) { + report_errno(path); + return (errors + 1); + } + while ((de = readdir(dir)) != NULL) { + char *child; + size_t path_len; + size_t name_len; + size_t total; + struct stat child_st; + + if (strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0) + continue; + path_len = strlen(path); + name_len = strlen(de->d_name); + total = path_len + 1 + name_len + 1; + child = xcalloc(total, 1); + memcpy(child, path, path_len); + child[path_len] = '/'; + memcpy(child + path_len + 1, de->d_name, name_len); + + if (stat_path(child, follow_children, &child_st) != 0) { + report_errno(child); + errors++; + free(child); + continue; + } + if (S_ISDIR(child_st.st_mode)) { + errors += process_path_recursive(child, follow_children, + follow_children, ops, opts, visited); + } else { + errors += process_single_path(child, follow_children, + ops, opts); + } + free(child); + } + if (closedir(dir) != 0) { + report_errno(path); + errors++; + } + } + + return (errors); +} + +static char ** +read_path_operands_from_stdin(struct parser_state *parser) +{ + char **items; + char *line; + size_t cap; + size_t count; + size_t linecap; + ssize_t linelen; + + if (parser->stdin_reserved) { + report_error("setfacl: cannot specify more than one stdin source"); + exit(1); + } + parser->stdin_reserved = true; + + cap = 8; + count = 0; + items = xcalloc(cap, sizeof(*items)); + line = NULL; + linecap = 0; + while ((linelen = getline(&line, &linecap, stdin)) != -1) { + char *text; + + while (linelen > 0 && + (line[linelen - 1] == '\n' || line[linelen - 1] == '\r')) { + line[--linelen] = '\0'; + } + text = trim_whitespace(line); + if (*text == '\0') { + report_error("setfacl: stdin: empty pathname"); + parser->stdin_path_error = true; + continue; + } + if (count + 1 >= cap) { + cap *= 2; + items = xreallocarray(items, cap, sizeof(*items)); + } + items[count++] = xstrdup(text); + } + free(line); + if (ferror(stdin) != 0) { + report_errno("stdin"); + exit(1); + } + items[count] = NULL; + return (items); +} + +static int +process_operand(const char *path, const struct operation_list *ops, + const struct options *opts, struct visited_set *visited) +{ + bool follow_root; + bool follow_children; + + if (!opts->recursive) + return (process_single_path(path, !opts->no_follow, ops, opts)); + + switch (opts->walk_mode) { + case WALK_LOGICAL: + follow_root = true; + follow_children = true; + break; + case WALK_HYBRID: + follow_root = true; + follow_children = false; + break; + case WALK_PHYSICAL: + default: + follow_root = false; + follow_children = false; + break; + } + + return (process_path_recursive(path, follow_root, follow_children, ops, opts, + visited)); +} + +int +main(int argc, char *argv[]) +{ + struct operation_list ops; + struct parser_state parser; + struct options opts; + struct visited_set visited; + char **paths; + int ch; + int errors; + int i; + + op_list_init(&ops); + parser.current_kind = ACL_KIND_ACCESS; + parser.stdin_reserved = false; + parser.stdin_path_error = false; + memset(&opts, 0, sizeof(opts)); + opts.walk_mode = WALK_PHYSICAL; + visited_init(&visited); + + while ((ch = getopt_long(argc, argv, "HLM:PRX:a:bdhkm:nx:", long_options, + NULL)) != -1) { + struct operation op; + char errbuf[256]; + char *end; + + memset(&op, 0, sizeof(op)); + acl_list_init(&op.spec.acl); + op.kind = parser.current_kind; + + switch (ch) { + case 'H': + opts.walk_mode = WALK_HYBRID; + break; + case 'L': + opts.walk_mode = WALK_LOGICAL; + break; + case 'M': + op.type = OP_MODIFY; + if (parse_acl_file(optarg, parser.current_kind, false, &parser, + &op.spec, errbuf, sizeof(errbuf)) != 0) { + report_error("setfacl: %s", errbuf); + acl_list_free(&op.spec.acl); + op_list_free(&ops); + visited_free(&visited); + return (1); + } + op_list_push(&ops, &op); + break; + case 'P': + opts.walk_mode = WALK_PHYSICAL; + break; + case 'R': + opts.recursive = true; + break; + case 'X': + op.type = OP_REMOVE_SPEC; + if (parse_acl_file(optarg, parser.current_kind, true, &parser, + &op.spec, errbuf, sizeof(errbuf)) != 0) { + report_error("setfacl: %s", errbuf); + acl_list_free(&op.spec.acl); + op_list_free(&ops); + visited_free(&visited); + return (1); + } + op_list_push(&ops, &op); + break; + case 'a': + report_error("setfacl: option -a is not supported on Linux " + "(NFSv4 ACLs have no Linux POSIX ACL equivalent)"); + acl_list_free(&op.spec.acl); + op_list_free(&ops); + visited_free(&visited); + return (1); + case 'b': + op.type = OP_REMOVE_ALL; + op_list_push(&ops, &op); + break; + case 'd': + parser.current_kind = ACL_KIND_DEFAULT; + acl_list_free(&op.spec.acl); + break; + case 'h': + opts.no_follow = true; + break; + case 'k': + op.type = OP_REMOVE_DEFAULT; + op.kind = ACL_KIND_DEFAULT; + op_list_push(&ops, &op); + break; + case 'm': + op.type = OP_MODIFY; + if (parse_acl_text_list(optarg, parser.current_kind, false, + &op.spec, errbuf, sizeof(errbuf)) != 0) { + report_error("setfacl: %s", errbuf); + acl_list_free(&op.spec.acl); + op_list_free(&ops); + visited_free(&visited); + return (1); + } + op_list_push(&ops, &op); + break; + case 'n': + opts.no_mask = true; + acl_list_free(&op.spec.acl); + break; + case 'x': + errno = 0; + op.index = strtoul(optarg, &end, 10); + if (*optarg != '\0' && *end == '\0' && errno == 0) { + op.type = OP_REMOVE_INDEX; + op_list_push(&ops, &op); + break; + } + op.type = OP_REMOVE_SPEC; + if (parse_acl_text_list(optarg, parser.current_kind, true, + &op.spec, errbuf, sizeof(errbuf)) != 0) { + report_error("setfacl: %s", errbuf); + acl_list_free(&op.spec.acl); + op_list_free(&ops); + visited_free(&visited); + return (1); + } + op_list_push(&ops, &op); + break; + default: + acl_list_free(&op.spec.acl); + op_list_free(&ops); + visited_free(&visited); + usage(); + } + } + + if (opts.recursive && opts.no_follow) { + report_error("setfacl: the -R and -h options may not be specified together"); + op_list_free(&ops); + visited_free(&visited); + return (1); + } + if (ops.count == 0) { + op_list_free(&ops); + visited_free(&visited); + usage(); + } + + argc -= optind; + argv += optind; + if (argc == 0 || (argc == 1 && strcmp(argv[0], "-") == 0)) + paths = read_path_operands_from_stdin(&parser); + else + paths = argv; + + errors = 0; + for (i = 0; paths[i] != NULL; i++) { + visited.count = 0; + errors += process_operand(paths[i], &ops, &opts, &visited); + } + if (parser.stdin_path_error) + errors++; + + if (paths != argv) { + for (i = 0; paths[i] != NULL; i++) + free(paths[i]); + free(paths); + } + op_list_free(&ops); + visited_free(&visited); + return (errors != 0 ? 1 : 0); +} diff --git a/corebinutils/setfacl/tests/test.sh b/corebinutils/setfacl/tests/test.sh new file mode 100644 index 0000000000..6a59159425 --- /dev/null +++ b/corebinutils/setfacl/tests/test.sh @@ -0,0 +1,257 @@ +#!/bin/sh +set -eu + +LC_ALL=C +export LC_ALL + +: "${SETFACL_BIN:?SETFACL_BIN is required}" + +ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +TMPBASE="$ROOT/.tmp-tests" +mkdir -p "$TMPBASE" +WORKDIR=$(mktemp -d "$TMPBASE/setfacl-test.XXXXXX") +STDOUT_FILE="$WORKDIR/stdout" +STDERR_FILE="$WORKDIR/stderr" +trap 'rm -rf "$WORKDIR"' EXIT INT TERM + +fail() { + printf '%s\n' "FAIL: $1" >&2 + exit 1 +} + +assert_eq() { + expected=$1 + actual=$2 + message=$3 + [ "$expected" = "$actual" ] || { + printf '%s\n' "FAIL: $message" >&2 + printf '%s\n' "--- expected ---" >&2 + printf '%s\n' "$expected" >&2 + printf '%s\n' "--- actual ---" >&2 + printf '%s\n' "$actual" >&2 + exit 1 + } +} + +assert_match() { + pattern=$1 + text=$2 + message=$3 + printf '%s\n' "$text" | grep -Eq "$pattern" || fail "$message" +} + +assert_not_match() { + pattern=$1 + text=$2 + message=$3 + if printf '%s\n' "$text" | grep -Eq "$pattern"; then + fail "$message" + fi +} + +assert_mode() { + expected=$1 + path=$2 + actual=$(stat -c '%a' "$path") + [ "$actual" = "$expected" ] || fail "$path mode expected $expected got $actual" +} + +run_cmd() { + : >"$STDOUT_FILE" + : >"$STDERR_FILE" + set +e + "$@" >"$STDOUT_FILE" 2>"$STDERR_FILE" + CMD_STATUS=$? + set -e + CMD_STDOUT=$(cat "$STDOUT_FILE") + CMD_STDERR=$(cat "$STDERR_FILE") +} + +run_stdin_cmd() { + input=$1 + shift + : >"$STDOUT_FILE" + : >"$STDERR_FILE" + set +e + printf '%s' "$input" | "$@" >"$STDOUT_FILE" 2>"$STDERR_FILE" + CMD_STATUS=$? + set -e + CMD_STDOUT=$(cat "$STDOUT_FILE") + CMD_STDERR=$(cat "$STDERR_FILE") +} + +read_acl_body() { + getfacl_bin=$1 + path=$2 + shift 2 + "$getfacl_bin" -cpE "$@" "$path" \ + | sed '/^#/d;/^$/d' \ + | sed 's/[[:space:]]\+/ /g' \ + | sort +} + +[ -x "$SETFACL_BIN" ] || fail "missing binary: $SETFACL_BIN" + +run_cmd "$SETFACL_BIN" -z +[ "$CMD_STATUS" -eq 1 ] || fail "invalid option should exit 1" +assert_match '^usage: setfacl ' "$CMD_STDERR" "usage output missing for invalid option" + +run_cmd "$SETFACL_BIN" -a 0 "u::rwx" /dev/null +[ "$CMD_STATUS" -eq 1 ] || fail "unsupported -a should exit 1" +assert_eq "setfacl: option -a is not supported on Linux (NFSv4 ACLs have no Linux POSIX ACL equivalent)" "$CMD_STDERR" "unsupported -a message mismatch" + +run_cmd "$SETFACL_BIN" -R -h -m "u::rwx,g::r-x,o::r-x" /dev/null +[ "$CMD_STATUS" -eq 1 ] || fail "-R with -h should exit 1" +assert_eq "setfacl: the -R and -h options may not be specified together" "$CMD_STDERR" "recursive symlink mode conflict message mismatch" + +touch "$WORKDIR/base" +chmod 600 "$WORKDIR/base" +run_cmd "$SETFACL_BIN" -m "u::rw,g::r,o::---" "$WORKDIR/base" +[ "$CMD_STATUS" -eq 0 ] || fail "base-only ACL modify should succeed" +assert_mode 640 "$WORKDIR/base" + +run_stdin_cmd "$(printf '\n%s\n' "$WORKDIR/base")" "$SETFACL_BIN" -m "u::rw,g::r,o::---" +[ "$CMD_STATUS" -eq 1 ] || fail "empty stdin pathname should report failure" +assert_match '^setfacl: stdin: empty pathname$' "$CMD_STDERR" "empty stdin pathname message missing" +assert_mode 640 "$WORKDIR/base" + +ln -s "$WORKDIR/base" "$WORKDIR/base-link" +run_cmd "$SETFACL_BIN" -h -m "u::rw,g::r,o::---" "$WORKDIR/base-link" +[ "$CMD_STATUS" -eq 1 ] || fail "symlink -h should fail on Linux" +assert_eq "setfacl: $WORKDIR/base-link: symbolic link ACLs are not supported on Linux" "$CMD_STDERR" "symlink -h message mismatch" + +mkdir -p "$WORKDIR/tree/sub" +touch "$WORKDIR/tree/sub/file" +chmod 700 "$WORKDIR/tree" +chmod 600 "$WORKDIR/tree/sub/file" +chmod 700 "$WORKDIR/tree/sub" +run_cmd "$SETFACL_BIN" -R -m "u::rwx,g::rx,o::rx" "$WORKDIR/tree" +[ "$CMD_STATUS" -eq 0 ] || fail "recursive base-only ACL modify should succeed" +assert_mode 755 "$WORKDIR/tree" +assert_mode 755 "$WORKDIR/tree/sub" +assert_mode 755 "$WORKDIR/tree/sub/file" + +mkdir "$WORKDIR/realdir" +touch "$WORKDIR/realdir/file" +chmod 700 "$WORKDIR/realdir" +chmod 600 "$WORKDIR/realdir/file" +ln -s "$WORKDIR/realdir" "$WORKDIR/realdir-link" +run_cmd "$SETFACL_BIN" -R -H -m "u::rwx,g::rx,o::rx" "$WORKDIR/realdir-link" +[ "$CMD_STATUS" -eq 0 ] || fail "-R -H should follow command-line symlink" +assert_mode 755 "$WORKDIR/realdir" +assert_mode 755 "$WORKDIR/realdir/file" + +run_cmd "$SETFACL_BIN" -m "owner@:rwx::allow" "$WORKDIR/base" +[ "$CMD_STATUS" -eq 1 ] || fail "NFSv4 ACL syntax should be rejected" +assert_eq "setfacl: NFSv4 ACL entries are not supported on Linux" "$CMD_STDERR" "NFSv4 rejection message mismatch" + +GETFACL_BIN= +if command -v getfacl >/dev/null 2>&1; then + GETFACL_BIN=$(command -v getfacl) +elif [ -x "$ROOT/../getfacl/out/getfacl" ]; then + GETFACL_BIN="$ROOT/../getfacl/out/getfacl" +fi + +acl_supported=0 +if [ -n "$GETFACL_BIN" ] && command -v setfacl >/dev/null 2>&1; then + touch "$WORKDIR/probe" + chmod 640 "$WORKDIR/probe" + if setfacl -m "u:$(id -u):rw-" "$WORKDIR/probe" 2>/dev/null; then + if "$GETFACL_BIN" -cpE "$WORKDIR/probe" 2>/dev/null | + grep -Eq "^user:($(id -u)|$(id -un)):rw-$"; then + acl_supported=1 + fi + fi +fi + +if [ "$acl_supported" -eq 1 ]; then + uid=$(id -u) + + touch "$WORKDIR/ours-access" "$WORKDIR/sys-access" + chmod 640 "$WORKDIR/ours-access" "$WORKDIR/sys-access" + + run_cmd "$SETFACL_BIN" -m "u:$uid:rw-" "$WORKDIR/ours-access" + [ "$CMD_STATUS" -eq 0 ] || fail "extended ACL modify failed" + setfacl -m "u:$uid:rw-" "$WORKDIR/sys-access" + assert_eq "$(read_acl_body "$GETFACL_BIN" "$WORKDIR/sys-access")" "$(read_acl_body "$GETFACL_BIN" "$WORKDIR/ours-access")" "extended ACL modify diverges from system setfacl" + + run_cmd "$SETFACL_BIN" -n -m "g::r--" "$WORKDIR/ours-access" + [ "$CMD_STATUS" -eq 0 ] || fail "no-mask ACL modify failed" + setfacl -n -m "g::r--" "$WORKDIR/sys-access" + assert_eq "$(read_acl_body "$GETFACL_BIN" "$WORKDIR/sys-access")" "$(read_acl_body "$GETFACL_BIN" "$WORKDIR/ours-access")" "no-mask modify diverges from system setfacl" + + run_cmd "$SETFACL_BIN" -x "u:$uid" "$WORKDIR/ours-access" + [ "$CMD_STATUS" -eq 0 ] || fail "ACL entry removal failed" + setfacl -x "u:$uid" "$WORKDIR/sys-access" + assert_eq "$(read_acl_body "$GETFACL_BIN" "$WORKDIR/sys-access")" "$(read_acl_body "$GETFACL_BIN" "$WORKDIR/ours-access")" "ACL entry removal diverges from system setfacl" + + run_cmd "$SETFACL_BIN" -m "u:$uid:rw-" "$WORKDIR/ours-access" + [ "$CMD_STATUS" -eq 0 ] || fail "ACL entry re-add failed for index removal test" + run_cmd "$SETFACL_BIN" -x 1 "$WORKDIR/ours-access" + [ "$CMD_STATUS" -eq 0 ] || fail "index-based ACL removal failed" + assert_not_match "^user:($(id -u)|$(id -un)):rw-$" "$(read_acl_body "$GETFACL_BIN" "$WORKDIR/ours-access")" "index-based removal left named user entry behind" + + run_cmd "$SETFACL_BIN" -x 99 "$WORKDIR/ours-access" + [ "$CMD_STATUS" -eq 1 ] || fail "out-of-range index removal should fail" + assert_eq "setfacl: $WORKDIR/ours-access: ACL entry index 99 is out of range" "$CMD_STDERR" "out-of-range index removal message mismatch" + + run_cmd "$SETFACL_BIN" -b "$WORKDIR/ours-access" + [ "$CMD_STATUS" -eq 0 ] || fail "remove-all should succeed" + setfacl -b "$WORKDIR/sys-access" + assert_eq "$(read_acl_body "$GETFACL_BIN" "$WORKDIR/sys-access")" "$(read_acl_body "$GETFACL_BIN" "$WORKDIR/ours-access")" "remove-all diverges from system setfacl" + + spec_file="$WORKDIR/spec.txt" + cat >"$spec_file" <<EOF +# comment + user:$uid:rw- + group::r-- +EOF + run_cmd "$SETFACL_BIN" -M "$spec_file" "$WORKDIR/ours-access" + [ "$CMD_STATUS" -eq 0 ] || fail "ACL modify from file failed" + setfacl -M "$spec_file" "$WORKDIR/sys-access" + assert_eq "$(read_acl_body "$GETFACL_BIN" "$WORKDIR/sys-access")" "$(read_acl_body "$GETFACL_BIN" "$WORKDIR/ours-access")" "ACL modify-file diverges from system setfacl" + + remove_file="$WORKDIR/remove.txt" + cat >"$remove_file" <<EOF +user:$uid +EOF + run_cmd "$SETFACL_BIN" -X "$remove_file" "$WORKDIR/ours-access" + [ "$CMD_STATUS" -eq 0 ] || fail "ACL remove-file failed" + setfacl -X "$remove_file" "$WORKDIR/sys-access" + assert_eq "$(read_acl_body "$GETFACL_BIN" "$WORKDIR/sys-access")" "$(read_acl_body "$GETFACL_BIN" "$WORKDIR/ours-access")" "ACL remove-file diverges from system setfacl" + + run_cmd "$SETFACL_BIN" -n -m "u:$uid:rw-" "$WORKDIR/base" + [ "$CMD_STATUS" -eq 1 ] || fail "named entry without mask under -n should fail" + assert_eq "setfacl: $WORKDIR/base: ACL requires a mask entry when -n is used" "$CMD_STDERR" "missing-mask error mismatch" + + run_cmd "$SETFACL_BIN" -x "u:0" "$WORKDIR/base" + [ "$CMD_STATUS" -eq 1 ] || fail "removing missing ACL entry should fail" + assert_eq "setfacl: $WORKDIR/base: cannot remove non-existent ACL entry" "$CMD_STDERR" "missing removal entry message mismatch" + + mkdir "$WORKDIR/ours-dir" "$WORKDIR/sys-dir" + run_cmd "$SETFACL_BIN" -d -m "u::rwx,g::r-x,o::---,u:$uid:rwx" "$WORKDIR/ours-dir" + [ "$CMD_STATUS" -eq 0 ] || fail "default ACL modify failed" + setfacl -d -m "u::rwx,g::r-x,o::---,u:$uid:rwx" "$WORKDIR/sys-dir" + assert_eq "$(read_acl_body "$GETFACL_BIN" "$WORKDIR/sys-dir")" "$(read_acl_body "$GETFACL_BIN" "$WORKDIR/ours-dir")" "default ACL modify diverges from system setfacl" + + run_cmd "$SETFACL_BIN" -k "$WORKDIR/ours-dir" + [ "$CMD_STATUS" -eq 0 ] || fail "default ACL removal failed" + setfacl -k "$WORKDIR/sys-dir" + assert_eq "$(read_acl_body "$GETFACL_BIN" "$WORKDIR/sys-dir")" "$(read_acl_body "$GETFACL_BIN" "$WORKDIR/ours-dir")" "default ACL removal diverges from system setfacl" + + default_remove_file="$WORKDIR/default-remove.txt" + cat >"$default_remove_file" <<EOF +user:$uid +EOF + run_cmd "$SETFACL_BIN" -d -X "$default_remove_file" "$WORKDIR/ours-dir" + [ "$CMD_STATUS" -eq 1 ] || fail "removing a missing entry from an empty default ACL should fail" + assert_eq "setfacl: $WORKDIR/ours-dir: cannot remove non-existent ACL entry" "$CMD_STDERR" "empty default ACL follow-up error mismatch" + + run_cmd "$SETFACL_BIN" -d -m "u:$uid:rwx" "$WORKDIR/ours-dir" + [ "$CMD_STATUS" -eq 1 ] || fail "incomplete default ACL should fail" + assert_eq "setfacl: $WORKDIR/ours-dir: ACL is missing required user:: entry" "$CMD_STDERR" "incomplete default ACL message mismatch" +else + printf '%s\n' "SKIP: extended ACL checks (system setfacl/getfacl unavailable or filesystem lacks POSIX ACL support)" >&2 +fi + +printf '%s\n' "PASS" |
