From 7291fba1dfe34531883e4c56e3f26dd942f5c9f5 Mon Sep 17 00:00:00 2001
From: Gionatan Danti <g.danti@assyoma.it>
Date: Fri, 31 Jan 2025 22:07:30 +0100
Subject: [PATCH] Add receive:append permission for limited receive

Force receive (zfs receive -F) can rollback or destroy snapshots and
file systems that do not exist on the sending side (see zfs-receive man
page). This means an user having the receive permission can effectively
delete data on receiving side, even if such user does not have explicit
rollback or destroy permissions.

This patch adds the receive:append permission, which only permits
limited, non-forced receive. Behavior for users with full receive
permission is not changed in any way.

Fixes https://github.com/openzfs/zfs/issues/16943

Signed-off-by: Gionatan Danti <g.danti@assyoma.it>
---
 cmd/zfs/zfs_main.c                            |  1 +
 include/sys/dsl_deleg.h                       |  1 +
 man/man8/zfs-allow.8                          |  3 +-
 module/zcommon/zfs_deleg.c                    |  1 +
 module/zfs/zfs_ioctl.c                        | 13 +++++-
 .../delegate/delegate_common.kshlib           | 42 +++++++++++++++++++
 .../functional/delegate/zfs_allow_010_pos.ksh |  5 ++-
 7 files changed, 62 insertions(+), 4 deletions(-)

diff --git a/cmd/zfs/zfs_main.c b/cmd/zfs/zfs_main.c
index 73ccf72d263c..d8cbf8f5aa4c 100644
--- a/cmd/zfs/zfs_main.c
+++ b/cmd/zfs/zfs_main.c
@@ -5292,6 +5292,7 @@ zfs_do_receive(int argc, char **argv)
 #define	ZFS_DELEG_PERM_SHARE		"share"
 #define	ZFS_DELEG_PERM_SEND		"send"
 #define	ZFS_DELEG_PERM_RECEIVE		"receive"
+#define	ZFS_DELEG_PERM_RECEIVE_APPEND	"receive:append"
 #define	ZFS_DELEG_PERM_ALLOW		"allow"
 #define	ZFS_DELEG_PERM_USERPROP		"userprop"
 #define	ZFS_DELEG_PERM_VSCAN		"vscan" /* ??? */
diff --git a/include/sys/dsl_deleg.h b/include/sys/dsl_deleg.h
index d6abac90bbcc..0761b0745065 100644
--- a/include/sys/dsl_deleg.h
+++ b/include/sys/dsl_deleg.h
@@ -46,6 +46,7 @@ extern "C" {
 #define	ZFS_DELEG_PERM_SHARE		"share"
 #define	ZFS_DELEG_PERM_SEND		"send"
 #define	ZFS_DELEG_PERM_RECEIVE		"receive"
+#define	ZFS_DELEG_PERM_RECEIVE_APPEND	"receive:append"
 #define	ZFS_DELEG_PERM_ALLOW		"allow"
 #define	ZFS_DELEG_PERM_USERPROP		"userprop"
 #define	ZFS_DELEG_PERM_VSCAN		"vscan"
diff --git a/man/man8/zfs-allow.8 b/man/man8/zfs-allow.8
index d26984317c2e..3b65befda832 100644
--- a/man/man8/zfs-allow.8
+++ b/man/man8/zfs-allow.8
@@ -207,7 +207,7 @@ load-key	subcommand	Allows loading and unloading of encryption key (see \fBzfs l
 change-key	subcommand	Allows changing an encryption key via \fBzfs change-key\fR.
 mount	subcommand	Allows mounting/umounting ZFS datasets
 promote	subcommand	Must also have the \fBmount\fR and \fBpromote\fR ability in the origin file system
-receive	subcommand	Must also have the \fBmount\fR and \fBcreate\fR ability
+receive	subcommand	Must also have the \fBmount\fR and \fBcreate\fR ability, required for \fBzfs receive -F\fR (see also \fBreceive:append\fR for limited, non forced receive)
 release	subcommand	Allows releasing a user hold which might destroy the snapshot
 rename	subcommand	Must also have the \fBmount\fR and \fBcreate\fR ability in the new parent
 rollback	subcommand	Must also have the \fBmount\fR ability
@@ -215,6 +215,7 @@ send	subcommand
 share	subcommand	Allows sharing file systems over NFS or SMB protocols
 snapshot	subcommand	Must also have the \fBmount\fR ability
 
+receive:append	other	Must also have the \fBmount\fR and \fBcreate\fR ability, limited receive ability (can not do receive -F)
 groupquota	other	Allows accessing any \fBgroupquota@\fI…\fR property
 groupobjquota	other	Allows accessing any \fBgroupobjquota@\fI…\fR property
 groupused	other	Allows reading any \fBgroupused@\fI…\fR property
diff --git a/module/zcommon/zfs_deleg.c b/module/zcommon/zfs_deleg.c
index f977c761147d..05b71a9643a2 100644
--- a/module/zcommon/zfs_deleg.c
+++ b/module/zcommon/zfs_deleg.c
@@ -52,6 +52,7 @@ const zfs_deleg_perm_tab_t zfs_deleg_perm_tab[] = {
 	{ZFS_DELEG_PERM_MOUNT},
 	{ZFS_DELEG_PERM_PROMOTE},
 	{ZFS_DELEG_PERM_RECEIVE},
+	{ZFS_DELEG_PERM_RECEIVE_APPEND},
 	{ZFS_DELEG_PERM_RENAME},
 	{ZFS_DELEG_PERM_ROLLBACK},
 	{ZFS_DELEG_PERM_SNAPSHOT},
diff --git a/module/zfs/zfs_ioctl.c b/module/zfs/zfs_ioctl.c
index b1b0ae54460b..2d1ba3c67ed4 100644
--- a/module/zfs/zfs_ioctl.c
+++ b/module/zfs/zfs_ioctl.c
@@ -900,9 +900,18 @@ zfs_secpolicy_recv(zfs_cmd_t *zc, nvlist_t *innvl, cred_t *cr)
 	(void) innvl;
 	int error;
 
+	/*
+	 * zfs receive -F requires full receive permission,
+	 * otherwise receive:append permission is enough
+	 */
 	if ((error = zfs_secpolicy_write_perms(zc->zc_name,
-	    ZFS_DELEG_PERM_RECEIVE, cr)) != 0)
-		return (error);
+	    ZFS_DELEG_PERM_RECEIVE, cr)) != 0) {
+		if (zc->zc_guid || nvlist_exists(innvl, "force"))
+			return (error);
+		if ((error = zfs_secpolicy_write_perms(zc->zc_name,
+		    ZFS_DELEG_PERM_RECEIVE_APPEND, cr)) != 0)
+			return (error);
+	}
 
 	if ((error = zfs_secpolicy_write_perms(zc->zc_name,
 	    ZFS_DELEG_PERM_MOUNT, cr)) != 0)
diff --git a/tests/zfs-tests/tests/functional/delegate/delegate_common.kshlib b/tests/zfs-tests/tests/functional/delegate/delegate_common.kshlib
index 5ddb6ca2ddc8..8e628b8e4382 100644
--- a/tests/zfs-tests/tests/functional/delegate/delegate_common.kshlib
+++ b/tests/zfs-tests/tests/functional/delegate/delegate_common.kshlib
@@ -256,6 +256,9 @@ function check_fs_perm
 		receive)
 			verify_fs_receive $user $perm $fs
 			;;
+		receive:append)
+			verify_fs_receive_append $user $perm $fs
+			;;
 		*)
 			common_perm $user $perm $fs
 			;;
@@ -425,6 +428,45 @@ function verify_fs_receive
 	return 0
 }
 
+function verify_fs_receive_append
+{
+	typeset user=$1
+	typeset perm=$2
+	typeset fs=$3
+
+	typeset dtst
+	typeset stamp=${perm}.${user}.$RANDOM
+	typeset newfs=$fs/newfs.$stamp
+	typeset bak_user=$TEST_BASE_DIR/bak.$user.$stamp
+
+	log_must zfs create $newfs
+	typeset dtst="$newfs"
+
+	typeset dtstsnap=$dtst@snap.$stamp
+	log_must zfs snapshot $dtstsnap
+
+	log_must eval "zfs send $dtstsnap > $bak_user"
+	log_must_busy zfs destroy -rf $dtst
+
+	log_must zfs allow $user create,mount,canmount $fs
+	user_run $user eval "zfs receive -o canmount=off -F $dtst < $bak_user"
+	log_must zfs unallow $user create,mount,canmount $fs
+	if datasetexists $dtstsnap ; then
+		return 1
+	fi
+
+	log_must zfs allow $user create,mount,canmount $fs
+	user_run $user eval "zfs receive -o canmount=off $dtst < $bak_user"
+	log_must zfs unallow $user create,mount,canmount $fs
+	if ! datasetexists $dtstsnap ; then
+		return 1
+	fi
+
+	rm -rf $bak_user
+
+	return 0
+}
+
 function verify_userprop
 {
 	typeset user=$1
diff --git a/tests/zfs-tests/tests/functional/delegate/zfs_allow_010_pos.ksh b/tests/zfs-tests/tests/functional/delegate/zfs_allow_010_pos.ksh
index 549928697edd..22406c72f82a 100755
--- a/tests/zfs-tests/tests/functional/delegate/zfs_allow_010_pos.ksh
+++ b/tests/zfs-tests/tests/functional/delegate/zfs_allow_010_pos.ksh
@@ -86,7 +86,8 @@ set -A perms	create		true		false	\
 		clone		true		true	\
 		promote		true		true	\
 		xattr		true		false	\
-		receive		true		false
+		receive		true		false	\
+		receive:append	true		false
 
 elif is_freebsd; then
 #				Results in	Results in
@@ -126,6 +127,7 @@ set -A perms	create		true		false	\
 		rename		true		true	\
 		promote		true		true	\
 		receive		true		false   \
+		receive:append	true		false	\
 		destroy		true		true
 
 else
@@ -160,6 +162,7 @@ set -A perms	create		true		false	\
 		zoned		true		false	\
 		xattr		true		false	\
 		receive		true		false	\
+		receive:append	true		false	\
 		destroy		true		true
 
 if is_global_zone; then