* [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.