diff --git a/libfrog/Makefile b/libfrog/Makefile
index 0b5b23893..acddc894e 100644
--- a/libfrog/Makefile
+++ b/libfrog/Makefile
@@ -20,6 +20,7 @@ convert.c \
 crc32.c \
 file_exchange.c \
 fsgeom.c \
+fsproperties.c \
 getparents.c \
 histogram.c \
 list_sort.c \
@@ -47,6 +48,7 @@ dahashselftest.h \
 div64.h \
 file_exchange.h \
 fsgeom.h \
+fsproperties.h \
 getparents.h \
 histogram.h \
 logging.h \
@@ -60,6 +62,11 @@ workqueue.h
 
 LSRCFILES += gen_crc32table.c
 
+ifeq ($(HAVE_LIBATTR),yes)
+CFILES+=fsprops.c
+HFILES+=fsprops.h
+endif
+
 LDIRT = gen_crc32table crc32table.h
 
 default: ltdepend $(LTLIBRARY)
diff --git a/libfrog/fsproperties.c b/libfrog/fsproperties.c
new file mode 100644
index 000000000..c317d15c1
--- /dev/null
+++ b/libfrog/fsproperties.c
@@ -0,0 +1,39 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (c) 2024 Oracle.  All Rights Reserved.
+ * Author: Darrick J. Wong <djwong@kernel.org>
+ */
+#include <string.h>
+#include "xfs.h"
+#include "libfrog/fsgeom.h"
+#include "libfrog/fsproperties.h"
+#include "list.h"
+
+/* Return the offset of a string in a string table, or -1 if not found. */
+static inline int
+__fsprops_lookup(
+	const char	*values[],
+	unsigned int	nr_values,
+	const char	*value)
+{
+	unsigned int	i;
+
+	for (i = 0; i < nr_values; i++) {
+		if (values[i] && !strcmp(value, values[i]))
+			return i;
+	}
+
+	return -1;
+}
+
+#define fsprops_lookup(values, value) \
+	__fsprops_lookup((values), ARRAY_SIZE(values), (value))
+
+/* Return true if a fs property name=value tuple is allowed. */
+bool
+fsprop_validate(
+	const char	*name,
+	const char	*value)
+{
+	return true;
+}
diff --git a/libfrog/fsproperties.h b/libfrog/fsproperties.h
new file mode 100644
index 000000000..b1ac4cdd7
--- /dev/null
+++ b/libfrog/fsproperties.h
@@ -0,0 +1,53 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (c) 2024 Oracle.  All Rights Reserved.
+ * Author: Darrick J. Wong <djwong@kernel.org>
+ */
+#ifndef __LIBFROG_FSPROPERTIES_H__
+#define __LIBFROG_FSPROPERTIES_H__
+
+/* Name space for filesystem properties. */
+#define FSPROP_NAMESPACE	"trusted."
+
+/*
+ * All filesystem property xattr names must have this string after the
+ * namespace.  For example, the VFS xattr calls should use the name
+ * "trusted.xfs:fubar".  The xfs xattr ioctls would set ATTR_ROOT and use the
+ * name "xfs:fubar".
+ */
+#define FSPROP_NAME_PREFIX	"xfs:"
+
+/* Maximum size the value of a filesystem property. */
+#define FSPROP_MAX_VALUELEN	(65536)
+
+static inline int
+fsprop_name_to_attr_name(
+	const char		*prop_name,
+	char			**attr_name)
+{
+	return asprintf(attr_name, FSPROP_NAME_PREFIX "%s", prop_name);
+}
+
+static inline const char *
+attr_name_to_fsprop_name(
+	const char		*attr_name)
+{
+	const size_t		bytes = sizeof(FSPROP_NAME_PREFIX) - 1;
+	unsigned int		i;
+
+	for (i = 0; i < bytes; i++) {
+		if (attr_name[i] == 0)
+			return NULL;
+	}
+
+	if (memcmp(attr_name, FSPROP_NAME_PREFIX, bytes) != 0)
+		return NULL;
+
+	return attr_name + bytes;
+}
+
+bool fsprop_validate(const char *name, const char *value);
+
+/* Specific Filesystem Properties */
+
+#endif /* __LIBFROG_FSPROPERTIES_H__ */
diff --git a/libfrog/fsprops.c b/libfrog/fsprops.c
new file mode 100644
index 000000000..88046b7a0
--- /dev/null
+++ b/libfrog/fsprops.c
@@ -0,0 +1,202 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (c) 2024 Oracle.  All Rights Reserved.
+ * Author: Darrick J. Wong <djwong@kernel.org>
+ */
+#include "xfs.h"
+#include "handle.h"
+#include "libfrog/fsgeom.h"
+#include "libfrog/paths.h"
+#include "libfrog/bulkstat.h"
+#include "libfrog/fsprops.h"
+#include "libfrog/fsproperties.h"
+
+#include <attr/attributes.h>
+
+/*
+ * Given an xfd and a mount table path, get us the handle for the root dir so
+ * we can set filesystem properties.
+ */
+int
+fsprops_open_handle(
+	struct xfs_fd		*xfd,
+	struct fs_path		*fs_path,
+	struct fsprops_handle	*fph)
+{
+	struct xfs_bulkstat	bulkstat;
+	struct stat		sb;
+	int			ret;
+
+	/* fs properties only supported on V5 filesystems */
+	if (!(xfd->fsgeom.flags & XFS_FSOP_GEOM_FLAGS_V5SB)) {
+		errno = EOPNOTSUPP;
+		return -1;
+	}
+
+	ret = -xfrog_bulkstat_single(xfd, XFS_BULK_IREQ_SPECIAL_ROOT,
+			XFS_BULK_IREQ_SPECIAL, &bulkstat);
+	if (ret)
+		return ret;
+
+	ret = fstat(xfd->fd, &sb);
+	if (ret)
+		return ret;
+
+	if (sb.st_ino != bulkstat.bs_ino) {
+		errno = ESRMNT;
+		return -1;
+	}
+
+	return fd_to_handle(xfd->fd, &fph->hanp, &fph->hlen);
+}
+
+/* Free a fsproperties handle. */
+void
+fsprops_free_handle(
+	struct fsprops_handle	*fph)
+{
+	if (fph->hanp)
+		free_handle(fph->hanp, fph->hlen);
+	fph->hanp = NULL;
+	fph->hlen = 0;
+}
+
+/* Call the given callback on every fs property accessible through the handle. */
+int
+fsprops_walk_names(
+	struct fsprops_handle	*fph,
+	fsprops_name_walk_fn	walk_fn,
+	void			*priv)
+{
+	struct attrlist_cursor	cur = { };
+	char			attrbuf[XFS_XATTR_LIST_MAX];
+	struct attrlist		*attrlist = (struct attrlist *)attrbuf;
+	int			ret;
+
+	memset(attrbuf, 0, XFS_XATTR_LIST_MAX);
+
+	while ((ret = attr_list_by_handle(fph->hanp, fph->hlen, attrbuf,
+				XFS_XATTR_LIST_MAX, XFS_IOC_ATTR_ROOT,
+				&cur)) == 0) {
+		unsigned int	i;
+
+		for (i = 0; i < attrlist->al_count; i++) {
+			struct attrlist_ent	*ent = ATTR_ENTRY(attrlist, i);
+			const char		*p =
+				attr_name_to_fsprop_name(ent->a_name);
+
+			if (p) {
+				ret = walk_fn(fph, p, ent->a_valuelen, priv);
+				if (ret)
+					break;
+			}
+		}
+
+		if (!attrlist->al_more)
+			break;
+	}
+
+	return ret;
+}
+
+/* Retrieve the value of a specific fileystem property. */
+int
+fsprops_get(
+	struct fsprops_handle	*fph,
+	const char		*name,
+	void			*valuebuf,
+	size_t			*valuelen)
+{
+	struct xfs_attr_multiop	ops = {
+		.am_opcode	= ATTR_OP_GET,
+		.am_attrvalue	= valuebuf,
+		.am_length	= *valuelen,
+		.am_flags	= XFS_IOC_ATTR_ROOT,
+	};
+	int			ret;
+
+	ret = fsprop_name_to_attr_name(name, (char **)&ops.am_attrname);
+	if (ret < 0)
+		return ret;
+
+	ret = attr_multi_by_handle(fph->hanp, fph->hlen, &ops, 1, 0);
+	if (ret < 0)
+		goto out_name;
+
+	if (ops.am_error) {
+		errno = -ops.am_error;
+		ret = -1;
+		goto out_name;
+	}
+
+	*valuelen = ops.am_length;
+out_name:
+	free(ops.am_attrname);
+	return ret;
+}
+
+/* Set the value of a specific fileystem property. */
+int
+fsprops_set(
+	struct fsprops_handle	*fph,
+	const char		*name,
+	void			*valuebuf,
+	size_t			valuelen)
+{
+	struct xfs_attr_multiop	ops = {
+		.am_opcode	= ATTR_OP_SET,
+		.am_attrvalue	= valuebuf,
+		.am_length	= valuelen,
+		.am_flags	= XFS_IOC_ATTR_ROOT,
+	};
+	int			ret;
+
+	ret = fsprop_name_to_attr_name(name, (char **)&ops.am_attrname);
+	if (ret < 0)
+		return ret;
+
+	ret = attr_multi_by_handle(fph->hanp, fph->hlen, &ops, 1, 0);
+	if (ret < 0)
+		goto out_name;
+
+	if (ops.am_error) {
+		errno = -ops.am_error;
+		ret = -1;
+		goto out_name;
+	}
+
+out_name:
+	free(ops.am_attrname);
+	return ret;
+}
+
+/* Unset the value of a specific fileystem property. */
+int
+fsprops_remove(
+	struct fsprops_handle	*fph,
+	const char		*name)
+{
+	struct xfs_attr_multiop	ops = {
+		.am_opcode	= ATTR_OP_REMOVE,
+		.am_flags	= XFS_IOC_ATTR_ROOT,
+	};
+	int			ret;
+
+	ret = fsprop_name_to_attr_name(name, (char **)&ops.am_attrname);
+	if (ret < 0)
+		return ret;
+
+	ret = attr_multi_by_handle(fph->hanp, fph->hlen, &ops, 1, 0);
+	if (ret < 0)
+		goto out_name;
+
+	if (ops.am_error) {
+		errno = -ops.am_error;
+		ret = -1;
+		goto out_name;
+	}
+
+out_name:
+	free(ops.am_attrname);
+	return ret;
+}
diff --git a/libfrog/fsprops.h b/libfrog/fsprops.h
new file mode 100644
index 000000000..9276f2425
--- /dev/null
+++ b/libfrog/fsprops.h
@@ -0,0 +1,34 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (c) 2024 Oracle.  All Rights Reserved.
+ * Author: Darrick J. Wong <djwong@kernel.org>
+ */
+#ifndef __LIBFROG_FSPROPS_H__
+#define __LIBFROG_FSPROPS_H__
+
+/* Edit and view filesystem property sets. */
+
+struct fsprops_handle {
+	void	*hanp;
+	size_t	hlen;
+};
+
+struct xfs_fd;
+struct fs_path;
+
+int fsprops_open_handle(struct xfs_fd *xfd, struct fs_path *fspath,
+		struct fsprops_handle *fph);
+void fsprops_free_handle(struct fsprops_handle *fph);
+
+typedef int (*fsprops_name_walk_fn)(struct fsprops_handle *fph,
+		const char *name, size_t valuelen, void *priv);
+
+int fsprops_walk_names(struct fsprops_handle *fph, fsprops_name_walk_fn walk_fn,
+		void *priv);
+int fsprops_get(struct fsprops_handle *fph, const char *name, void *attrbuf,
+		size_t *attrlen);
+int fsprops_set(struct fsprops_handle *fph, const char *name, void *attrbuf,
+		size_t attrlen);
+int fsprops_remove(struct fsprops_handle *fph, const char *name);
+
+#endif /* __LIBFROG_FSPROPS_H__ */
