git: 22fba3a9d641 - main - find: add -xattr and -xttrname

From: Kyle Evans <kevans_at_FreeBSD.org>
Date: Thu, 09 Apr 2026 02:41:22 UTC
The branch main has been updated by kevans:

URL: https://cgit.FreeBSD.org/src/commit/?id=22fba3a9d64140d80a9e2093cfc02c9c503b2e19

commit 22fba3a9d64140d80a9e2093cfc02c9c503b2e19
Author:     Kyle Evans <kevans@FreeBSD.org>
AuthorDate: 2026-04-09 02:41:12 +0000
Commit:     Kyle Evans <kevans@FreeBSD.org>
CommitDate: 2026-04-09 02:41:12 +0000

    find: add -xattr and -xttrname
    
    We use -xattr in our openrsync tests for convenience, and it seems like
    a good addition to FreeBSD.  -xattr and -xattrname will both consult all
    available namespaces by default, but -xattrname allows filtering by
    namespace using a "user:" or "system:" prefix.
    
    Inspired by:    https://github.com/apple-oss-distributions/shell_cmds
    Reviewed by:    kib, rmacklem
    Sponsored by:   Klara, Inc.
    Differential Revision:  https://reviews.freebsd.org/D55286
---
 usr.bin/find/extern.h           |  2 +
 usr.bin/find/find.1             | 23 +++++++++-
 usr.bin/find/function.c         | 94 +++++++++++++++++++++++++++++++++++++++++
 usr.bin/find/option.c           |  2 +
 usr.bin/find/tests/find_test.sh | 80 +++++++++++++++++++++++++++++++++++
 5 files changed, 199 insertions(+), 2 deletions(-)

diff --git a/usr.bin/find/extern.h b/usr.bin/find/extern.h
index 02c85d06a34c..250b5fb6689c 100644
--- a/usr.bin/find/extern.h
+++ b/usr.bin/find/extern.h
@@ -122,6 +122,8 @@ exec_f	f_sparse;
 exec_f	f_type;
 exec_f	f_user;
 exec_f	f_writable;
+exec_f	f_xattr;
+exec_f	f_xattrname;
 
 extern int ftsoptions, ignore_readdir_race, isdepth, isoutput;
 extern int issort, isxargs;
diff --git a/usr.bin/find/find.1 b/usr.bin/find/find.1
index 98521a98762d..afe10a36607d 100644
--- a/usr.bin/find/find.1
+++ b/usr.bin/find/find.1
@@ -28,7 +28,7 @@
 .\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 .\" SUCH DAMAGE.
 .\"
-.Dd July 26, 2025
+.Dd February 14, 2026
 .Dt FIND 1
 .Os
 .Sh NAME
@@ -976,6 +976,23 @@ This test makes use of the
 .Xr access 2
 system call, and so can be fooled by NFS servers which do UID mapping (or root-squashing).
 This is a GNU find extension.
+.It Ic -xattr
+Matches files which have extended attributes set in any supported namespace.
+.It Ic -xattrname Ar xattr
+Matches files which have the specified
+.Ar xattr
+extended attribute set.
+All supported namespaces are searched by default, but
+.Ar xattr
+may be prefixed with
+.Dq user:
+or
+.Dq system:
+to filter by namespace.
+.Pp
+Note that named attributes are not supported, only extended attributes as set
+by, e.g.,
+.Xr setextattr 8 .
 .El
 .Sh OPERATORS
 The primaries may be combined using the following operators.
@@ -1245,6 +1262,7 @@ section below for details.
 .Xr whereis 1 ,
 .Xr which 1 ,
 .Xr xargs 1 ,
+.Xr extattr 2 ,
 .Xr stat 2 ,
 .Xr acl 3 ,
 .Xr fts 3 ,
@@ -1253,7 +1271,8 @@ section below for details.
 .Xr strmode 3 ,
 .Xr ascii 7 ,
 .Xr re_format 7 ,
-.Xr symlink 7
+.Xr symlink 7 ,
+.Xr setextattr 8
 .Sh STANDARDS
 The
 .Nm
diff --git a/usr.bin/find/function.c b/usr.bin/find/function.c
index b260a71ef4a9..c62ac39a9e82 100644
--- a/usr.bin/find/function.c
+++ b/usr.bin/find/function.c
@@ -37,6 +37,7 @@
 #include <sys/stat.h>
 #include <sys/types.h>
 #include <sys/acl.h>
+#include <sys/extattr.h>
 #include <sys/wait.h>
 #include <sys/mount.h>
 
@@ -49,6 +50,7 @@
 #include <limits.h>
 #include <pwd.h>
 #include <regex.h>
+#include <stdbool.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
@@ -57,6 +59,8 @@
 
 #include "find.h"
 
+static const char * const xattr_ns[] = EXTATTR_NAMESPACE_NAMES;
+
 static PLAN *palloc(OPTION *);
 static long long find_parsenum(PLAN *, const char *, char *, char *);
 static long long find_parsetime(PLAN *, const char *, char *);
@@ -1752,6 +1756,96 @@ c_user(OPTION *option, char ***argvp)
 	return new;
 }
 
+/*
+ * -xattr functions --
+ *
+ *	True if the entry has any extended attribute in any namespace.
+ */
+int
+f_xattr(PLAN *plan __unused, FTSENT *entry)
+{
+	ssize_t asz;
+	bool deref_link;
+
+	deref_link = (ftsoptions & FTS_LOGICAL) != 0;
+	if (entry->fts_level == 0 && (ftsoptions & FTS_COMFOLLOW) != 0)
+		deref_link = true;
+
+	for (size_t ns = 0; ns < nitems(xattr_ns); ns++) {
+		if (ns == EXTATTR_NAMESPACE_EMPTY)
+			continue;
+
+		if (deref_link)
+			asz = extattr_list_file(entry->fts_accpath, ns, NULL, 0);
+		else
+			asz = extattr_list_link(entry->fts_accpath, ns, NULL, 0);
+		if (asz > 0)
+			return 1;
+	}
+
+	return 0;
+}
+
+static bool
+find_has_xattr(const char *path, int ns, const char *aname, bool deref_link)
+{
+	size_t asz;
+
+	if (deref_link)
+		asz = extattr_get_file(path, ns, aname, NULL, 0);
+	else
+		asz = extattr_get_link(path, ns, aname, NULL, 0);
+
+	return asz != (size_t)-1;
+}
+
+/*
+ * -xattrname xattr functions --
+ *
+ *	True if the entry has the given extended attribute xattr.  The xattr
+ *	may be prefixed with "user:" or "system:" to scope the search
+ *	explicitly, otherwise we assume the user namespace is requested.
+ */
+int
+f_xattrname(PLAN *plan, FTSENT *entry)
+{
+	const char *aname;
+	bool deref_link;
+
+	deref_link = (ftsoptions & FTS_LOGICAL) != 0;
+	if (entry->fts_level == 0 && (ftsoptions & FTS_COMFOLLOW) != 0)
+		deref_link = true;
+
+	aname = plan->c_data;
+	for (size_t ns = 0; ns < nitems(xattr_ns); ns++) {
+		const char *name;
+		size_t namelen;
+
+		if (ns == EXTATTR_NAMESPACE_EMPTY)
+			continue;
+
+		name = xattr_ns[ns];
+		namelen = strlen(xattr_ns[ns]);
+		if (strncmp(aname, name, namelen) == 0 &&
+		    aname[namelen] == ':') {
+			aname += namelen + 1;
+			return find_has_xattr(entry->fts_accpath, ns, aname,
+			    deref_link);
+		}
+	}
+
+	for (size_t ns = 0; ns < nitems(xattr_ns); ns++) {
+		if (ns == EXTATTR_NAMESPACE_EMPTY)
+			continue;
+
+		if (find_has_xattr(entry->fts_accpath, ns, aname,
+		    deref_link))
+			return 1;
+	}
+
+	return 0;
+}
+
 /*
  * -xdev functions --
  *
diff --git a/usr.bin/find/option.c b/usr.bin/find/option.c
index fa09231a3152..fe3d9b00f90f 100644
--- a/usr.bin/find/option.c
+++ b/usr.bin/find/option.c
@@ -162,6 +162,8 @@ static OPTION const options[] = {
 	{ "-user",	c_user,		f_user,		0 },
 	{ "-wholename",	c_name,		f_path,		0 },
 	{ "-writable",	c_simple,	f_writable,	0 },
+	{ "-xattr",	c_simple,	f_xattr,	0 },
+	{ "-xattrname",	c_name,		f_xattrname,	0 },
 	{ "-xdev",	c_xdev,		f_always_true,	0 },
 // -xtype
 };
diff --git a/usr.bin/find/tests/find_test.sh b/usr.bin/find/tests/find_test.sh
index 99d2f6af4d45..deb6a66a8dfb 100755
--- a/usr.bin/find/tests/find_test.sh
+++ b/usr.bin/find/tests/find_test.sh
@@ -174,9 +174,89 @@ find_printf_body()
 	    find -s dir -printf '%Te\n'
 }
 
+atf_test_case find_xattr
+find_xattr_head()
+{
+	atf_set "descr" "Test the -xattr primary"
+}
+find_xattr_body()
+{
+	mkdir dir
+	ln -s dir dirlink
+
+	# No xattrs here
+	atf_check find dir -xattr
+	atf_check find dirlink -xattr
+
+	# Set one on the directory and be sure that we also dereference symlinks
+	# as appropriate with -H/-L.
+	if ! setextattr user find_test.attr val dir; then
+		atf_skip "Failed to set xattr (not supported on this fs?)"
+	fi
+
+	atf_check -o match:"dir$" find dir -xattr
+	atf_check -o match:"dirlink$" find -H dirlink -xattr
+	atf_check -o match:"dirlink$" find -L dirlink -xattr
+
+	atf_check -o match:"dir$" -o match:"dirlink" find -sL . -xattr
+	atf_check -o match:"dir$" -o not-match:"dirlink$" find -sH . -xattr
+	atf_check -o match:"dir$" -o not-match:"dirlink$" find -s . -xattr
+}
+
+atf_test_case find_xattrname
+find_xattrname_head()
+{
+	atf_set "descr" "Test the -xattrname primary"
+	atf_set "require.user" "root"
+}
+find_xattrname_body()
+{
+	touch foo bar baz none
+
+	ln -s foo link
+	if ! setextattr user find_test.special1 val foo; then
+		atf_skip "Failed to set xattr (not supported on this fs?)"
+	fi
+
+	atf_check setextattr user find_test.special2 val bar
+	atf_check setextattr user find_test.special2 val baz
+
+	# We want an unqualified 'find_test.special2' search to find all three
+	# of these, while 'user:' and 'system:' filter appropriately.
+	atf_check setextattr system find_test.special2 val foo
+
+	atf_check find . -xattrname 'find_test.special3'
+
+	# Be sure that we get symlink dereferencing right, so that one can use
+	# -H/-L/-P to get the right behavior.
+	atf_check -o match:foo -o not-match:"bar|baz|link|none" \
+	    find . -xattrname 'find_test.special1'
+	atf_check -o match:foo -o match:link \
+	    find -H foo link -xattrname 'find_test.special1'
+	atf_check -o match:foo -o match:link -o not-match:"bar|baz|none" \
+	    find -L . -xattrname 'find_test.special1'
+
+	atf_check -o match:foo -o match:bar -o match:baz \
+	    -o not-match:"none|link" find . -xattrname 'find_test.special2'
+	atf_check -o not-match:"foo|none|link" -o match:bar -o match:baz \
+	    find . -xattrname 'user:find_test.special2'
+	atf_check -o match:foo -o not-match:"bar|baz|none|link" \
+	    find . -xattrname 'system:find_test.special2'
+
+	# Now set an extattr on the link itself and be sure that find(1) can
+	# detect it.  With -L, we shouldn't see anything with a special3 xattr
+	# as symlinks are dereferenced.
+	atf_check setextattr -h user find_test.special3 val link
+	atf_check -o match:link find . -xattrname "find_test.special3"
+	atf_check find -L . -xattrname "find_test.special3"
+	atf_check find -H link -xattrname "find_test.special3"
+}
+
 atf_init_test_cases()
 {
 	atf_add_test_case find_newer_link
 	atf_add_test_case find_samefile_link
 	atf_add_test_case find_printf
+	atf_add_test_case find_xattr
+	atf_add_test_case find_xattrname
 }