all lists on lists.proxmox.com
 help / color / mirror / Atom feed
* [PATCH zfsonlinux 0/2] fix #8178, #7653
@ 2026-10-20 14:02 Stoiko Ivanov
  2026-10-20 14:02 ` [PATCH zfsonlinux 1/2] fix #8178: d/patches: add patches already queued for zfs-2.4.5 Stoiko Ivanov
  2026-10-20 14:02 ` [PATCH zfsonlinux 2/2] fix #7653: d/patches: cherry-pick fix for stale page-cache on receive/rollback Stoiko Ivanov
  0 siblings, 2 replies; 3+ messages in thread
From: Stoiko Ivanov @ 2026-10-20 14:02 UTC (permalink / raw)
  To: pve-devel

Both issues seem to have found fixes upstream recently.
for #7653 this is a single commit merged to upstream master [0], which
can be picked to 2.4.4 cleanly,
while for #8178 there were a few clean-ups upstream, which are already
queued up for 2.4.5 upstream [1].

I picked all patches applied to zfs-2.4.5-staging upstream for #8178, as
they were either a prerequisite for a clean pick of the fix or did not
seem too invasive.

minimally tested on 2 VMs with replicated guests, but did not manage or try
to reproduce the reported issues.

[0] https://github.com/openzfs/zfs/pull/19013
[1] https://github.com/openzfs/zfs/pull/19244

Stoiko Ivanov (2):
  fix #8178: d/patches: add patches already queued for zfs-2.4.5
  fix #7653: d/patches: cherry-pick fix for stale page-cache on
    receive/rollback

 ...the-project-ID-for-every-object-type.patch | 209 +++++++++
 ...thin-a-single-directory-under-projec.patch |  74 ++++
 ...t-ID-inheritance-and-renames-within-.patch | 208 +++++++++
 ...uf_hdr-s-anon-state-invariant-checks.patch | 113 +++++
 ...-support-for-building-akmod-packages.patch | 344 +++++++++++++++
 ...k-delivery-of-SIGUSR1-in-send_worker.patch |  93 ++++
 ...I-baseline-naming-on-RHEL-to-prevent.patch | 417 ++++++++++++++++++
 ...ader-destruction-for-in-flight-L2-wr.patch | 119 +++++
 ...2HDR-in-arc_release-for-in-flight-wr.patch | 174 ++++++++
 ...etween-arc_release-and-arc_read_done.patch | 179 ++++++++
 ...inux-drop-cached-pages-in-zfs_rezget.patch |  53 +++
 debian/patches/series                         |  11 +
 12 files changed, 1994 insertions(+)
 create mode 100644 debian/patches/0011-Inherit-the-project-ID-for-every-object-type.patch
 create mode 100644 debian/patches/0012-Allow-renames-within-a-single-directory-under-projec.patch
 create mode 100644 debian/patches/0013-ZTS-check-project-ID-inheritance-and-renames-within-.patch
 create mode 100644 debian/patches/0014-arc-harness-buf_hdr-s-anon-state-invariant-checks.patch
 create mode 100644 debian/patches/0015-rpm-Add-support-for-building-akmod-packages.patch
 create mode 100644 debian/patches/0016-libzfs_core-block-delivery-of-SIGUSR1-in-send_worker.patch
 create mode 100644 debian/patches/0017-kmodtool-use-kABI-baseline-naming-on-RHEL-to-prevent.patch
 create mode 100644 debian/patches/0018-L2ARC-Reorder-header-destruction-for-in-flight-L2-wr.patch
 create mode 100644 debian/patches/0019-L2ARC-Preserve-L2HDR-in-arc_release-for-in-flight-wr.patch
 create mode 100644 debian/patches/0020-arc-fix-race-between-arc_release-and-arc_read_done.patch
 create mode 100644 debian/patches/0021-Linux-drop-cached-pages-in-zfs_rezget.patch

-- 
2.47.3





^ permalink raw reply	[flat|nested] 3+ messages in thread

* [PATCH zfsonlinux 1/2] fix #8178: d/patches: add patches already queued for zfs-2.4.5
  2026-10-20 14:02 [PATCH zfsonlinux 0/2] fix #8178, #7653 Stoiko Ivanov
@ 2026-10-20 14:02 ` Stoiko Ivanov
  2026-10-20 14:02 ` [PATCH zfsonlinux 2/2] fix #7653: d/patches: cherry-pick fix for stale page-cache on receive/rollback Stoiko Ivanov
  1 sibling, 0 replies; 3+ messages in thread
From: Stoiko Ivanov @ 2026-10-20 14:02 UTC (permalink / raw)
  To: pve-devel

https://bugzilla.proxmox.com/show_bug.cgi?id=8178 should be fixed
in upstream master by
 216de07d8 ("arc: fix race between arc_release() and arc_read_done()")

This and needed prerequisites were already applied to
zfs-2.4.5-staging.
as the remaining patches don't look problematic (in not touching
functionality commonly used in our products) - simply pull
all patches since zfs-2.4.4 on zfs-2.4.5-staging.

Signed-off-by: Stoiko Ivanov <s.ivanov@proxmox.com>
---
 ...the-project-ID-for-every-object-type.patch | 209 +++++++++
 ...thin-a-single-directory-under-projec.patch |  74 ++++
 ...t-ID-inheritance-and-renames-within-.patch | 208 +++++++++
 ...uf_hdr-s-anon-state-invariant-checks.patch | 113 +++++
 ...-support-for-building-akmod-packages.patch | 344 +++++++++++++++
 ...k-delivery-of-SIGUSR1-in-send_worker.patch |  93 ++++
 ...I-baseline-naming-on-RHEL-to-prevent.patch | 417 ++++++++++++++++++
 ...ader-destruction-for-in-flight-L2-wr.patch | 119 +++++
 ...2HDR-in-arc_release-for-in-flight-wr.patch | 174 ++++++++
 ...etween-arc_release-and-arc_read_done.patch | 179 ++++++++
 debian/patches/series                         |  10 +
 11 files changed, 1940 insertions(+)
 create mode 100644 debian/patches/0011-Inherit-the-project-ID-for-every-object-type.patch
 create mode 100644 debian/patches/0012-Allow-renames-within-a-single-directory-under-projec.patch
 create mode 100644 debian/patches/0013-ZTS-check-project-ID-inheritance-and-renames-within-.patch
 create mode 100644 debian/patches/0014-arc-harness-buf_hdr-s-anon-state-invariant-checks.patch
 create mode 100644 debian/patches/0015-rpm-Add-support-for-building-akmod-packages.patch
 create mode 100644 debian/patches/0016-libzfs_core-block-delivery-of-SIGUSR1-in-send_worker.patch
 create mode 100644 debian/patches/0017-kmodtool-use-kABI-baseline-naming-on-RHEL-to-prevent.patch
 create mode 100644 debian/patches/0018-L2ARC-Reorder-header-destruction-for-in-flight-L2-wr.patch
 create mode 100644 debian/patches/0019-L2ARC-Preserve-L2HDR-in-arc_release-for-in-flight-wr.patch
 create mode 100644 debian/patches/0020-arc-fix-race-between-arc_release-and-arc_read_done.patch

diff --git a/debian/patches/0011-Inherit-the-project-ID-for-every-object-type.patch b/debian/patches/0011-Inherit-the-project-ID-for-every-object-type.patch
new file mode 100644
index 000000000..aa7af3992
--- /dev/null
+++ b/debian/patches/0011-Inherit-the-project-ID-for-every-object-type.patch
@@ -0,0 +1,209 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Matt Turner <mattst88@gmail.com>
+Date: Fri, 14 Aug 2026 14:59:08 -0400
+Subject: [PATCH] Inherit the project ID for every object type
+
+zfs_mknode() only assigned a project ID to regular files and
+directories, so a symlink, device node, FIFO or socket created inside a
+directory with ZFS_PROJINHERIT set was left at ZFS_DEFAULT_PROJID. The
+cross-project checks in zfs_rename() and zfs_link() compare the object's
+project ID against the directory's, so such an object is treated as
+foreign to the very directory holding it, and cannot be renamed or
+linked there at all -- "ln -sfn", which creates the new symlink under a
+temporary name and renames it into place, fails with EXDEV.
+
+ext4 and XFS store a project ID on every inode type and so do not have
+this problem. Do the same, and quota-check new symlinks and rename
+whiteouts against the inherited ID rather than the default one, so their
+space is accounted to the project that owns them.
+
+This changes accounting for newly created objects: symlinks, device
+nodes and FIFOs now consume the project's quota where they previously
+consumed none, so on a dataset already at its project quota, creating
+one begins to fail with EDQUOT where it used to succeed. The space
+involved is small, but the change in behavior is visible.
+
+The ZFS_PROJINHERIT flag itself keeps propagating to regular files and
+directories only, leaving lsattr(1) output unchanged.
+
+Note that this only covers newly created objects; symlinks already on
+disk keep the default project ID.
+
+Reviewed-by: Brian Behlendorf <behlendorf1@llnl.gov>
+Signed-off-by: Matt Turner <mattst88@gmail.com>
+Closes #18932
+(cherry picked from commit 7f0d8d3d1fdf8a1c0711350b7ec6f9b6a571c647)
+Signed-off-by: Stoiko Ivanov <s.ivanov@proxmox.com>
+---
+ module/os/freebsd/zfs/zfs_vnops_os.c |  5 ++--
+ module/os/freebsd/zfs/zfs_znode_os.c | 36 ++++++++++++++++------------
+ module/os/linux/zfs/zfs_vnops_os.c   | 15 ++++--------
+ module/os/linux/zfs/zfs_znode_os.c   | 36 ++++++++++++++++------------
+ 4 files changed, 49 insertions(+), 43 deletions(-)
+
+diff --git a/module/os/freebsd/zfs/zfs_vnops_os.c b/module/os/freebsd/zfs/zfs_vnops_os.c
+index 0080986787cc19481ada9a411ee7307522faa762..f1278f00bdc067f536442b4abb24c6d538f6a413 100644
+--- a/module/os/freebsd/zfs/zfs_vnops_os.c
++++ b/module/os/freebsd/zfs/zfs_vnops_os.c
+@@ -1119,8 +1119,7 @@ zfs_create(znode_t *dzp, const char *name, vattr_t *vap, int excl, int mode,
+ 	    cr, vsecp, &acl_ids, NULL)) != 0)
+ 		goto out;
+ 
+-	if (S_ISREG(vap->va_mode) || S_ISDIR(vap->va_mode))
+-		projid = zfs_inherit_projid(dzp);
++	projid = zfs_inherit_projid(dzp);
+ 	if (zfs_acl_ids_overquota(zfsvfs, &acl_ids, projid)) {
+ 		zfs_acl_ids_free(&acl_ids);
+ 		error = SET_ERROR(EDQUOT);
+@@ -3659,7 +3658,7 @@ zfs_symlink(znode_t *dzp, const char *name, vattr_t *vap,
+ 		return (error);
+ 	}
+ 
+-	if (zfs_acl_ids_overquota(zfsvfs, &acl_ids, ZFS_DEFAULT_PROJID)) {
++	if (zfs_acl_ids_overquota(zfsvfs, &acl_ids, zfs_inherit_projid(dzp))) {
+ 		zfs_acl_ids_free(&acl_ids);
+ 		zfs_exit(zfsvfs, FTAG);
+ 		return (SET_ERROR(EDQUOT));
+diff --git a/module/os/freebsd/zfs/zfs_znode_os.c b/module/os/freebsd/zfs/zfs_znode_os.c
+index b9f427b397055c1e69856014206a402559438ef9..089a221b64a0b6f2fae26d9a7fd34ca3dfb2fe6e 100644
+--- a/module/os/freebsd/zfs/zfs_znode_os.c
++++ b/module/os/freebsd/zfs/zfs_znode_os.c
+@@ -667,22 +667,28 @@ zfs_mknode(znode_t *dzp, vattr_t *vap, dmu_tx_t *tx, cred_t *cr,
+ 	if (flag & IS_XATTR)
+ 		pflags |= ZFS_XATTR;
+ 
+-	if (vap->va_type == VREG || vap->va_type == VDIR) {
+-		/*
+-		 * With ZFS_PROJID flag, we can easily know whether there is
+-		 * project ID stored on disk or not. See zpl_get_file_info().
+-		 */
+-		if (obj_type != DMU_OT_ZNODE &&
+-		    dmu_objset_projectquota_enabled(zfsvfs->z_os))
+-			pflags |= ZFS_PROJID;
++	/*
++	 * With ZFS_PROJID flag, we can easily know whether there is
++	 * project ID stored on disk or not. See zpl_get_file_info().
++	 */
++	if (obj_type != DMU_OT_ZNODE &&
++	    dmu_objset_projectquota_enabled(zfsvfs->z_os))
++		pflags |= ZFS_PROJID;
+ 
+-		/*
+-		 * Inherit project ID from parent if required.
+-		 */
+-		projid = zfs_inherit_projid(dzp);
+-		if (dzp_pflags & ZFS_PROJINHERIT)
+-			pflags |= ZFS_PROJINHERIT;
+-	}
++	/*
++	 * Inherit project ID from parent if required.  Every object type
++	 * takes part, as ext4 and XFS do: an object that carried no project
++	 * ID of its own would be treated as belonging to a different project
++	 * than the directory holding it, so zfs_rename() and zfs_link()
++	 * would refuse it with EXDEV even within its own project.
++	 *
++	 * The ZFS_PROJINHERIT flag itself keeps passing to regular files and
++	 * directories only, as before, so that lsattr(1) output is unchanged.
++	 */
++	projid = zfs_inherit_projid(dzp);
++	if ((vap->va_type == VREG || vap->va_type == VDIR) &&
++	    (dzp_pflags & ZFS_PROJINHERIT))
++		pflags |= ZFS_PROJINHERIT;
+ 
+ 	/*
+ 	 * No execs denied will be determined when zfs_mode_compute() is called.
+diff --git a/module/os/linux/zfs/zfs_vnops_os.c b/module/os/linux/zfs/zfs_vnops_os.c
+index cb6321e4f454cbb9fd361f71230499eefe54faa6..4557b8916bca4f06aa9159c6c689b9b1d477d896 100644
+--- a/module/os/linux/zfs/zfs_vnops_os.c
++++ b/module/os/linux/zfs/zfs_vnops_os.c
+@@ -736,8 +736,7 @@ top:
+ 			goto out;
+ 		have_acl = B_TRUE;
+ 
+-		if (S_ISREG(vap->va_mode) || S_ISDIR(vap->va_mode))
+-			projid = zfs_inherit_projid(dzp);
++		projid = zfs_inherit_projid(dzp);
+ 		if (zfs_acl_ids_overquota(zfsvfs, &acl_ids, projid)) {
+ 			zfs_acl_ids_free(&acl_ids);
+ 			error = SET_ERROR(EDQUOT);
+@@ -930,8 +929,7 @@ top:
+ 		goto out;
+ 	have_acl = B_TRUE;
+ 
+-	if (S_ISREG(vap->va_mode) || S_ISDIR(vap->va_mode))
+-		projid = zfs_inherit_projid(dzp);
++	projid = zfs_inherit_projid(dzp);
+ 	if (zfs_acl_ids_overquota(zfsvfs, &acl_ids, projid)) {
+ 		zfs_acl_ids_free(&acl_ids);
+ 		error = SET_ERROR(EDQUOT);
+@@ -3057,11 +3055,8 @@ top:
+ 
+ 	/* Set up inode creation for RENAME_WHITEOUT. */
+ 	if (rflags & RENAME_WHITEOUT) {
+-		/*
+-		 * Whiteout files are not regular files or directories, so to
+-		 * match zfs_create() we do not inherit the project id.
+-		 */
+-		uint64_t wo_projid = ZFS_DEFAULT_PROJID;
++		/* Match zfs_create(): the whiteout joins its directory. */
++		uint64_t wo_projid = zfs_inherit_projid(sdzp);
+ 
+ 		error = zfs_zaccess(sdzp, ACE_ADD_FILE, 0, B_FALSE, cr, mnt_ns);
+ 		if (error)
+@@ -3389,7 +3384,7 @@ top:
+ 		return (error);
+ 	}
+ 
+-	if (zfs_acl_ids_overquota(zfsvfs, &acl_ids, ZFS_DEFAULT_PROJID)) {
++	if (zfs_acl_ids_overquota(zfsvfs, &acl_ids, zfs_inherit_projid(dzp))) {
+ 		zfs_acl_ids_free(&acl_ids);
+ 		zfs_dirent_unlock(dl);
+ 		zfs_exit(zfsvfs, FTAG);
+diff --git a/module/os/linux/zfs/zfs_znode_os.c b/module/os/linux/zfs/zfs_znode_os.c
+index dd6bd21a60425b07297ef2d35c6fe90c9ff4dcbe..a106081702c4d0ae12a0004c60d0be055fe65bd5 100644
+--- a/module/os/linux/zfs/zfs_znode_os.c
++++ b/module/os/linux/zfs/zfs_znode_os.c
+@@ -775,22 +775,28 @@ zfs_mknode(znode_t *dzp, vattr_t *vap, dmu_tx_t *tx, cred_t *cr,
+ 	if (flag & IS_XATTR)
+ 		pflags |= ZFS_XATTR;
+ 
+-	if (S_ISREG(vap->va_mode) || S_ISDIR(vap->va_mode)) {
+-		/*
+-		 * With ZFS_PROJID flag, we can easily know whether there is
+-		 * project ID stored on disk or not. See zpl_get_file_info().
+-		 */
+-		if (obj_type != DMU_OT_ZNODE &&
+-		    dmu_objset_projectquota_enabled(zfsvfs->z_os))
+-			pflags |= ZFS_PROJID;
++	/*
++	 * With ZFS_PROJID flag, we can easily know whether there is
++	 * project ID stored on disk or not. See zpl_get_file_info().
++	 */
++	if (obj_type != DMU_OT_ZNODE &&
++	    dmu_objset_projectquota_enabled(zfsvfs->z_os))
++		pflags |= ZFS_PROJID;
+ 
+-		/*
+-		 * Inherit project ID from parent if required.
+-		 */
+-		projid = zfs_inherit_projid(dzp);
+-		if (dzp->z_pflags & ZFS_PROJINHERIT)
+-			pflags |= ZFS_PROJINHERIT;
+-	}
++	/*
++	 * Inherit project ID from parent if required.  Every object type
++	 * takes part, as ext4 and XFS do: an object that carried no project
++	 * ID of its own would be treated as belonging to a different project
++	 * than the directory holding it, so zfs_rename() and zfs_link()
++	 * would refuse it with EXDEV even within its own project.
++	 *
++	 * The ZFS_PROJINHERIT flag itself keeps passing to regular files and
++	 * directories only, as before, so that lsattr(1) output is unchanged.
++	 */
++	projid = zfs_inherit_projid(dzp);
++	if ((S_ISREG(vap->va_mode) || S_ISDIR(vap->va_mode)) &&
++	    (dzp->z_pflags & ZFS_PROJINHERIT))
++		pflags |= ZFS_PROJINHERIT;
+ 
+ 	/*
+ 	 * No execs denied will be determined when zfs_mode_compute() is called.
diff --git a/debian/patches/0012-Allow-renames-within-a-single-directory-under-projec.patch b/debian/patches/0012-Allow-renames-within-a-single-directory-under-projec.patch
new file mode 100644
index 000000000..98414213f
--- /dev/null
+++ b/debian/patches/0012-Allow-renames-within-a-single-directory-under-projec.patch
@@ -0,0 +1,74 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Matt Turner <mattst88@gmail.com>
+Date: Sat, 15 Aug 2026 22:05:41 -0400
+Subject: [PATCH] Allow renames within a single directory under project
+ inheritance
+
+A rename that keeps the object in the directory it already lives in
+cannot move it between projects, so refusing it with EXDEV is never
+right. Objects created before the previous commit carry no project ID of
+their own, which makes this reachable on existing pools: a symlink in a
+project directory cannot be renamed even to another name beside itself,
+and "ln -sfn" over an existing path fails.
+
+zfs_link() keeps its unconditional check. It has no source directory to
+compare against -- a hard link names an object that may live anywhere --
+so there is no equivalent "the object is already here" case to exempt.
+Linking a pre-existing symlink, device node or FIFO into the project
+directory that already holds it therefore still fails with EXDEV, until
+the object is given a project ID of its own.
+
+Reviewed-by: Brian Behlendorf <behlendorf1@llnl.gov>
+Signed-off-by: Matt Turner <mattst88@gmail.com>
+Closes #18932
+(cherry picked from commit 594332758f794fa9f969e9c72b6e93f0bf9f71af)
+Signed-off-by: Stoiko Ivanov <s.ivanov@proxmox.com>
+---
+ module/os/freebsd/zfs/zfs_vnops_os.c | 10 +++++++++-
+ module/os/linux/zfs/zfs_vnops_os.c   | 10 +++++++++-
+ 2 files changed, 18 insertions(+), 2 deletions(-)
+
+diff --git a/module/os/freebsd/zfs/zfs_vnops_os.c b/module/os/freebsd/zfs/zfs_vnops_os.c
+index f1278f00bdc067f536442b4abb24c6d538f6a413..91181ea86a450a43687c0af842b11d08ed2016be 100644
+--- a/module/os/freebsd/zfs/zfs_vnops_os.c
++++ b/module/os/freebsd/zfs/zfs_vnops_os.c
+@@ -3384,8 +3384,16 @@ zfs_do_rename_impl(vnode_t *sdvp, vnode_t **svpp, struct componentname *scnp,
+ 	 * not only the project ID, but also the ZFS_PROJINHERIT flag. Under
+ 	 * such case, we only allow renames into our tree when the project
+ 	 * IDs are the same.
++	 *
++	 * A rename within a single directory leaves the object exactly where
++	 * it already is, so it cannot move it between projects and is always
++	 * allowed.  Objects created before symlinks and other non-regular
++	 * files began inheriting a project ID carry none of their own, and
++	 * would otherwise not be renameable within the very directory that
++	 * holds them -- which breaks "ln -sfn", implemented as
++	 * create-under-a-temporary-name-then-rename.
+ 	 */
+-	if (tdzp->z_pflags & ZFS_PROJINHERIT &&
++	if (sdzp != tdzp && tdzp->z_pflags & ZFS_PROJINHERIT &&
+ 	    tdzp->z_projid != szp->z_projid) {
+ 		error = SET_ERROR(EXDEV);
+ 		goto out;
+diff --git a/module/os/linux/zfs/zfs_vnops_os.c b/module/os/linux/zfs/zfs_vnops_os.c
+index 4557b8916bca4f06aa9159c6c689b9b1d477d896..f27b877da06dd187f496102924fcc040efb38405 100644
+--- a/module/os/linux/zfs/zfs_vnops_os.c
++++ b/module/os/linux/zfs/zfs_vnops_os.c
+@@ -2993,8 +2993,16 @@ top:
+ 	 * not only the project ID, but also the ZFS_PROJINHERIT flag. Under
+ 	 * such case, we only allow renames into our tree when the project
+ 	 * IDs are the same.
++	 *
++	 * A rename within a single directory leaves the object exactly where
++	 * it already is, so it cannot move it between projects and is always
++	 * allowed.  Objects created before symlinks and other non-regular
++	 * files began inheriting a project ID carry none of their own, and
++	 * would otherwise not be renameable within the very directory that
++	 * holds them -- which breaks "ln -sfn", implemented as
++	 * create-under-a-temporary-name-then-rename.
+ 	 */
+-	if (tdzp->z_pflags & ZFS_PROJINHERIT &&
++	if (sdzp != tdzp && tdzp->z_pflags & ZFS_PROJINHERIT &&
+ 	    tdzp->z_projid != szp->z_projid) {
+ 		error = SET_ERROR(EXDEV);
+ 		goto out;
diff --git a/debian/patches/0013-ZTS-check-project-ID-inheritance-and-renames-within-.patch b/debian/patches/0013-ZTS-check-project-ID-inheritance-and-renames-within-.patch
new file mode 100644
index 000000000..aad0af67f
--- /dev/null
+++ b/debian/patches/0013-ZTS-check-project-ID-inheritance-and-renames-within-.patch
@@ -0,0 +1,208 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Matt Turner <mattst88@gmail.com>
+Date: Sat, 15 Aug 2026 22:07:58 -0400
+Subject: [PATCH] ZTS: check project ID inheritance and renames within a
+ project directory
+
+Covers both of the preceding changes. For the inheritance: a new symlink
+and a new FIFO each add an object to the containing directory's project,
+which only happens once non-regular files inherit a project ID, and a
+symlink can then be renamed into a different directory carrying the same
+project ID, which rename(2) permits only when the two project IDs match.
+
+For the rename exemption: a regular file and a symlink are created
+before the directory is tagged, so they carry no project ID of their
+own, as every symlink on an existing pool does. Renaming each of them
+within that directory afterwards fails with EXDEV unless the
+cross-project check exempts renames whose source and target directories
+are the same. The same renames are then repeated for objects created
+after the tagging, along with replacing a symlink with "ln -sfn", and
+again in an inheriting subdirectory.
+
+A rename that does cross into a different project is still refused.
+
+Renames use renameat2(1) rather than mv(1), which falls back to copying
+on EXDEV and would report success either way. The test is skipped where
+the renameat2(2) syscall is unavailable.
+
+Reviewed-by: Brian Behlendorf <behlendorf1@llnl.gov>
+Signed-off-by: Matt Turner <mattst88@gmail.com>
+Closes #18932
+(cherry picked from commit 2cf1bd209e43be2051dce28349bb171300be8c44)
+Signed-off-by: Stoiko Ivanov <s.ivanov@proxmox.com>
+---
+ tests/runfiles/linux.run                      |   1 +
+ tests/zfs-tests/tests/Makefile.am             |   1 +
+ .../projectquota/projectid_004_pos.ksh        | 139 ++++++++++++++++++
+ 3 files changed, 141 insertions(+)
+ create mode 100755 tests/zfs-tests/tests/functional/projectquota/projectid_004_pos.ksh
+
+diff --git a/tests/runfiles/linux.run b/tests/runfiles/linux.run
+index 2e2fb457585310db3a317bf0e8dc0862b14f860f..8615369dbaa0ae9355a2e982d6fec5266a84c541 100644
+--- a/tests/runfiles/linux.run
++++ b/tests/runfiles/linux.run
+@@ -193,6 +193,7 @@ tags = ['functional', 'procfs']
+ [tests/functional/projectquota:Linux]
+ tests = ['defaultprojectquota_001_pos', 'defaultprojectquota_005_pos',
+     'projectid_001_pos', 'projectid_002_pos', 'projectid_003_pos',
++    'projectid_004_pos',
+     'projectquota_001_pos', 'projectquota_003_pos', 'projectquota_006_pos',
+     'projectspace_001_pos', 'projectspace_002_pos', 'projectspace_003_pos',
+     'projectspace_004_pos', 'projectspace_005_pos', 'projecttree_001_pos']
+diff --git a/tests/zfs-tests/tests/Makefile.am b/tests/zfs-tests/tests/Makefile.am
+index f29ba9660662dad2959607572ed3e106c25a3e51..dcfa1ada5ea5b3dc756887e68cdb0ef53ddd152e 100644
+--- a/tests/zfs-tests/tests/Makefile.am
++++ b/tests/zfs-tests/tests/Makefile.am
+@@ -1835,6 +1835,7 @@ nobase_dist_datadir_zfs_tests_tests_SCRIPTS += \
+ 	functional/projectquota/projectid_001_pos.ksh \
+ 	functional/projectquota/projectid_002_pos.ksh \
+ 	functional/projectquota/projectid_003_pos.ksh \
++	functional/projectquota/projectid_004_pos.ksh \
+ 	functional/projectquota/projectquota_001_pos.ksh \
+ 	functional/projectquota/projectquota_002_pos.ksh \
+ 	functional/projectquota/projectquota_003_pos.ksh \
+diff --git a/tests/zfs-tests/tests/functional/projectquota/projectid_004_pos.ksh b/tests/zfs-tests/tests/functional/projectquota/projectid_004_pos.ksh
+new file mode 100755
+index 0000000000000000000000000000000000000000..08b020a550add991f248f76576a7bd507dfae856
+--- /dev/null
++++ b/tests/zfs-tests/tests/functional/projectquota/projectid_004_pos.ksh
+@@ -0,0 +1,139 @@
++#!/bin/ksh -p
++# SPDX-License-Identifier: CDDL-1.0
++#
++# This file and its contents are supplied under the terms of the
++# Common Development and Distribution License ("CDDL"), version 1.0.
++# You may only use this file in accordance with the terms of version
++# 1.0 of the CDDL.
++#
++# A full copy of the text of the CDDL should have accompanied this
++# source.  A copy of the CDDL is also available via the Internet at
++# https://opensource.org/license/CDDL-1.0.
++#
++
++#
++# Copyright (c) 2026 by Matt Turner. All rights reserved.
++#
++
++. $STF_SUITE/tests/functional/projectquota/projectquota_common.kshlib
++
++#
++# DESCRIPTION:
++#	Every object type inherits the project ID, and renaming within a
++#	project directory is always allowed
++#
++#
++# STRATEGY:
++#	1. Create a regular file and a symlink in a directory, then set the
++#	   directory's project ID with the inherit flag, leaving the two
++#	   objects carrying no project ID of their own
++#	2. Rename each of them within that directory, which has to be allowed
++#	   whatever project ID they carry
++#	3. Rename a regular file and a symlink created after the directory was
++#	   tagged, and replace a symlink with "ln -sfn", which creates the new
++#	   symlink under a temporary name and renames it into place
++#	4. Check that a new symlink and a new FIFO are accounted to the
++#	   directory's project, which they only are if they inherited its
++#	   project ID
++#	5. Rename a symlink into a different directory carrying the same
++#	   project ID, which rename(2) only permits if the symlink carries
++#	   that project ID too
++#	6. Check that a rename into a different project is still refused,
++#	   and that a symlink carrying no project ID cannot be renamed into
++#	   one, which shows the check in step 5 is reached and enforced
++#
++
++function cleanup
++{
++	log_must rm -rf $PRJDIR1 $PRJDIR2 $PRJDIR3 $TESTDIR/tlink6
++}
++
++if ! lsattr -pd > /dev/null 2>&1; then
++	log_unsupported "Current e2fsprogs does not support set/show project ID"
++fi
++
++# renameat2(1) is used throughout rather than mv(1), which falls back to
++# copying on EXDEV and would report success either way.
++if ! renameat2 -C; then
++	log_unsupported "renameat2 not supported on this (pre-3.15) linux kernel"
++fi
++
++log_onexit cleanup
++
++log_assert "Every object type inherits the project ID, and renaming within" \
++    "a project directory is always allowed"
++
++log_must mkdir $PRJDIR1
++
++#
++# Objects created before the directory was tagged carry no project ID of
++# their own, as every symlink on an existing pool does. This is the case
++# the same-directory exemption exists for: renaming within one directory
++# cannot move an object between projects, so it has to be allowed whatever
++# project ID the object carries -- including none at all.
++#
++log_must touch $PRJDIR1/tofile1
++log_must ln -s target $PRJDIR1/tolink1
++
++log_must chattr +P -p $PRJID1 $PRJDIR1
++
++log_must renameat2 $PRJDIR1/tofile1 $PRJDIR1/tofile2
++log_must renameat2 $PRJDIR1/tolink1 $PRJDIR1/tolink2
++
++# Objects created after the directory was tagged inherit its project ID, and
++# renaming those within it has to keep working too.
++log_must touch $PRJDIR1/tfile1
++log_must renameat2 $PRJDIR1/tfile1 $PRJDIR1/tfile2
++
++log_must ln -s target $PRJDIR1/tlink1
++log_must renameat2 $PRJDIR1/tlink1 $PRJDIR1/tlink2
++
++log_must ln -sfn other $PRJDIR1/tlink2
++log_must eval '[[ $(readlink $PRJDIR1/tlink2) == other ]]'
++
++# A subdirectory inherits both the project ID and the inherit flag, so it
++# has to behave the same way.
++log_must mkdir $PRJDIR1/subdir
++log_must ln -s target $PRJDIR1/subdir/tlink3
++log_must renameat2 $PRJDIR1/subdir/tlink3 $PRJDIR1/subdir/tlink4
++
++#
++# A symlink and a FIFO are not regular files or directories, and used to be
++# left at the default project ID. Creating one now adds an object to the
++# directory's project.
++#
++sync_pool
++typeset prj_bef=$(project_obj_count $QFS $PRJID1)
++
++log_must ln -s target $PRJDIR1/tlink5
++log_must mkfifo $PRJDIR1/tfifo1
++
++sync_pool
++typeset prj_aft=$(project_obj_count $QFS $PRJID1)
++
++[[ $prj_aft -eq $((prj_bef + 2)) ]] ||
++	log_fail "new value ($prj_aft) is NOT 2 larger than old one ($prj_bef)"
++
++#
++# rename(2) into a different directory is allowed only when the project IDs
++# match, so this passes only if the symlink really did inherit one.
++#
++log_must mkdir $PRJDIR2
++log_must chattr +P -p $PRJID1 $PRJDIR2
++log_must renameat2 $PRJDIR1/tlink5 $PRJDIR2/tlink5
++
++# Crossing into a different project is still refused.
++log_must mkdir $PRJDIR3
++log_must chattr +P -p $PRJID2 $PRJDIR3
++log_mustnot renameat2 $PRJDIR2/tlink5 $PRJDIR3/tlink5
++
++# And a symlink created outside any project carries no project ID, so it
++# cannot be renamed into one. This is what makes the check above meaningful:
++# it shows the cross-project test is reached and enforced, so the rename that
++# succeeded did so because the symlink had inherited a project ID.
++log_must ln -s target $TESTDIR/tlink6
++log_mustnot renameat2 $TESTDIR/tlink6 $PRJDIR1/tlink6
++log_must rm -f $TESTDIR/tlink6
++
++log_pass "Every object type inherits the project ID, and renaming within" \
++    "a project directory is always allowed"
diff --git a/debian/patches/0014-arc-harness-buf_hdr-s-anon-state-invariant-checks.patch b/debian/patches/0014-arc-harness-buf_hdr-s-anon-state-invariant-checks.patch
new file mode 100644
index 000000000..10a3ce347
--- /dev/null
+++ b/debian/patches/0014-arc-harness-buf_hdr-s-anon-state-invariant-checks.patch
@@ -0,0 +1,113 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Andriy Tkachuk <atkachuk@wasabi.com>
+Date: Thu, 27 Aug 2026 17:38:22 +0100
+Subject: [PATCH] arc: harness buf_hdr's anon state invariant checks
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+There are several places where hdr is checked for arc_anon state
+without the hash_lock, for example at arc_release() or at
+arc_buf_destroy(), and it's supposed that the hdr must not be
+in the hash table in this state and not having IO_IN_PROGRESS.
+However, those invariants are not asserted in release builds.
+But even if they would, there is one place in the current code
+where they would pass without noticing the problem.
+
+This place is at arc_write_done() where we first insert hdr
+into the hash table, then clear IO_IN_PROGRESS flag, then drop
+the reference and only then move its state from anon to mru:
+
+  7048  exists = buf_hash_insert(hdr, &hash_lock); /* still anon */
+        ...
+  7081  arc_hdr_clear_flags(hdr, ARC_FLAG_IO_IN_PROGRESS);
+  7082	VERIFY3S(remove_reference(hdr, hdr), >, 0); /* ref: 2 -> 1 */
+  7084  if (exists == NULL && hdr->b_l1hdr.b_state == arc_anon)
+  7085          arc_access(hdr, 0, B_FALSE);      /* anon -> MRU */
+  7086  mutex_exit(hash_lock);
+
+Let's imagine arc_buf_destroy() is called concurrently:
+
+- hdr->b_l1hdr.b_state == arc_anon → true, takes the fast path
+- ASSERT3P(hdr->b_l1hdr.b_buf, ==, buf) → passes
+- ASSERT(ARC_BUF_LAST(buf)) → passes
+- ASSERT(!HDR_IO_IN_PROGRESS(hdr)) → passes, cleared at 7081
+- VERIFY0(remove_reference(hdr, tag)) → refcnt 1→0, returns 0 → passes
+- remove_reference() sees state == arc_anon → arc_hdr_destroy(hdr)
+
+Every single guard is satisfied, and the header is kmem_cache_freed
+while still linked in its hash bucket. Could it be the root cause
+of #18782 kernel panic? We cannot prove this, but the harnessing
+we suggest here would allow to prove it.
+
+(Actually, since commit 023d44b9e VERIFY0P(hdr->b_hash_next) at
+arc_hdr_destroy() should catch it, but I think it would be better
+catching it earlier by the code and more explicitly.)
+
+So apart from promoting a couple asserts to verifies at
+arc_hdr_destroy() and arc_release(), like !HDR_IO_IN_PROGRESS(hdr)
+and !HDR_IN_HASH_TABLE(hdr), which are cheap, we also close the
+window at arc_write_done() which allows anon hdr to satisfy some
+of the assertions, and yet be in the hash table, by changing hdr's
+state from anon to MRU *before* clearing ARC_FLAG_IO_IN_PROGRESS
+flag and dropping reference on it.
+
+Reviewed-by: Alexander Motin <alexander.motin@TrueNAS.com>
+Reviewed-by: Brian Behlendorf <behlendorf1@llnl.gov>
+Signed-off-by: Andriy Tkachuk <atkachuk@wasabi.com>
+Closes #18968
+(cherry picked from commit b0fff48dc10459b781f18c905399a24db3dae529)
+(cherry picked from commit 57fbddf6bbcf9e130157de825785f2f9d9ffa6bb)
+Signed-off-by: Stoiko Ivanov <s.ivanov@proxmox.com>
+---
+ module/zfs/arc.c | 12 ++++++------
+ 1 file changed, 6 insertions(+), 6 deletions(-)
+
+diff --git a/module/zfs/arc.c b/module/zfs/arc.c
+index 34dad8b0e27e49c42281c6878fd12c9b2ad45179..4cd2a67eb2a85398627fad5531dc1aa20f1b2f5d 100644
+--- a/module/zfs/arc.c
++++ b/module/zfs/arc.c
+@@ -3654,8 +3654,8 @@ arc_hdr_destroy(arc_buf_hdr_t *hdr)
+ 		ASSERT(zfs_refcount_is_zero(&hdr->b_l1hdr.b_refcnt));
+ 		ASSERT3P(hdr->b_l1hdr.b_state, ==, arc_anon);
+ 	}
+-	ASSERT(!HDR_IO_IN_PROGRESS(hdr));
+-	ASSERT(!HDR_IN_HASH_TABLE(hdr));
++	VERIFY(!HDR_IO_IN_PROGRESS(hdr));
++	VERIFY(!HDR_IN_HASH_TABLE(hdr));
+ 
+ 	if (HDR_HAS_L2HDR(hdr)) {
+ 		l2arc_dev_t *dev = hdr->b_l2hdr.b_dev;
+@@ -6612,7 +6612,7 @@ arc_release(arc_buf_t *buf, const void *tag)
+ 	 * linked into the hash table.
+ 	 */
+ 	if (hdr->b_l1hdr.b_state == arc_anon) {
+-		ASSERT(!HDR_IO_IN_PROGRESS(hdr));
++		VERIFY(!HDR_IO_IN_PROGRESS(hdr));
+ 		ASSERT(!HDR_IN_HASH_TABLE(hdr));
+ 		ASSERT(!HDR_HAS_L2HDR(hdr));
+ 
+@@ -6827,8 +6827,8 @@ arc_write_ready(zio_t *zio)
+ 	if (HDR_IO_IN_PROGRESS(hdr)) {
+ 		ASSERT(zio->io_flags & ZIO_FLAG_REEXECUTED);
+ 	} else {
+-		arc_hdr_set_flags(hdr, ARC_FLAG_IO_IN_PROGRESS);
+ 		add_reference(hdr, hdr); /* For IO_IN_PROGRESS. */
++		arc_hdr_set_flags(hdr, ARC_FLAG_IO_IN_PROGRESS);
+ 	}
+ 
+ 	if (BP_IS_PROTECTED(bp)) {
+@@ -7023,11 +7023,11 @@ arc_write_done(zio_t *zio)
+ 				ASSERT0(BP_GET_LEVEL(zio->io_bp));
+ 			}
+ 		}
+-		arc_hdr_clear_flags(hdr, ARC_FLAG_IO_IN_PROGRESS);
+-		VERIFY3S(remove_reference(hdr, hdr), >, 0);
+ 		/* if it's not anon, we are doing a scrub */
+ 		if (exists == NULL && hdr->b_l1hdr.b_state == arc_anon)
+ 			arc_access(hdr, 0, B_FALSE);
++		arc_hdr_clear_flags(hdr, ARC_FLAG_IO_IN_PROGRESS);
++		VERIFY3S(remove_reference(hdr, hdr), >, 0);
+ 		mutex_exit(hash_lock);
+ 	} else {
+ 		arc_hdr_clear_flags(hdr, ARC_FLAG_IO_IN_PROGRESS);
diff --git a/debian/patches/0015-rpm-Add-support-for-building-akmod-packages.patch b/debian/patches/0015-rpm-Add-support-for-building-akmod-packages.patch
new file mode 100644
index 000000000..8468d175a
--- /dev/null
+++ b/debian/patches/0015-rpm-Add-support-for-building-akmod-packages.patch
@@ -0,0 +1,344 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: "Vincent S. Cojot" <ElCoyote27@users.noreply.github.com>
+Date: Mon, 21 Sep 2026 18:47:34 +0200
+Subject: [PATCH] rpm: Add support for building akmod packages
+
+Add akmod (Automatic Kernel Module) support to ZFS, providing a third
+packaging option alongside the existing DKMS and kmod packages.
+
+Akmod packages automatically build binary kmod RPMs when new kernels
+are installed, enabling:
+- Automatic kernel module rebuilds on kernel updates
+- Distribution of binary kmod RPMs without recompilation
+- Seamless minor version upgrades on RHEL/Fedora and clones
+
+Key changes:
+- rpm/generic/zfs-kmod.spec.in: Add conditional akmod support via
+  rpmbuild --define '_enable_akmod 1'. When enabled, sets
+  buildforkernels=akmod and repo=rpmfusion so kmodtool generates an
+  akmod-zfs package. Add -common subpackage for shared license/doc
+  files. Guard chmod and %files sections for empty kernel_versions.
+  Make kmod-devel packages opt-in via %bcond_with devel to prevent
+  akmods from installing unnecessary devel packages on every kernel
+  update. When devel is not enabled, devel header files are cleaned
+  from the buildroot to avoid unpackaged-files errors.
+- config/rpm.am: Add 'make rpm-akmod' and 'make srpm-akmod' targets.
+  These targets use the generic spec with _enable_akmod and
+  intentionally omit kernel-specific defines (kernels, ksrc, kobj)
+  so kmodtool enters its akmod-only code path.  The existing
+  rpm-kmod/srpm-kmod targets pass --with devel to preserve the
+  current behavior of producing devel packages.
+- config/zfs-build.m4: Update comments to document the akmod vs
+  regular kmod distinction for RPM_DEFINE_KMOD.
+- scripts/kmodtool: Fix a pre-existing bug where
+  print_rpmtemplate_kmoddevelpkg (the common kmod-NAME-devel package)
+  was called inside the per-kernel loop in print_customrpmtemplate
+  and print_rpmtemplate, causing duplicate %package definitions when
+  multiple kernel-devel packages are installed. Move it outside the
+  loop so it is emitted exactly once.
+
+The akmod infrastructure requires the 'akmods' package to be installed
+on the target system. When the akmod-zfs RPM is installed, it places
+a zfs-kmod src.rpm in /usr/src/akmods/. The akmods service then
+automatically rebuilds it for the running kernel via rpmbuild
+--define 'kernels <uname-r>' --rebuild.
+
+Usage:
+  make rpm-akmod    # Build akmod-zfs, kmod-zfs (meta), zfs-kmod-common
+  make rpm-kmod     # Build kmod with devel packages (unchanged behavior)
+
+Tested on Fedora 43 (kernel 7.1), RHEL 9.8 (kernel 5.14), and
+Alma Linux 10 (kernel 6.12). Verified akmods rebuilds produce only
+kmod-zfs (no devel packages) after kernel updates.
+
+Reviewed-by: Tony Hutter <hutter2@llnl.gov>
+Reviewed-by: Brian Behlendorf <behlendorf1@llnl.gov>
+Signed-off-by: Vincent S. Cojot <vcojot@redhat.com>
+Closes #19099
+(cherry picked from commit bb1c7ed97a92d77096852f063cbfdbfee177b185)
+Signed-off-by: Stoiko Ivanov <s.ivanov@proxmox.com>
+---
+ config/rpm.am                | 20 ++++++++++----
+ config/zfs-build.m4          |  6 ++---
+ rpm/generic/zfs-kmod.spec.in | 41 ++++++++++++++++++++++++-----
+ scripts/kmodtool             | 51 ++++++++++++++++++++++--------------
+ 4 files changed, 85 insertions(+), 33 deletions(-)
+
+diff --git a/config/rpm.am b/config/rpm.am
+index 4ccf549b3a7a842d4a048b97fec8e86a70e00529..5d8188309c08bfb77cc4042c67a63d52e5ff35d5 100644
+--- a/config/rpm.am
++++ b/config/rpm.am
+@@ -7,15 +7,15 @@
+ # Build targets for RPM packages.
+ ###############################################################################
+ 
+-PHONY += srpm srpms srpm-kmod srpm-dkms srpm-utils
+-PHONY += rpm rpms rpm-kmod rpm-dkms rpm-utils rpm-utils-initramfs
++PHONY += srpm srpms srpm-kmod srpm-dkms srpm-utils srpm-akmod
++PHONY += rpm rpms rpm-kmod rpm-dkms rpm-utils rpm-utils-initramfs rpm-akmod
+ PHONY += srpm-common rpm-common rpm-local
+ 
+-srpm-kmod srpm-dkms srpm-utils: dist
++srpm-kmod srpm-dkms srpm-utils srpm-akmod: dist
+ 
+ srpm-kmod:
+ 	$(MAKE) $(AM_MAKEFLAGS) pkg="${PACKAGE}-kmod" \
+-		def='${SRPM_DEFINE_COMMON} ${SRPM_DEFINE_KMOD}' srpm-common
++		def='${SRPM_DEFINE_COMMON} ${SRPM_DEFINE_KMOD} --with devel' srpm-common
+ 
+ srpm-dkms:
+ 	$(MAKE) $(AM_MAKEFLAGS) pkg="${PACKAGE}-dkms" \
+@@ -25,12 +25,17 @@ srpm-utils:
+ 	$(MAKE) $(AM_MAKEFLAGS) pkg="${PACKAGE}" \
+ 		def='${SRPM_DEFINE_COMMON} ${SRPM_DEFINE_UTIL}' srpm-common
+ 
++srpm-akmod:
++	$(MAKE) $(AM_MAKEFLAGS) pkg="${PACKAGE}-kmod" \
++		def='${SRPM_DEFINE_COMMON} --define "_wrong_version_format_terminate_build 0" --define "_enable_akmod 1"' \
++		RPM_SPEC_DIR="rpm/generic" srpm-common
++
+ srpm: srpm-kmod srpm-dkms srpm-utils
+ srpms: srpm-kmod srpm-dkms srpm-utils
+ 
+ rpm-kmod: srpm-kmod
+ 	$(MAKE) $(AM_MAKEFLAGS) pkg="${PACKAGE}-kmod" \
+-		def='${RPM_DEFINE_COMMON} ${RPM_DEFINE_KMOD}' rpm-common
++		def='${RPM_DEFINE_COMMON} ${RPM_DEFINE_KMOD} --with devel' rpm-common
+ 
+ rpm-dkms: srpm-dkms
+ 	$(MAKE) $(AM_MAKEFLAGS) pkg="${PACKAGE}-dkms" \
+@@ -52,6 +57,11 @@ rpm-utils-initramfs: srpm-utils
+ 	$(MAKE) $(AM_MAKEFLAGS) pkg="${PACKAGE}" \
+ 		def='${RPM_DEFINE_COMMON} ${RPM_DEFINE_UTIL} ${RPM_DEFINE_INITRAMFS}' rpm-common
+ 
++rpm-akmod: srpm-akmod
++	$(MAKE) $(AM_MAKEFLAGS) pkg="${PACKAGE}-kmod" \
++		def='${RPM_DEFINE_COMMON} --define "_wrong_version_format_terminate_build 0" --define "_enable_akmod 1"' \
++		RPM_SPEC_DIR="rpm/generic" rpm-common
++
+ rpm: rpm-kmod rpm-dkms rpm-utils
+ rpms: rpm-kmod rpm-dkms rpm-utils
+ 
+diff --git a/config/zfs-build.m4 b/config/zfs-build.m4
+index 49e46dfd5632164a23eae67b4ace4ee9e6fc5e6e..5d245b638d1e097009e9fc589ff0199511567a24 100644
+--- a/config/zfs-build.m4
++++ b/config/zfs-build.m4
+@@ -438,9 +438,9 @@ AC_DEFUN([ZFS_AC_RPM], [
+ 		AC_SUBST(MULTIARCH_LIBDIR)
+ 	])
+ 
+-	dnl # Make RPM_DEFINE_KMOD additions conditional on CONFIG_KERNEL,
+-	dnl # since the values will not be set otherwise. The spec files
+-	dnl # provide defaults for them.
++	dnl # For akmod builds, we don't need to define kernel build parameters
++	dnl # since akmods handles this automatically. For regular kmod builds,
++	dnl # we need all the kernel build parameters.
+ 	dnl #
+ 	RPM_DEFINE_KMOD='--define "_wrong_version_format_terminate_build 0"'
+ 	AM_COND_IF([CONFIG_KERNEL], [
+diff --git a/rpm/generic/zfs-kmod.spec.in b/rpm/generic/zfs-kmod.spec.in
+index 7ed828bd0c9c4f5227f6fc93c7ea822a8f42e45a..b23c8cca27af2dc74963f40c827c7a9f7f770deb 100644
+--- a/rpm/generic/zfs-kmod.spec.in
++++ b/rpm/generic/zfs-kmod.spec.in
+@@ -1,3 +1,4 @@
++%define debug_package %{nil}
+ %define module  @PACKAGE@
+ 
+ %if !%{defined ksrc}
+@@ -28,16 +29,19 @@
+ %endif
+ %endif
+ 
+-#define repo    rpmfusion
++# Conditional akmod support - can be enabled via: rpmbuild --define "_enable_akmod 1"
++%{?_enable_akmod:%define repo rpmfusion}
++%{!?_enable_akmod:#define repo rpmfusion}
+ #define repo    chaos
+ 
+ # (un)define the next line to either build for the newest or all current kernels
+-%define buildforkernels newest
++%{?_enable_akmod:%define buildforkernels akmod}
++%{!?_enable_akmod:%define buildforkernels newest}
+ #define buildforkernels current
+-#define buildforkernels akmod
+ 
+ %bcond_with     debug
+ %bcond_with     debuginfo
++%bcond_with     devel
+ 
+ 
+ Name:           %{module}-kmod
+@@ -72,7 +76,8 @@ Conflicts:      %{module}-dkms
+ # Building for a repository use the proper build-sysbuild package
+ # to determine which kernel-devel packages should be installed.
+ BuildRequires:  %{_bindir}/kmodtool
+-%{!?kernels:BuildRequires: buildsys-build-%{repo}-kerneldevpkgs-%{?buildforkernels:%{buildforkernels}}%{!?buildforkernels:current}-%{_target_cpu}}
++# Skip buildsys-build for akmod builds as akmods handles kernel dependencies
++%{!?_enable_akmod:%{!?kernels:BuildRequires: buildsys-build-%{repo}-kerneldevpkgs-%{?buildforkernels:%{buildforkernels}}%{!?buildforkernels:current}-%{_target_cpu}}}
+ 
+ %else
+ 
+@@ -99,18 +104,28 @@ BuildRequires:  %{_bindir}/kmodtool
+ # Kmodtool does its magic here.  A patched version of kmodtool is shipped
+ # with the source rpm until kmod development packages are supported upstream.
+ # https://bugzilla.rpmfusion.org/show_bug.cgi?id=2714
+-%{expand:%(bash %{SOURCE10} --target %{_target_cpu} %{?repo:--repo %{?repo}} --kmodname %{name} %{?buildforkernels:--%{buildforkernels}} --devel %{?prefix:--prefix "%{?prefix}"} %{?kernels:--for-kernels "%{?kernels}"} %{?kernelbuildroot:--buildroot "%{?kernelbuildroot}"} 2>/dev/null) }
++%{expand:%(bash %{SOURCE10} --target %{_target_cpu} %{?repo:--repo %{?repo}} --kmodname %{name} %{?buildforkernels:--%{buildforkernels}} %{?with_devel:--devel} %{?prefix:--prefix "%{?prefix}"} %{?kernels:--for-kernels "%{?kernels}"} %{?kernelbuildroot:--buildroot "%{?kernelbuildroot}"} 2>/dev/null) }
+ 
+ 
+ %description
+ This package contains the ZFS kernel modules.
+ 
++# akmod package requires -common subpackage
++%package common
++Summary: Common files for the ZFS kernel modules
++Requires: %{name} = %{version}-%{release}
++BuildArch: noarch
++
++%description common
++This package contains the common files (license, documentation, etc.) for the
++ZFS kernel modules.
++
+ %prep
+ # Error out if there was something wrong with kmodtool.
+ %{?kmodtool_check}
+ 
+ # Print kmodtool output for debugging purposes:
+-bash %{SOURCE10}  --target %{_target_cpu} %{?repo:--repo %{?repo}} --kmodname %{name} %{?buildforkernels:--%{buildforkernels}} --devel %{?prefix:--prefix "%{?prefix}"} %{?kernels:--for-kernels "%{?kernels}"} %{?kernelbuildroot:--buildroot "%{?kernelbuildroot}"} 2>/dev/null
++bash %{SOURCE10}  --target %{_target_cpu} %{?repo:--repo %{?repo}} --kmodname %{name} %{?buildforkernels:--%{buildforkernels}} %{?with_devel:--devel} %{?prefix:--prefix "%{?prefix}"} %{?kernels:--for-kernels "%{?kernels}"} %{?kernelbuildroot:--buildroot "%{?kernelbuildroot}"} 2>/dev/null
+ 
+ %if %{with debug}
+     %define debug --enable-debug
+@@ -129,6 +144,9 @@ bash %{SOURCE10}  --target %{_target_cpu} %{?repo:--repo %{?repo}} --kmodname %{
+ 
+ %setup -q -c -T -a 0
+ 
++# For common package
++cp %{module}-%{version}/{COPYRIGHT,LICENSE,NOTICE,README.md} .
++
+ for kernel_version in %{?kernel_versions}; do
+     %{__mkdir} _kmod_build_${kernel_version%%___*}
+ done
+@@ -207,9 +225,20 @@ for kernel_version in %{?kernel_versions}; do
+     cd ..
+ done
+ # find-debuginfo.sh only considers executables
++%if %{defined kernel_versions}
+ chmod u+x ${RPM_BUILD_ROOT}%{kmodinstdir_prefix}/*/extra/*/*
++%endif
+ %{?akmod_install}
++# Remove devel files when devel packages are not being built,
++# to prevent rpmbuild from erroring on unpackaged files.
++%if !%{with devel}
++rm -rf ${RPM_BUILD_ROOT}/usr/src/%{module}-*
++%endif
+ 
+ 
+ %clean
+ rm -rf $RPM_BUILD_ROOT
++
++%files common
++%doc README.md
++%license COPYRIGHT LICENSE NOTICE
+diff --git a/scripts/kmodtool b/scripts/kmodtool
+index e24f206e698a29d19d8a771f831ff700c7858f04..d8d2af04fc9c2d9e384a1c05b718fb1703812cc3 100755
+--- a/scripts/kmodtool
++++ b/scripts/kmodtool
+@@ -332,44 +332,47 @@ EOF
+ 
+ print_customrpmtemplate ()
+ {
++	local first_kernel_custom=""
++	local first_kernel_redhat=""
++	local first_kernel_redhat_variant=""
++
+ 	for kernel in ${1}
+ 	do
+ 		if [[ -e "${prefix}/lib/modules/${kernel}/build/Makefile" ]]; then
+-			# likely a user-build-kernel with available buildfiles
+-			# fixme: we should check if uname from Makefile is the same as ${kernel}
+-
+ 			kernel_versions="${kernel_versions}${kernel}___${prefix}/lib/modules/${kernel}/build/ "
+ 			print_rpmtemplate_per_kmodpkg --custom "${kernel}"
+ 
+-			# create development package
+ 			if [[ -n "${devel}" ]]; then
+-				# create devel package including common headers
+-				print_rpmtemplate_kmoddevelpkg --custom "${kernel}"
+-
+-				# create devel package
+ 				print_rpmtemplate_per_kmoddevelpkg --custom "${kernel}"
+ 			fi
++			[[ -z "${first_kernel_custom}" ]] && first_kernel_custom="${kernel}"
+ 		elif [[ -e "${buildroot}/usr/src/kernels/${kernel}" ]]; then
+-			# this looks like a Fedora/RH kernel -- print a normal template (which includes the proper BR) and be happy :)
+ 			kernel_versions="${kernel_versions}${kernel}___${buildroot}%{_usrsrc}/kernels/${kernel} "
+ 
+-			# parse kernel versions string and print template
+ 			local kernel_verrelarch=${kernel%%${kernels_known_variants}}
+ 			print_rpmtemplate_per_kmodpkg --redhat ${kernel} ${kernel##${kernel_verrelarch}}
+ 
+-			# create development package
+ 			if [[ -n "${devel}" ]]; then
+-				# create devel package including common headers
+-				print_rpmtemplate_kmoddevelpkg --redhat ${kernel} ${kernel##${kernel_verrelarch}}
+-
+-				# create devel package
+ 				print_rpmtemplate_per_kmoddevelpkg --redhat ${kernel} ${kernel##${kernel_verrelarch}}
+ 			fi
++			if [[ -z "${first_kernel_redhat}" ]]; then
++				first_kernel_redhat="${kernel}"
++				first_kernel_redhat_variant="${kernel##${kernel_verrelarch}}"
++			fi
+ 		else
+ 			error_out 2 "Don't know how to handle ${kernel} -- ${prefix}/lib/modules/${kernel}/build/Makefile not found"
+ 		fi
+ 	done
+ 
++	# Common devel package (only once, outside the per-kernel loop)
++	if [[ -n "${devel}" ]]; then
++		if [[ -n "${first_kernel_custom}" ]]; then
++			print_rpmtemplate_kmoddevelpkg --custom "${first_kernel_custom}"
++		elif [[ -n "${first_kernel_redhat}" ]]; then
++			print_rpmtemplate_kmoddevelpkg --redhat ${first_kernel_redhat} ${first_kernel_redhat_variant}
++		fi
++	fi
++
+ 	# well, it's no header anymore, but who cares ;-)
+ 	print_rpmtemplate_header
+ }
+@@ -386,6 +389,9 @@ print_rpmtemplate ()
+ 	# and print it and some other required stuff as macro
+ 	print_rpmtemplate_header
+ 
++	local first_kernel=""
++	local first_kernel_variant=""
++
+ 	# now print the packages
+ 	for kernel in ${kernel_versions_to_build_for} ; do
+ 
+@@ -398,13 +404,20 @@ print_rpmtemplate ()
+ 		print_rpmtemplate_per_kmodpkg "${kernel}" "${kernel##${kernel_verrelarch}}"
+ 
+ 		if [[ -n "${devel}" ]]; then
+-			# create devel package including common headers
+-			print_rpmtemplate_kmoddevelpkg "${kernel}" "${kernel##${kernel_verrelarch}}"
+-
+-			# create devel package
++			# create per-kernel devel package
+ 			print_rpmtemplate_per_kmoddevelpkg "${kernel}" "${kernel##${kernel_verrelarch}}"
+ 		fi
++
++		if [[ -z "${first_kernel}" ]]; then
++			first_kernel="${kernel}"
++			first_kernel_variant="${kernel##${kernel_verrelarch}}"
++		fi
+ 	done
++
++	# Common devel package (only once, outside the per-kernel loop)
++	if [[ -n "${devel}" && -n "${first_kernel}" ]]; then
++		print_rpmtemplate_kmoddevelpkg "${first_kernel}" "${first_kernel_variant}"
++	fi
+ }
+ 
+ myprog_help ()
diff --git a/debian/patches/0016-libzfs_core-block-delivery-of-SIGUSR1-in-send_worker.patch b/debian/patches/0016-libzfs_core-block-delivery-of-SIGUSR1-in-send_worker.patch
new file mode 100644
index 000000000..ae5926810
--- /dev/null
+++ b/debian/patches/0016-libzfs_core-block-delivery-of-SIGUSR1-in-send_worker.patch
@@ -0,0 +1,93 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Nico Sonack <nsonack@herrhotzenplotz.de>
+Date: Mon, 14 Sep 2026 18:36:11 +0000
+Subject: [PATCH] libzfs_core: block delivery of SIGUSR1 in send_worker thread
+
+This fixes a Linux-specific bug.
+
+3a909fe33 (libzfs, libzfs_core: send: always write to pipe, 2022-02-21)
+introduced a subtle bug where zfs send -RPv would not print status
+information periodically anymore when the stdout is redirected to
+something that isn't a pipe (e.g. >/dev/null).
+
+This is because the send_worker thread introduced by this commit
+is created with an unmodified signal mask from libzfs. When
+zfs_send_space is called it creates this thread.  When we request
+verbose status information another thread is created which, in
+theory, should periodically receive a SIGUSR1 via a POSIX timer.
+
+The main thread blocks USR1 delivery *after* the creation of the
+progress thread, leaving the send_worker thread's signal mask
+unmodified. The delivery of SIGUSR1 is now random and for at least
+Debian Trixie with kernel 6.12.107+deb13-amd64 this causes the
+send_worker to be favoured.
+
+Interestingly, when stracing the zfs send, sometimes delivery flaps
+over to the correct thread. Without tracing this doesn't happen and
+the output remains silent instead.
+
+Fix this by blocking delivery of SIGUSR1 in the send_worker thread.
+We don't need to take care of SIGINFO since this thread is only
+created on Linux and SIGINFO is a BSD-specific signal.
+
+Reviewed-by: Brian Behlendorf <behlendorf1@llnl.gov>
+Signed-off-by: Nico Sonack <nsonack@herrhotzenplotz.de>
+Closes #19098
+(cherry picked from commit 47906ec304216b91277a149d4a574ea7ce196575)
+Signed-off-by: Stoiko Ivanov <s.ivanov@proxmox.com>
+---
+ lib/libzfs_core/libzfs_core.c | 27 +++++++++++++++++++++++++--
+ 1 file changed, 25 insertions(+), 2 deletions(-)
+
+diff --git a/lib/libzfs_core/libzfs_core.c b/lib/libzfs_core/libzfs_core.c
+index 9347aa7c6a28dd3da5bd905689b21336b9972cb5..b0f59d04f8e1a0c192e9e088f1e4fcc355c63066 100644
+--- a/lib/libzfs_core/libzfs_core.c
++++ b/lib/libzfs_core/libzfs_core.c
+@@ -78,6 +78,7 @@
+ #include <libzfs_core.h>
+ #include <ctype.h>
+ #include <unistd.h>
++#include <signal.h>
+ #include <stdlib.h>
+ #include <string.h>
+ #ifdef ZFS_DEBUG
+@@ -699,7 +700,28 @@ send_worker(void *arg)
+ {
+ 	struct send_worker_ctx *ctx = arg;
+ 	unsigned int bufsiz = max_pipe_buffer(ctx->from);
+-	ssize_t rd;
++	sigset_t block_mask;
++	ssize_t rd = -1;
++
++	/*
++	 * When users request verbose status information about the
++	 * send progress SIGUSR1 is delivered periodically in libzfs
++	 * to a dedicated thread.
++	 *
++	 * However, we get created before this progress thread is
++	 * created and the signal mask of the parent is only modified to
++	 * block SIGUSR1 afterwards.
++	 *
++	 * Block delivery of USR1 to us such that we don't get
++	 * interrupted and the right thread gets the signal.
++	 */
++	sigemptyset(&block_mask);
++	sigaddset(&block_mask, SIGUSR1);
++	int err = pthread_sigmask(SIG_BLOCK, &block_mask, NULL);
++	if (err != 0) {
++		close(ctx->from);
++		return ((void *)(uintptr_t)err);
++	}
+ 
+ 	for (;;) {
+ 		rd = splice(ctx->from, NULL, ctx->to, NULL, bufsiz,
+@@ -707,7 +729,8 @@ send_worker(void *arg)
+ 		if ((rd == -1 && errno != EINTR) || rd == 0)
+ 			break;
+ 	}
+-	int err = (rd == -1) ? errno : 0;
++
++	err = (rd == -1) ? errno : 0;
+ 	close(ctx->from);
+ 	return ((void *)(uintptr_t)err);
+ }
diff --git a/debian/patches/0017-kmodtool-use-kABI-baseline-naming-on-RHEL-to-prevent.patch b/debian/patches/0017-kmodtool-use-kABI-baseline-naming-on-RHEL-to-prevent.patch
new file mode 100644
index 000000000..16acc6a98
--- /dev/null
+++ b/debian/patches/0017-kmodtool-use-kABI-baseline-naming-on-RHEL-to-prevent.patch
@@ -0,0 +1,417 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: "Vincent S. Cojot" <vcojot@redhat.com>
+Date: Fri, 2 Oct 2026 19:11:17 +0200
+Subject: [PATCH] kmodtool: use kABI-baseline naming on RHEL to prevent kmod
+ accumulation
+
+On RHEL, errata kernels within a minor release share a stable kABI.
+The existing kmodtool generates a unique kmod package name per exact
+kernel version (e.g. kmod-zfs-5.14.0-687.52.1.el9_8), which causes
+unbounded kmod package accumulation as new errata kernels are installed
+and akmods rebuilds for each one.
+
+Port the kABI-aware naming logic from RPM Fusion's kmodtool:
+
+- Add init_kernel_uname_r_vars() to parse kernel uname -r into
+  components including the kABI baseline (kernel_uname_r_short).
+  Example: 5.14.0-687.52.1.el9_8.x86_64 -> 5.14.0-687.el9_8
+
+- On RHEL (%{?rhel}), use kernel_uname_r_short in package names
+  so all errata kernels within a minor release produce the same
+  package name (e.g. kmod-zfs-5.14.0-687.el9_8).
+
+- On Fedora (no kABI guarantee), keep the full kernel_uname_r in
+  package names, preserving current behavior.
+
+- Add RHEL kABI-specific features:
+  - Provides: installonlypkg(kernel-module)
+  - Requires: kernel >= short, kernel < short+1 (range deps)
+  - weak-modules integration in %post/%preun/%postun scriptlets
+
+- File paths always use the full kernel_uname_r regardless of
+  the naming scheme.
+
+This matches the approach used by RPM Fusion's kmodtool (kmodtool
+package v1.1) and negativo17's nvidia-kmod packaging, both of which
+have been using kABI-baseline naming successfully on RHEL for years.
+
+Signed-off-by: Vincent S. Cojot <ElCoyote27@users.noreply.github.com>
+(cherry picked from commit 65ea88f071abf32622912c69ccfefde3709e2ac7)
+Signed-off-by: Stoiko Ivanov <s.ivanov@proxmox.com>
+---
+ scripts/kmodtool | 239 ++++++++++++++++++++++++++++++++++++++---------
+ 1 file changed, 197 insertions(+), 42 deletions(-)
+
+diff --git a/scripts/kmodtool b/scripts/kmodtool
+index d8d2af04fc9c2d9e384a1c05b718fb1703812cc3..7fa34408e2954e651e6fe04441faf1529abf16b3 100755
+--- a/scripts/kmodtool
++++ b/scripts/kmodtool
+@@ -1,6 +1,6 @@
+ #!/usr/bin/env bash
+ # SPDX-License-Identifier: MIT
+-# shellcheck disable=SC2086,SC2295
++# shellcheck disable=SC2001,SC2034,SC2086,SC2249,SC2295
+ 
+ # kmodtool - Helper script for building kernel module RPMs
+ # Copyright (c) 2003-2012 Ville Skyttä <ville.skytta@iki.fi>,
+@@ -29,7 +29,7 @@
+ shopt -s extglob
+ 
+ myprog="kmodtool-${repo}"
+-myver="0.12.1"
++myver="0.13.0"
+ 
+ kmodname=
+ build_kernels="current"
+@@ -42,6 +42,14 @@ target=
+ buildroot=
+ dashvariant=
+ 
++kernel_uname_r=""
++kernel_uname_r_short=""
++kernel_uname_r_wo_arch=""
++kernel_uname_r_dist=""
++kernel_uname_r_rel=""
++kernel_uname_r_rel_plus_one=""
++kernel_uname_r_short_plus_one=""
++
+ error_out()
+ {
+ 	local errorlevel=${1}
+@@ -61,6 +69,69 @@ print_rpmtemplate_header()
+ 	echo
+ }
+ 
++init_kernel_uname_r_vars()
++{
++	## Parse kernel uname -r into useful substrings for kABI-aware naming.
++	## On RHEL, errata kernels share a kABI baseline (e.g. 5.14.0-687.el9)
++	## so modules built for one errata kernel are compatible with all
++	## kernels in the same minor release.  Using the short form in package
++	## names avoids unbounded kmod accumulation.
++	##
++	## Examples (RHEL 9.8, x86_64):
++	##   kernel_uname_r                5.14.0-687.52.1.el9_8.x86_64
++	##   kernel_uname_r_short          5.14.0-687.el9_8
++	##   kernel_uname_r_wo_arch        5.14.0-687.52.1.el9_8
++	##   kernel_uname_r_dist           el9_8
++	##   kernel_uname_r_rel            687
++	##   kernel_uname_r_rel_plus_one   688
++	##   kernel_uname_r_short_plus_one 5.14.0-688.el9_8
++	##
++	kernel_uname_r=${1}
++	kernel_uname_r_short=$(echo "${1}" | sed -n \
++		-e "s/\.${target}//" \
++		-e "s/\(.*\)-\([[:alnum:]]\+\)\(\.[[:alnum:]\.]\+\|\)\(\.el.*\|\.fc.*\)/\1-\2\4/p")
++	kernel_uname_r_wo_arch=$(echo "${1}" | sed -e "s/\.${target}//")
++	kernel_uname_r_dist=$(echo "${1}" | sed -n \
++		-e "s/\.${target}//" \
++		-e "s/\(.*\)-\([[:alnum:]]\+\.[[:alnum:]]\+\-\|[[:alnum:]]\+\)\([[:alnum:]\.]\+\|\)\(\.el.*\|\.fc.*\)/\4/" \
++		-e "s/^\.//p")
++	kernel_uname_r_rel=$(echo "${1}" | sed -n \
++		-e "s/\.${target}//" \
++		-e "s/\(.*\)-\([[:alnum:]]\+\.[[:alnum:]]\+\-\|[[:alnum:]]\+\.\)\([[:alnum:]\.]\+\|\)\(.*\)/\2/" \
++		-e "s/^\.//" -e "s/[\.\-]$//p")
++	if echo "${kernel_uname_r_rel}" | grep -qE '^[0-9]+$' ; then
++		kernel_uname_r_rel_plus_one=$(( kernel_uname_r_rel+1 ))
++	else
++		kernel_uname_r_rel_plus_one=""
++	fi
++	kernel_uname_r_short_plus_one=$(echo "${kernel_uname_r_short}" | \
++		sed -e "s/-${kernel_uname_r_rel}\./-${kernel_uname_r_rel_plus_one}\./")
++}
++
++print_rpmtemplate_macros()
++{
++	cat <<EOF
++
++%{?rhel:%global with_rhel_kabi 1}
++
++%if 0%{?rhel} || 0%{?fedora}
++%global kmod_pkg_name kmod-${kmodname}
++%endif
++EOF
++}
++
++print_rpmtemplate_macros_pkg_name_version()
++{
++	init_kernel_uname_r_vars "${1}"
++	cat <<EOF
++
++%if 0%{?rhel} || 0%{?fedora}
++%global kmod_for_kernel %{?with_rhel_kabi:${kernel_uname_r_short}}%{!?with_rhel_kabi:${kernel_uname_r}}
++%global package_kmod_name %{kmod_pkg_name}-%{kmod_for_kernel}
++%endif
++EOF
++}
++
+ print_akmodtemplate ()
+ {
+ 	echo
+@@ -79,7 +150,6 @@ Requires:   kmodtool
+ Requires: 	akmods
+ %{?AkmodsBuildRequires:Requires: %{AkmodsBuildRequires}}
+ # same requires and provides as a kmods package would have
+-Requires: 	${kmodname}-kmod-common >= %{?epoch:%{epoch}:}%{version}
+ Provides: 	${kmodname}-kmod = %{?epoch:%{epoch}:}%{version}-%{release}
+ EOF
+ 
+@@ -145,65 +215,122 @@ print_rpmtemplate_per_kmodpkg ()
+ 	fi
+ 
+ 	# first part
++	#
++	# On RHEL, use the kABI-baseline version (kernel_uname_r_short) in
++	# the package name so that errata kernel rebuilds produce the same
++	# package name and avoid unbounded kmod accumulation.  The file
++	# paths still use the full kernel_uname_r.
++	#
++	# On Fedora (no kABI guarantee), use the full kernel_uname_r in
++	# the package name as before.
+ 	cat <<EOF
+-%package       -n kmod-${kmodname}-${kernel_uname_r}
+-Summary:          ${kmodname} kernel module(s) for ${kernel_uname_r}
++%package       -n %{package_kmod_name}
++Summary:          ${kmodname} kernel module(s) for %{kmod_for_kernel}
+ Group:            System Environment/Kernel
+-Provides:         kernel-modules-for-kernel = ${kernel_uname_r}
++Provides:         kernel-modules%{!?with_rhel_kabi:-for-kernel} = ${kernel_uname_r}
+ Provides:         kmod-${kmodname}-uname-r = ${kernel_uname_r}
+ Provides:         ${kmodname}-kmod = %{?epoch:%{epoch}:}%{version}-%{release}
+-Requires:         ${kmodname}-kmod-common >= %{?epoch:%{epoch}:}%{version}
+-
+-%if 0%{?rhel} == 6 || 0%{?centos} == 6
+-Requires(post):   module-init-tools
+-Requires(postun): module-init-tools
++%if %{with rhel_kabi}
++Provides:         installonlypkg(kernel-module)
++Requires(post):   kmod
++Requires(postun): kmod
++Requires(post):   %{_prefix}/sbin/weak-modules
++Requires(postun): %{_prefix}/sbin/weak-modules
+ %else
+ Requires(post):   kmod
+ Requires(postun): kmod
+ %endif
+ EOF
+ 
+-	# second part
++	# second part — depmod helper macro
++	cat <<EOF
++%global _kmodtool_depmod_post \\
++if [ -f /boot/System.map-${kernel_uname_r} ] ; then \\
++  ${prefix}${depmod_path} -aeF /boot/System.map-${kernel_uname_r} ${kernel_uname_r} >/dev/null ; \\
++elif [ -f /lib/modules/${kernel_uname_r}/System.map ] ; then \\
++  ${prefix}${depmod_path} -aeF /lib/modules/${kernel_uname_r}/System.map ${kernel_uname_r} >/dev/null ; \\
++else \\
++  ${prefix}${depmod_path} -a >/dev/null ; \\
++fi || :
++EOF
++
+ 	if [[ -z "${customkernel}" ]]; then
+ 	     cat <<EOF
++%if %{with rhel_kabi}
++Requires:         kernel >= ${kernel_uname_r_short}
++Requires:         kernel < ${kernel_uname_r_short_plus_one}
++BuildRequires:    kernel-devel = ${kernel_uname_r_wo_arch} redhat-rpm-config
++%else
+ Requires:         kernel-uname-r = ${kernel_uname_r}
+-BuildRequires:	  kernel-devel-uname-r = ${kernel_uname_r}
++BuildRequires:    kernel-devel-uname-r = ${kernel_uname_r}
++%endif
+ %{?KmodsRequires:Requires: %{KmodsRequires}-uname-r = ${kernel_uname_r}}
+ %{?KmodsRequires:BuildRequires: %{KmodsRequires}-uname-r = ${kernel_uname_r}}
+-%post          -n kmod-${kmodname}-${kernel_uname_r}
+-if [[ -f "/boot/System.map-${kernel_uname_r}" ]]; then
+-	${prefix}${depmod_path} -aeF /boot/System.map-${kernel_uname_r} ${kernel_uname_r} > /dev/null || :
+-elif [[ -f "/lib/modules/${kernel_uname_r}/System.map" ]]; then
+-	${prefix}${depmod_path} -aeF /lib/modules/${kernel_uname_r}/System.map ${kernel_uname_r} > /dev/null || :
+-else
+-	${prefix}${depmod_path} -ae ${kernel_uname_r} &> /dev/null || :
+-fi
+-%postun        -n kmod-${kmodname}-${kernel_uname_r}
+-if [[ -f "/boot/System.map-${kernel_uname_r}" ]]; then
+-	${prefix}${depmod_path} -aF /boot/System.map-${kernel_uname_r} ${kernel_uname_r} &> /dev/null || :
+-elif [[ -f "/lib/modules/${kernel_uname_r}/System.map" ]]; then
+-	${prefix}${depmod_path} -aF /lib/modules/${kernel_uname_r}/System.map ${kernel_uname_r} &> /dev/null || :
+-else
+-	${prefix}${depmod_path} -a ${kernel_uname_r} &> /dev/null || :
+-fi
++
++%post          -n %{package_kmod_name}
++%{_kmodtool_depmod_post}
++%if %{with rhel_kabi}
++modules=( \$(find ${prefix}/lib/modules/${kernel_uname_r}/extra/${kmodname}/ \
++	-name "*.ko" -o -name "*.ko.xz" -o -name "*.ko.gz" -o -name "*.ko.zst") )
++printf '%s\\n' "\${modules[@]}" | %{_prefix}/sbin/weak-modules --add-modules
++%endif
++
++%if %{with rhel_kabi}
++%preun         -n %{package_kmod_name}
++rpm -ql %{package_kmod_name}-%{version}-%{release}.$(arch) \
++	| grep -E '\.ko(\.xz|\.gz|\.zst)?$' \
++	> /var/run/rpm-%{package_kmod_name}-modules
++%endif
++
++%postun        -n %{package_kmod_name}
++%{_kmodtool_depmod_post}
++%if %{with rhel_kabi}
++modules=( \$(cat /var/run/rpm-%{package_kmod_name}-modules) )
++rm -f /var/run/rpm-%{package_kmod_name}-modules
++printf '%s\\n' "\${modules[@]}" | %{_prefix}/sbin/weak-modules --remove-modules
++%endif
+ 
+ EOF
+ 	else
+ 	  cat <<EOF
+-%post          -n kmod-${kmodname}-${kernel_uname_r}
++%post          -n %{package_kmod_name}
+ [ "\$(uname -r)" = "${kernel_uname_r}"  ] && ${prefix}${depmod_path} -a > /dev/null || :
+-%postun        -n kmod-${kmodname}-${kernel_uname_r}
++%if %{with rhel_kabi}
++if [[ "\$(uname -r)" == "${kernel_uname_r}" ]] ; then
++	modules=( \$(find ${prefix}/lib/modules/${kernel_uname_r}/extra/${kmodname}/ \
++		-name "*.ko" -o -name "*.ko.xz" -o -name "*.ko.gz" -o -name "*.ko.zst") )
++	printf '%s\\n' "\${modules[@]}" | %{_prefix}/sbin/weak-modules --add-modules
++fi
++%endif
++
++%if %{with rhel_kabi}
++%preun         -n %{package_kmod_name}
++if [[ "\$(uname -r)" == "${kernel_uname_r}" ]] ; then
++	rpm -ql %{package_kmod_name}-%{version}-%{release}.$(arch) \
++		| grep -E '\.ko(\.xz|\.gz|\.zst)?$' \
++		> /var/run/rpm-%{package_kmod_name}-modules
++fi
++%endif
++
++%postun        -n %{package_kmod_name}
+ [ "\$(uname -r)" = "${kernel_uname_r}"  ] && ${prefix}${depmod_path} -a > /dev/null || :
++%if %{with rhel_kabi}
++if [[ "\$(uname -r)" == "${kernel_uname_r}" ]] ; then
++	modules=( \$(cat /var/run/rpm-%{package_kmod_name}-modules) )
++	rm -f /var/run/rpm-%{package_kmod_name}-modules
++	printf '%s\\n' "\${modules[@]}" | %{_prefix}/sbin/weak-modules --remove-modules
++fi
++%endif
+ 
+ EOF
+ 	fi
+ 
+-  # third part
++  # third part — file paths always use the full kernel_uname_r
+ 	cat <<EOF
+-%description  -n kmod-${kmodname}-${kernel_uname_r}
++%description  -n %{package_kmod_name}
+ This package provides the ${kmodname} kernel modules built for the Linux
+-kernel ${kernel_uname_r} for the %{_target_cpu} family of processors.
+-%files        -n kmod-${kmodname}-${kernel_uname_r}
++kernel %{kmod_for_kernel} for the %{_target_cpu} family of processors.
++%files        -n %{package_kmod_name}
+ %defattr(644,root,root,755)
+ %dir $prefix/lib/modules/${kernel_uname_r}/extra
+ ${prefix}/lib/modules/${kernel_uname_r}/extra/${kmodname}/
+@@ -303,27 +430,26 @@ EOF
+ 
+ print_rpmtemplate_kmodmetapkg ()
+ {
+-		local kernel_uname_r=${1}
+-		local kernel_variant="${2:+-${2}}"
++		local kernel_variant="${1:+-${1}}"
+ 
+ 		cat <<EOF
+-%package      -n kmod-${kmodname}${kernel_variant}
++%package      -n %{kmod_pkg_name}${kernel_variant}
+ Summary:         Metapackage which tracks in ${kmodname} kernel module for newest kernel${kernel_variant}
+ Group:           System Environment/Kernel
+ 
+ Provides:        ${kmodname}-kmod = %{?epoch:%{epoch}:}%{version}-%{release}
+-Requires:        kmod-${kmodname}-${kernel_uname_r} >= %{?epoch:%{epoch}:}%{version}-%{release}
++Requires:        %{package_kmod_name} >= %{?epoch:%{epoch}:}%{version}-%{release}
+ %{?KmodsMetaRequires:Requires: %{?KmodsMetaRequires}}
+ EOF
+ 
+ 		cat <<EOF
+ 
+-%description  -n kmod-${kmodname}${kernel_variant}
++%description  -n %{kmod_pkg_name}${kernel_variant}
+ This is a meta-package without payload which sole purpose is to require the
+ ${kmodname} kernel module(s) for the newest kernel${kernel_variant}.
+ to make sure you get it together with a new kernel.
+ 
+-%files        -n kmod-${kmodname}${kernel_variant}
++%files        -n %{kmod_pkg_name}${kernel_variant}
+ %defattr(644,root,root,755)
+ 
+ 
+@@ -335,11 +461,27 @@ print_customrpmtemplate ()
+ 	local first_kernel_custom=""
+ 	local first_kernel_redhat=""
+ 	local first_kernel_redhat_variant=""
++	local seen_kabi_baselines=""
+ 
+ 	for kernel in ${1}
+ 	do
++		# Deduplicate kernels that share the same kABI baseline.
++		# On RHEL, errata kernels (e.g. 5.14.0-687.49.1 through
++		# 5.14.0-687.54.1) all map to the same kABI-baseline
++		# package name (kmod-xxx-5.14.0-687.el9_8).  Generating
++		# duplicate %package definitions causes rpmbuild to fail.
++		# On Fedora each kernel has a unique short name, so no
++		# kernels are skipped.
++		init_kernel_uname_r_vars "${kernel}"
++		local kabi_key="${kernel_uname_r_short:-${kernel}}"
++		case " ${seen_kabi_baselines} " in
++			*" ${kabi_key} "*) continue ;;
++		esac
++		seen_kabi_baselines="${seen_kabi_baselines} ${kabi_key}"
++
+ 		if [[ -e "${prefix}/lib/modules/${kernel}/build/Makefile" ]]; then
+ 			kernel_versions="${kernel_versions}${kernel}___${prefix}/lib/modules/${kernel}/build/ "
++			print_rpmtemplate_macros_pkg_name_version "${kernel}"
+ 			print_rpmtemplate_per_kmodpkg --custom "${kernel}"
+ 
+ 			if [[ -n "${devel}" ]]; then
+@@ -350,6 +492,7 @@ print_customrpmtemplate ()
+ 			kernel_versions="${kernel_versions}${kernel}___${buildroot}%{_usrsrc}/kernels/${kernel} "
+ 
+ 			local kernel_verrelarch=${kernel%%${kernels_known_variants}}
++			print_rpmtemplate_macros_pkg_name_version "${kernel}"
+ 			print_rpmtemplate_per_kmodpkg --redhat ${kernel} ${kernel##${kernel_verrelarch}}
+ 
+ 			if [[ -n "${devel}" ]]; then
+@@ -391,14 +534,25 @@ print_rpmtemplate ()
+ 
+ 	local first_kernel=""
+ 	local first_kernel_variant=""
++	local seen_kabi_baselines=""
+ 
+ 	# now print the packages
+ 	for kernel in ${kernel_versions_to_build_for} ; do
+ 
++		# Deduplicate by kABI baseline (see print_customrpmtemplate)
++		init_kernel_uname_r_vars "${kernel}"
++		local kabi_key="${kernel_uname_r_short:-${kernel}}"
++		case " ${seen_kabi_baselines} " in
++			*" ${kabi_key} "*) continue ;;
++		esac
++		seen_kabi_baselines="${seen_kabi_baselines} ${kabi_key}"
++
+ 		local kernel_verrelarch=${kernel%%${kernels_known_variants}}
+ 
++		print_rpmtemplate_macros_pkg_name_version "${kernel}"
++
+ 		# create metapackage
+-		print_rpmtemplate_kmodmetapkg "${kernel}" "${kernel##${kernel_verrelarch}}"
++		print_rpmtemplate_kmodmetapkg "${kernel##${kernel_verrelarch}}"
+ 
+ 		# create package
+ 		print_rpmtemplate_per_kmodpkg "${kernel}" "${kernel##${kernel_verrelarch}}"
+@@ -546,6 +700,7 @@ elif [[ -z "${kernels_known_variants}" ]] ; then
+ fi
+ 
+ # go
++print_rpmtemplate_macros
+ if [[ -n "${for_kernels}" ]]; then
+ 	# this is easy:
+ 	print_customrpmtemplate "${for_kernels}"
diff --git a/debian/patches/0018-L2ARC-Reorder-header-destruction-for-in-flight-L2-wr.patch b/debian/patches/0018-L2ARC-Reorder-header-destruction-for-in-flight-L2-wr.patch
new file mode 100644
index 000000000..b7bebd12d
--- /dev/null
+++ b/debian/patches/0018-L2ARC-Reorder-header-destruction-for-in-flight-L2-wr.patch
@@ -0,0 +1,119 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Ameer Hamza <ahamza@ixsystems.com>
+Date: Wed, 10 Sep 2025 20:20:13 +0500
+Subject: [PATCH] L2ARC: Reorder header destruction for in-flight L2 writes
+
+With multiple L2ARC devices, headers can be destroyed asynchronously
+(e.g., during zpool sync) while L2_WRITING is set. The original code
+destroyed L2HDR before L1HDR, causing ABDs to lose their device
+association (b_l2hdr.b_dev) when arc_hdr_free_abd() is called.
+
+This caused ABDs to be added to the global free-on-write list without
+device information. When any L2ARC device completed its write and
+attempted to free these orphaned ABDs, it would panic on
+ASSERT(!list_link_active(&abd->abd_gang_link)) because the ABD was
+still part of another device's vdev_queue I/O aggregation gang.
+
+Fix by extending l2ad_mtx lock scope to cover L1HDR destruction and
+reordering to destroy L1HDR before L2HDR when L2_WRITING is set. This
+ensures arc_hdr_free_abd() can access b_l2hdr.b_dev to properly tag
+ABDs with their device for deferred cleanup.
+
+Reviewed-by: Alexander Motin <alexander.motin@TrueNAS.com>
+Reviewed-by: Brian Behlendorf <behlendorf1@llnl.gov>
+Signed-off-by: Ameer Hamza <ahamza@ixsystems.com>
+Closes #18093
+(cherry picked from commit b8610c3d93fec2297a9f69fcab3f5b35f01e941f)
+Signed-off-by: Andriy Tkachuk <atkachuk@wasabi.com>
+(cherry picked from commit cece1476c6a19c19c9ac355c923774788cee11bb)
+Signed-off-by: Stoiko Ivanov <s.ivanov@proxmox.com>
+---
+ module/zfs/arc.c | 55 ++++++++++++++++++++++++++++++++----------------
+ 1 file changed, 37 insertions(+), 18 deletions(-)
+
+diff --git a/module/zfs/arc.c b/module/zfs/arc.c
+index 4cd2a67eb2a85398627fad5531dc1aa20f1b2f5d..8556b55c2bf235bdb80bd451c7489f276f6322f5 100644
+--- a/module/zfs/arc.c
++++ b/module/zfs/arc.c
+@@ -3656,7 +3656,13 @@ arc_hdr_destroy(arc_buf_hdr_t *hdr)
+ 	}
+ 	VERIFY(!HDR_IO_IN_PROGRESS(hdr));
+ 	VERIFY(!HDR_IN_HASH_TABLE(hdr));
++	boolean_t l1hdr_destroyed = B_FALSE;
+ 
++	/*
++	 * If L2_WRITING, destroy L1HDR before L2HDR (under mutex) so
++	 * arc_hdr_free_abd() can properly defer ABDs. Otherwise, destroy
++	 * L1HDR outside mutex to minimize contention.
++	 */
+ 	if (HDR_HAS_L2HDR(hdr)) {
+ 		l2arc_dev_t *dev = hdr->b_l2hdr.b_dev;
+ 		boolean_t buflist_held = MUTEX_HELD(&dev->l2ad_mtx);
+@@ -3674,9 +3680,26 @@ arc_hdr_destroy(arc_buf_hdr_t *hdr)
+ 		 * want to re-destroy the header's L2 portion.
+ 		 */
+ 		if (HDR_HAS_L2HDR(hdr)) {
++			if (HDR_L2_WRITING(hdr)) {
++				l1hdr_destroyed = B_TRUE;
+ 
+-			if (!HDR_EMPTY(hdr))
+-				buf_discard_identity(hdr);
++				if (!HDR_EMPTY(hdr))
++					buf_discard_identity(hdr);
++
++				if (HDR_HAS_L1HDR(hdr)) {
++					arc_cksum_free(hdr);
++
++					while (hdr->b_l1hdr.b_buf != NULL)
++						arc_buf_destroy_impl(
++						    hdr->b_l1hdr.b_buf);
++
++					if (hdr->b_l1hdr.b_pabd != NULL)
++						arc_hdr_free_abd(hdr, B_FALSE);
++
++					if (HDR_HAS_RABD(hdr))
++						arc_hdr_free_abd(hdr, B_TRUE);
++				}
++			}
+ 
+ 			arc_hdr_l2hdr_destroy(hdr);
+ 		}
+@@ -3685,26 +3708,22 @@ arc_hdr_destroy(arc_buf_hdr_t *hdr)
+ 			mutex_exit(&dev->l2ad_mtx);
+ 	}
+ 
+-	/*
+-	 * The header's identify can only be safely discarded once it is no
+-	 * longer discoverable.  This requires removing it from the hash table
+-	 * and the l2arc header list.  After this point the hash lock can not
+-	 * be used to protect the header.
+-	 */
+-	if (!HDR_EMPTY(hdr))
+-		buf_discard_identity(hdr);
++	if (!l1hdr_destroyed) {
++		if (!HDR_EMPTY(hdr))
++			buf_discard_identity(hdr);
+ 
+-	if (HDR_HAS_L1HDR(hdr)) {
+-		arc_cksum_free(hdr);
++		if (HDR_HAS_L1HDR(hdr)) {
++			arc_cksum_free(hdr);
+ 
+-		while (hdr->b_l1hdr.b_buf != NULL)
+-			arc_buf_destroy_impl(hdr->b_l1hdr.b_buf);
++			while (hdr->b_l1hdr.b_buf != NULL)
++				arc_buf_destroy_impl(hdr->b_l1hdr.b_buf);
+ 
+-		if (hdr->b_l1hdr.b_pabd != NULL)
+-			arc_hdr_free_abd(hdr, B_FALSE);
++			if (hdr->b_l1hdr.b_pabd != NULL)
++				arc_hdr_free_abd(hdr, B_FALSE);
+ 
+-		if (HDR_HAS_RABD(hdr))
+-			arc_hdr_free_abd(hdr, B_TRUE);
++			if (HDR_HAS_RABD(hdr))
++				arc_hdr_free_abd(hdr, B_TRUE);
++		}
+ 	}
+ 
+ 	VERIFY0P(hdr->b_hash_next);
diff --git a/debian/patches/0019-L2ARC-Preserve-L2HDR-in-arc_release-for-in-flight-wr.patch b/debian/patches/0019-L2ARC-Preserve-L2HDR-in-arc_release-for-in-flight-wr.patch
new file mode 100644
index 000000000..245ee4165
--- /dev/null
+++ b/debian/patches/0019-L2ARC-Preserve-L2HDR-in-arc_release-for-in-flight-wr.patch
@@ -0,0 +1,174 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Ameer Hamza <ahamza@ixsystems.com>
+Date: Sun, 4 Jan 2026 21:10:02 +0500
+Subject: [PATCH] L2ARC: Preserve L2HDR in arc_release() for in-flight writes
+
+When arc_release() is called on a header with a single buffer and
+L2_WRITING set, the L2HDR must be preserved for ABD cleanup (similar
+to the arc_hdr_destroy() case). If we destroy the L2HDR here, later
+arc_write() will allocate a new ABD and call arc_hdr_free_abd(),
+which needs b_l2hdr.b_dev to properly defer ABD cleanup, causing
+VERIFY(HDR_HAS_L2HDR(hdr)) to fail.
+
+Allocate a new header for the buffer in the single_buf_l2writing
+case (single buffer + L2_WRITING), leaving the original header with
+L2HDR intact. The original header becomes an "orphan" (no buffers, no
+b_pabd) but retains device association for ABD cleanup when
+l2arc_write_done() completes.
+
+The shared buffer case (HDR_SHARED_DATA) is excluded because L2ARC
+makes its own transformed copy via l2arc_apply_transforms(), so the
+original ABD is not used by the L2 write. The header can be safely
+reused without allocating a new one.
+
+For proper evictable space accounting, arc_buf_remove() must be
+called before remove_reference() in the single_buf_l2writing path.
+This ensures arc_evictable_space_increment() (during remove_reference)
+and arc_evictable_space_decrement() (during destruction) see the
+same state (b_buf=NULL), preventing accounting leaks that cause
+module unload to hang with non-zero esize.
+
+Reviewed-by: Alexander Motin <alexander.motin@TrueNAS.com>
+Reviewed-by: Brian Behlendorf <behlendorf1@llnl.gov>
+Signed-off-by: Ameer Hamza <ahamza@ixsystems.com>
+Closes #18093
+(cherry picked from commit 825dc41ad4bf262e3d8e6ccb5d549709bf1e53c5)
+Signed-off-by: Andriy Tkachuk <atkachuk@wasabi.com>
+(cherry picked from commit acf6f13ff574eef41b1e5078f1fad98c9d1d8036)
+Signed-off-by: Stoiko Ivanov <s.ivanov@proxmox.com>
+---
+ module/zfs/arc.c | 88 ++++++++++++++++++++++++++++--------------------
+ 1 file changed, 51 insertions(+), 37 deletions(-)
+
+diff --git a/module/zfs/arc.c b/module/zfs/arc.c
+index 8556b55c2bf235bdb80bd451c7489f276f6322f5..5393c018ee82ba73eb53414683350abc7ba45c74 100644
+--- a/module/zfs/arc.c
++++ b/module/zfs/arc.c
+@@ -6669,16 +6669,22 @@ arc_release(arc_buf_t *buf, const void *tag)
+ 	ASSERT3S(zfs_refcount_count(&hdr->b_l1hdr.b_refcnt), >, 0);
+ 
+ 	/*
+-	 * Do we have more than one buf?
++	 * Do we have more than one buf? Or L2_WRITING with unshared data?
++	 * Single-buf L2_WRITING with shared data can reuse the header since
++	 * L2ARC uses its own transformed copy.
+ 	 */
+-	if (hdr->b_l1hdr.b_buf != buf || !ARC_BUF_LAST(buf)) {
++	if (hdr->b_l1hdr.b_buf != buf || !ARC_BUF_LAST(buf) ||
++	    (HDR_L2_WRITING(hdr) && !ARC_BUF_SHARED(buf))) {
+ 		arc_buf_hdr_t *nhdr;
+ 		uint64_t spa = hdr->b_spa;
+ 		uint64_t psize = HDR_GET_PSIZE(hdr);
+ 		uint64_t lsize = HDR_GET_LSIZE(hdr);
+ 		boolean_t protected = HDR_PROTECTED(hdr);
+ 		enum zio_compress compress = arc_hdr_get_compress(hdr);
++		uint8_t complevel = hdr->b_complevel;
+ 		arc_buf_contents_t type = arc_buf_type(hdr);
++		boolean_t single_buf_l2writing = (hdr->b_l1hdr.b_buf == buf &&
++		    ARC_BUF_LAST(buf) && HDR_L2_WRITING(hdr));
+ 
+ 		if (ARC_BUF_SHARED(buf) && !ARC_BUF_COMPRESSED(buf)) {
+ 			ASSERT3P(hdr->b_l1hdr.b_buf, !=, buf);
+@@ -6689,48 +6695,51 @@ arc_release(arc_buf_t *buf, const void *tag)
+ 		 * Pull the buffer off of this hdr and find the last buffer
+ 		 * in the hdr's buffer list.
+ 		 */
+-		VERIFY3S(remove_reference(hdr, tag), >, 0);
+ 		arc_buf_t *lastbuf = arc_buf_remove(hdr, buf);
+-		ASSERT3P(lastbuf, !=, NULL);
++		EQUIV(single_buf_l2writing, lastbuf == NULL);
+ 
+ 		/*
+ 		 * If the current arc_buf_t and the hdr are sharing their data
+ 		 * buffer, then we must stop sharing that block.
+ 		 */
+-		if (ARC_BUF_SHARED(buf)) {
+-			ASSERT(!arc_buf_is_shared(lastbuf));
++		if (!single_buf_l2writing) {
++			if (ARC_BUF_SHARED(buf)) {
++				ASSERT(!arc_buf_is_shared(lastbuf));
+ 
+-			/*
+-			 * First, sever the block sharing relationship between
+-			 * buf and the arc_buf_hdr_t.
+-			 */
+-			arc_unshare_buf(hdr, buf);
++				/*
++				 * First, sever the block sharing relationship
++				 * between buf and the arc_buf_hdr_t.
++				 */
++				arc_unshare_buf(hdr, buf);
+ 
+-			/*
+-			 * Now we need to recreate the hdr's b_pabd. Since we
+-			 * have lastbuf handy, we try to share with it, but if
+-			 * we can't then we allocate a new b_pabd and copy the
+-			 * data from buf into it.
+-			 */
+-			if (arc_can_share(hdr, lastbuf)) {
+-				arc_share_buf(hdr, lastbuf);
+-			} else {
+-				arc_hdr_alloc_abd(hdr, 0);
+-				abd_copy_from_buf(hdr->b_l1hdr.b_pabd,
+-				    buf->b_data, psize);
++				/*
++				 * Now we need to recreate the hdr's b_pabd.
++				 * Since we have lastbuf handy, we try to share
++				 * with it, but if we can't then we allocate a
++				 * new b_pabd and copy the data from buf into it
++				 */
++				if (arc_can_share(hdr, lastbuf)) {
++					arc_share_buf(hdr, lastbuf);
++				} else {
++					arc_hdr_alloc_abd(hdr, 0);
++					abd_copy_from_buf(hdr->b_l1hdr.b_pabd,
++					    buf->b_data, psize);
++				}
++			} else if (HDR_SHARED_DATA(hdr)) {
++				/*
++				 * Uncompressed shared buffers are always at the
++				 * end of the list. Compressed buffers don't
++				 * have the same requirements. This makes it
++				 * hard to simply assert that the lastbuf is
++				 * shared so we rely on the hdr's compression
++				 * flags to determine if we have a compressed,
++				 * shared buffer.
++				 */
++				ASSERT(arc_buf_is_shared(lastbuf) ||
++				    arc_hdr_get_compress(hdr) !=
++				    ZIO_COMPRESS_OFF);
++				ASSERT(!arc_buf_is_shared(buf));
+ 			}
+-		} else if (HDR_SHARED_DATA(hdr)) {
+-			/*
+-			 * Uncompressed shared buffers are always at the end
+-			 * of the list. Compressed buffers don't have the
+-			 * same requirements. This makes it hard to
+-			 * simply assert that the lastbuf is shared so
+-			 * we rely on the hdr's compression flags to determine
+-			 * if we have a compressed, shared buffer.
+-			 */
+-			ASSERT(arc_buf_is_shared(lastbuf) ||
+-			    arc_hdr_get_compress(hdr) != ZIO_COMPRESS_OFF);
+-			ASSERT(!arc_buf_is_shared(buf));
+ 		}
+ 
+ 		ASSERT(hdr->b_l1hdr.b_pabd != NULL || HDR_HAS_RABD(hdr));
+@@ -6745,10 +6754,15 @@ arc_release(arc_buf_t *buf, const void *tag)
+ 		if (!arc_hdr_has_uncompressed_buf(hdr))
+ 			arc_cksum_free(hdr);
+ 
++		if (single_buf_l2writing)
++			VERIFY3S(remove_reference(hdr, tag), ==, 0);
++		else
++			VERIFY3S(remove_reference(hdr, tag), >, 0);
++
+ 		mutex_exit(hash_lock);
+ 
+-		nhdr = arc_hdr_alloc(spa, psize, lsize, protected,
+-		    compress, hdr->b_complevel, type);
++		nhdr = arc_hdr_alloc(spa, psize, lsize, protected, compress,
++		    complevel, type);
+ 		ASSERT0P(nhdr->b_l1hdr.b_buf);
+ 		ASSERT0(zfs_refcount_count(&nhdr->b_l1hdr.b_refcnt));
+ 		VERIFY3U(nhdr->b_type, ==, type);
diff --git a/debian/patches/0020-arc-fix-race-between-arc_release-and-arc_read_done.patch b/debian/patches/0020-arc-fix-race-between-arc_release-and-arc_read_done.patch
new file mode 100644
index 000000000..02e9361f0
--- /dev/null
+++ b/debian/patches/0020-arc-fix-race-between-arc_release-and-arc_read_done.patch
@@ -0,0 +1,179 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Andriy Tkachuk <atkachuk@wasabi.com>
+Date: Thu, 6 Aug 2026 22:13:46 +0100
+Subject: [PATCH] arc: fix race between arc_release() and arc_read_done()
+
+Consider the following scenario:
+
+1. arc_release() is called on hdr with one buf, but which has
+   IO_IN_PROGRESS (reading more raw data in encypted pool while
+   keeping decrypted data in buf).
+2. arc_release() moves hdr to anon state and discards its identity.
+3. arc_read_done() is called, adds the 2nd buf to hdr, increasing
+   b_refcnt to 2.
+
+Now we have hdr in anon state with two bufs and without identity.
+
+Or here's a racing scenario:
+
+1. arc_release() checked that hdr is not in anon state, but before
+   taking hash_lock
+2. arc_read_done() takes hash_lock, moves hdr to anon state, in
+   case of an error.
+3. arc_release() now takes hash_lock and tries to move hdr to anon
+   state again.
+
+Solution: 1) in arc_read_done(), don't put hdr to anon state on an
+error immediately but allow remove_reference() do that when b_ref
+becomes 0; 2) in arc_release(), handle IO_IN_PROGRESS case similarly
+to case with several buffers without putting the hdr to anon state;
+3) in remove_reference(), don't put hdr with IO_ERROR flag to the
+eviction list, free it immediately, similar to uncached state.
+
+Note: hdr with IO_ERROR flag can now linger longer around before
+it gets released at remove_reference(), so we make some additional
+changes to facilitate that: 1) free b_radb or else b_pabd on error
+at arc_read_done(); 2) set IO_ERROR only if no valid data remains
+at hdr worth to cache. This allows next arc_read() calls on the
+hdr to avoid hit branch (HDR_HAS_RABD), returning bad data, but
+try to re-read it instead; as well as for arc_write() to not trip
+on ASSERT(!HDR_IO_ERROR(hdr)).
+
+Reviewed-by: Alexander Motin <alexander.motin@TrueNAS.com>
+Reviewed-by: Brian Behlendorf <behlendorf1@llnl.gov>
+Signed-off-by: Andriy Tkachuk <atkachuk@wasabi.com>
+Co-authored-by: Alexander Motin <alexander.motin@TrueNAS.com>
+Illumos-issue: https://www.illumos.org/issues/14003
+Related-issue: #13479
+Closes #18897
+(cherry picked from commit 216de07d89ffe0b6faadc6c090b0527a198a7ccb)
+Signed-off-by: Andriy Tkachuk <atkachuk@wasabi.com>
+(cherry picked from commit eb011f2aff3a37d69118cd5f3db184b80344940d)
+Signed-off-by: Stoiko Ivanov <s.ivanov@proxmox.com>
+---
+ module/zfs/arc.c | 54 ++++++++++++++++++++++++++++++++++++------------
+ 1 file changed, 41 insertions(+), 13 deletions(-)
+
+diff --git a/module/zfs/arc.c b/module/zfs/arc.c
+index 5393c018ee82ba73eb53414683350abc7ba45c74..24b0501d1eeba10b25cc2b61395468e12895b05a 100644
+--- a/module/zfs/arc.c
++++ b/module/zfs/arc.c
+@@ -2361,7 +2361,8 @@ remove_reference(arc_buf_hdr_t *hdr, const void *tag)
+ 		arc_hdr_destroy(hdr);
+ 		return (0);
+ 	}
+-	if (state == arc_uncached && !HDR_PREFETCH(hdr)) {
++	if ((state == arc_uncached && !HDR_PREFETCH(hdr)) ||
++	    HDR_IO_ERROR(hdr)) {
+ 		arc_change_state(arc_anon, hdr);
+ 		arc_hdr_destroy(hdr);
+ 		return (0);
+@@ -3728,7 +3729,7 @@ arc_hdr_destroy(arc_buf_hdr_t *hdr)
+ 
+ 	VERIFY0P(hdr->b_hash_next);
+ 	if (HDR_HAS_L1HDR(hdr)) {
+-		ASSERT(!multilist_link_active(&hdr->b_l1hdr.b_arc_node));
++		VERIFY(!multilist_link_active(&hdr->b_l1hdr.b_arc_node));
+ 		ASSERT0P(hdr->b_l1hdr.b_acb);
+ #ifdef ZFS_DEBUG
+ 		ASSERT0P(hdr->b_l1hdr.b_freeze_cksum);
+@@ -5607,6 +5608,7 @@ arc_read_done(zio_t *zio)
+ 	kmutex_t	*hash_lock = NULL;
+ 	arc_callback_t	*callback_list;
+ 	arc_callback_t	*acb;
++	boolean_t	 read_error = (zio->io_error != 0);
+ 
+ 	/*
+ 	 * The hdr was inserted into hash-table and removed from lists
+@@ -5760,11 +5762,29 @@ arc_read_done(zio_t *zio)
+ 	if (zio->io_error == 0) {
+ 		arc_hdr_verify(hdr, zio->io_bp);
+ 	} else {
+-		arc_hdr_set_flags(hdr, ARC_FLAG_IO_ERROR);
+-		if (hdr->b_l1hdr.b_state != arc_anon)
+-			arc_change_state(arc_anon, hdr);
+-		if (HDR_IN_HASH_TABLE(hdr))
+-			buf_hash_remove(hdr);
++		/*
++		 * A failed *physical* read leaves the raw/encrypted buffer it
++		 * was filling full of garbage.  If a valid decrypted b_pabd
++		 * survives (the raw re-read case) the header stays cached, and
++		 * a later raw read would be served this garbage as a hit —
++		 * arc_read()'s hit test honors HDR_HAS_RABD, not IO_ERROR, and
++		 * arc_cksum_verify() skips IO_ERROR headers. We should free it
++		 * so that representation misses and re-fetches from disk.
++		 *
++		 * Gate on read_error: a *valid* b_rabd whose consumer merely
++		 * failed to decrypt it (keys not loaded) also reaches here,
++		 * but the read succeeded and the data should be kept.
++		 */
++		if (read_error) {
++			if (HDR_HAS_RABD(hdr))
++				arc_hdr_free_abd(hdr, B_TRUE);
++			else if (hdr->b_l1hdr.b_pabd != NULL)
++				arc_hdr_free_abd(hdr, B_FALSE);
++		}
++		/* Flag for teardown only if nothing valid remains. */
++		if (hdr->b_l1hdr.b_pabd == NULL && !HDR_HAS_RABD(hdr) &&
++		    hdr->b_l1hdr.b_buf == NULL)
++			arc_hdr_set_flags(hdr, ARC_FLAG_IO_ERROR);
+ 	}
+ 
+ 	arc_hdr_clear_flags(hdr, ARC_FLAG_IO_IN_PROGRESS);
+@@ -6672,9 +6692,13 @@ arc_release(arc_buf_t *buf, const void *tag)
+ 	 * Do we have more than one buf? Or L2_WRITING with unshared data?
+ 	 * Single-buf L2_WRITING with shared data can reuse the header since
+ 	 * L2ARC uses its own transformed copy.
++	 * Or I/O is in progress (a raw/encrypted read filling b_rabd while
++	 * our decrypted buf backs b_pabd) and arc_read_done() is yet to add
++	 * more bufs?
+ 	 */
+ 	if (hdr->b_l1hdr.b_buf != buf || !ARC_BUF_LAST(buf) ||
+-	    (HDR_L2_WRITING(hdr) && !ARC_BUF_SHARED(buf))) {
++	    (HDR_L2_WRITING(hdr) && !ARC_BUF_SHARED(buf)) ||
++	    HDR_IO_IN_PROGRESS(hdr)) {
+ 		arc_buf_hdr_t *nhdr;
+ 		uint64_t spa = hdr->b_spa;
+ 		uint64_t psize = HDR_GET_PSIZE(hdr);
+@@ -6683,8 +6707,10 @@ arc_release(arc_buf_t *buf, const void *tag)
+ 		enum zio_compress compress = arc_hdr_get_compress(hdr);
+ 		uint8_t complevel = hdr->b_complevel;
+ 		arc_buf_contents_t type = arc_buf_type(hdr);
+-		boolean_t single_buf_l2writing = (hdr->b_l1hdr.b_buf == buf &&
+-		    ARC_BUF_LAST(buf) && HDR_L2_WRITING(hdr));
++		boolean_t single_buf = (hdr->b_l1hdr.b_buf == buf &&
++		    ARC_BUF_LAST(buf));
++		boolean_t single_buf_l2writing = (single_buf &&
++		    HDR_L2_WRITING(hdr) && !HDR_IO_IN_PROGRESS(hdr));
+ 
+ 		if (ARC_BUF_SHARED(buf) && !ARC_BUF_COMPRESSED(buf)) {
+ 			ASSERT3P(hdr->b_l1hdr.b_buf, !=, buf);
+@@ -6696,7 +6722,7 @@ arc_release(arc_buf_t *buf, const void *tag)
+ 		 * in the hdr's buffer list.
+ 		 */
+ 		arc_buf_t *lastbuf = arc_buf_remove(hdr, buf);
+-		EQUIV(single_buf_l2writing, lastbuf == NULL);
++		EQUIV(single_buf, lastbuf == NULL);
+ 
+ 		/*
+ 		 * If the current arc_buf_t and the hdr are sharing their data
+@@ -6704,7 +6730,8 @@ arc_release(arc_buf_t *buf, const void *tag)
+ 		 */
+ 		if (!single_buf_l2writing) {
+ 			if (ARC_BUF_SHARED(buf)) {
+-				ASSERT(!arc_buf_is_shared(lastbuf));
++				ASSERT(single_buf ||
++				    !arc_buf_is_shared(lastbuf));
+ 
+ 				/*
+ 				 * First, sever the block sharing relationship
+@@ -6718,7 +6745,8 @@ arc_release(arc_buf_t *buf, const void *tag)
+ 				 * with it, but if we can't then we allocate a
+ 				 * new b_pabd and copy the data from buf into it
+ 				 */
+-				if (arc_can_share(hdr, lastbuf)) {
++				if (!single_buf &&
++				    arc_can_share(hdr, lastbuf)) {
+ 					arc_share_buf(hdr, lastbuf);
+ 				} else {
+ 					arc_hdr_alloc_abd(hdr, 0);
diff --git a/debian/patches/series b/debian/patches/series
index 0ef82e7bb..c594842fc 100644
--- a/debian/patches/series
+++ b/debian/patches/series
@@ -8,3 +8,13 @@
 0008-zpool-status-tighten-bounds-for-noalloc-stat-availab.patch
 0009-libzfs-scrub-only-include-start-and-end-nv-pairs-if-.patch
 0010-Add-no-preserve-encryption-flag.patch
+0011-Inherit-the-project-ID-for-every-object-type.patch
+0012-Allow-renames-within-a-single-directory-under-projec.patch
+0013-ZTS-check-project-ID-inheritance-and-renames-within-.patch
+0014-arc-harness-buf_hdr-s-anon-state-invariant-checks.patch
+0015-rpm-Add-support-for-building-akmod-packages.patch
+0016-libzfs_core-block-delivery-of-SIGUSR1-in-send_worker.patch
+0017-kmodtool-use-kABI-baseline-naming-on-RHEL-to-prevent.patch
+0018-L2ARC-Reorder-header-destruction-for-in-flight-L2-wr.patch
+0019-L2ARC-Preserve-L2HDR-in-arc_release-for-in-flight-wr.patch
+0020-arc-fix-race-between-arc_release-and-arc_read_done.patch
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 3+ messages in thread

* [PATCH zfsonlinux 2/2] fix #7653: d/patches: cherry-pick fix for stale page-cache on receive/rollback
  2026-10-20 14:02 [PATCH zfsonlinux 0/2] fix #8178, #7653 Stoiko Ivanov
  2026-10-20 14:02 ` [PATCH zfsonlinux 1/2] fix #8178: d/patches: add patches already queued for zfs-2.4.5 Stoiko Ivanov
@ 2026-10-20 14:02 ` Stoiko Ivanov
  1 sibling, 0 replies; 3+ messages in thread
From: Stoiko Ivanov @ 2026-10-20 14:02 UTC (permalink / raw)
  To: pve-devel

https://bugzilla.proxmox.com/show_bug.cgi?id=7653 seems fixed by
 8eb65b0ea ("Linux: drop cached pages in zfs_rezget()")
based on the reporter's reproducer.

cherry-pick it from upstream/master.

Signed-off-by: Stoiko Ivanov <s.ivanov@proxmox.com>
---
 ...inux-drop-cached-pages-in-zfs_rezget.patch | 53 +++++++++++++++++++
 debian/patches/series                         |  1 +
 2 files changed, 54 insertions(+)
 create mode 100644 debian/patches/0021-Linux-drop-cached-pages-in-zfs_rezget.patch

diff --git a/debian/patches/0021-Linux-drop-cached-pages-in-zfs_rezget.patch b/debian/patches/0021-Linux-drop-cached-pages-in-zfs_rezget.patch
new file mode 100644
index 000000000..69f676f54
--- /dev/null
+++ b/debian/patches/0021-Linux-drop-cached-pages-in-zfs_rezget.patch
@@ -0,0 +1,53 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: cppudge <disident47@gmail.com>
+Date: Thu, 3 Sep 2026 23:34:48 +0700
+Subject: [PATCH] Linux: drop cached pages in zfs_rezget()
+
+After zfs rollback (or zfs recv -F) of a mounted dataset,
+zfs_resume_fs() calls zfs_rezget() for every surviving znode. On Linux
+it reloads the SA attributes but never touches the inode's page cache.
+A file whose object number, generation and size are unchanged but whose
+data blocks differ keeps serving pre-rollback data from pages that were
+resident (the file had been mmap()ed) and later refreshed by
+update_pages() from a write(). Both mappedread() and mmap() return the
+stale pages; reading through .zfs/snapshot is correct, so the on-disk
+data is fine.
+
+The FreeBSD port handles this by calling vn_pages_remove_valid() at the
+start of zfs_rezget(). Do the equivalent on Linux with
+truncate_inode_pages() before reloading the znode, so pages are dropped
+on the error paths as well. Dirty pages are discarded on purpose: their
+content belongs to the state being rolled back, and writeback is blocked
+on z_teardown_lock during suspend/resume anyway. Live mappings are
+unmapped and fault the correct data back in through zfs_getpage().
+
+Reviewed-by: Brian Behlendorf <behlendorf1@llnl.gov>
+Signed-off-by: Petr Slavyagin <disident47@gmail.com>
+Closes #10931
+Closes #19013
+(cherry picked from commit 8eb65b0ea563945280217d14fae82ea59580b774)
+Signed-off-by: Stoiko Ivanov <s.ivanov@proxmox.com>
+---
+ module/os/linux/zfs/zfs_znode_os.c | 9 +++++++++
+ 1 file changed, 9 insertions(+)
+
+diff --git a/module/os/linux/zfs/zfs_znode_os.c b/module/os/linux/zfs/zfs_znode_os.c
+index a106081702c4d0ae12a0004c60d0be055fe65bd5..3ed14dfbfc009e51cda59b57fbe2d0ad9cdde420 100644
+--- a/module/os/linux/zfs/zfs_znode_os.c
++++ b/module/os/linux/zfs/zfs_znode_os.c
+@@ -1194,6 +1194,15 @@ zfs_rezget(znode_t *zp)
+ 	if (zp->z_is_ctldir)
+ 		return (0);
+ 
++	/*
++	 * Drop cached pages before reloading the znode.  After a rollback or
++	 * a forced receive the object may hold different data under the same
++	 * inode, size and generation; stale Uptodate pages would otherwise be
++	 * served by mappedread() and mmap() (see #10931).  FreeBSD does the
++	 * same here via vn_pages_remove().
++	 */
++	truncate_inode_pages(ZTOI(zp)->i_mapping, 0);
++
+ 	zh = zfs_znode_hold_enter(zfsvfs, obj_num);
+ 
+ 	mutex_enter(&zp->z_acl_lock);
diff --git a/debian/patches/series b/debian/patches/series
index c594842fc..30c0d5ce1 100644
--- a/debian/patches/series
+++ b/debian/patches/series
@@ -18,3 +18,4 @@
 0018-L2ARC-Reorder-header-destruction-for-in-flight-L2-wr.patch
 0019-L2ARC-Preserve-L2HDR-in-arc_release-for-in-flight-wr.patch
 0020-arc-fix-race-between-arc_release-and-arc_read_done.patch
+0021-Linux-drop-cached-pages-in-zfs_rezget.patch
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 3+ messages in thread

end of thread, other threads:[~2026-10-20 14:04 UTC | newest]

Thread overview: 3+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-20 14:02 [PATCH zfsonlinux 0/2] fix #8178, #7653 Stoiko Ivanov
2026-10-20 14:02 ` [PATCH zfsonlinux 1/2] fix #8178: d/patches: add patches already queued for zfs-2.4.5 Stoiko Ivanov
2026-10-20 14:02 ` [PATCH zfsonlinux 2/2] fix #7653: d/patches: cherry-pick fix for stale page-cache on receive/rollback Stoiko Ivanov

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal