From: Taehee Yoo <ap420073@gmail.com>
To: "Alex Deucher" <alexander.deucher@amd.com>,
"Alexei Starovoitov" <ast@kernel.org>,
amd-gfx@lists.freedesktop.org,
"Andrew Lunn" <andrew+netdev@lunn.ch>,
"Andrii Nakryiko" <andrii@kernel.org>,
"Bill Wendling" <morbo@google.com>,
bpf@vger.kernel.org, "Christian König" <christian.koenig@amd.com>,
"Daniel Borkmann" <daniel@iogearbox.net>,
"David Airlie" <airlied@gmail.com>,
"David S. Miller" <davem@davemloft.net>,
"Donald Hunter" <donald.hunter@gmail.com>,
dri-devel@lists.freedesktop.org,
"Eduard Zingerman" <eddyz87@gmail.com>,
"Emil Tsalapatis" <emil@etsalapatis.com>,
"Eric Dumazet" <edumazet@google.com>,
"Felix Kuehling" <Felix.Kuehling@amd.com>,
"Hoyeon Lee" <hoyeon.rhee@gmail.com>,
"Ilias Apalodimas" <ilias.apalodimas@linaro.org>,
"Jakub Kicinski" <kuba@kernel.org>,
"Jesper Dangaard Brouer" <hawk@kernel.org>,
"Jiri Olsa" <jolsa@kernel.org>,
"John Fastabend" <john.fastabend@gmail.com>,
"Justin Stitt" <justinstitt@google.com>,
"Kees Cook" <kees@kernel.org>,
"Kumar Kartikeya Dwivedi" <memxor@gmail.com>,
"Leon Romanovsky" <leon@kernel.org>,
linaro-mm-sig@lists.linaro.org, linux-hardening@vger.kernel.org,
linux-kernel@vger.kernel.org, linux-kselftest@vger.kernel.org,
linux-media@vger.kernel.org, linux-rdma@vger.kernel.org,
llvm@lists.linux.dev, "Mark Bloch" <mbloch@nvidia.com>,
"Martin KaFai Lau" <martin.lau@linux.dev>,
"Michael Chan" <michael.chan@broadcom.com>,
"Nathan Chancellor" <nathan@kernel.org>,
netdev@vger.kernel.org,
"Nick Desaulniers" <ndesaulniers@google.com>,
"Paolo Abeni" <pabeni@redhat.com>,
"Pavan Chebbi" <pavan.chebbi@broadcom.com>,
"Saeed Mahameed" <saeedm@nvidia.com>,
"Shuah Khan" <shuah@kernel.org>,
"Simona Vetter" <simona@ffwll.ch>,
"Simon Horman" <horms@kernel.org>, "Song Liu" <song@kernel.org>,
"Stanislav Fomichev" <sdf@fomichev.me>,
"Sumit Semwal" <sumit.semwal@linaro.org>,
"Taehee Yoo" <ap420073@gmail.com>,
"Tariq Toukan" <tariqt@nvidia.com>,
"Yonghong Song" <yonghong.song@linux.dev>
Subject: [RFC PATCH net-next 13/13] drm/amdkfd: add IPsec full-packet offload
Date: Sun, 19 Jul 2026 17:58:57 +0000 [thread overview]
Message-ID: <20260719175857.4071636-14-ap420073@gmail.com> (raw)
In-Reply-To: <20260719175857.4071636-1-ap420073@gmail.com>
Add a second knod feature, alongside BPF, to show the accel ops are not
tied to a single use case: GPU-offloaded IPsec ESP. xfrm SAs installed
with XFRM_DEV_OFFLOAD_PACKET are handed to the GPU, which parses ESP,
looks up the SA, checks anti-replay, does AES-GCM decrypt and writes the
verdict back. Built as a separate module (knod_ipsec).
This is a functional proof of concept: it demonstrates that a
full-packet crypto feature can run on top of the knod accelerator, not
a hardened, production-ready implementation.
Signed-off-by: Taehee Yoo <ap420073@gmail.com>
(cherry picked from commit 56ea4f90b43677e6944f1bc3ca4cf02e736ec340)
---
drivers/gpu/drm/amd/amdkfd/Kconfig | 9 +
drivers/gpu/drm/amd/amdkfd/Makefile | 3 +
.../gpu/drm/amd/amdkfd/knod/aesgcm_shader.h | 984 ++++
.../drm/amd/amdkfd/knod/ipsec_fused_gfx10.h | 1796 +++++++
.../drm/amd/amdkfd/knod/ipsec_fused_gfx9.h | 1349 ++++++
drivers/gpu/drm/amd/amdkfd/knod/knod_ipsec.c | 4273 +++++++++++++++++
drivers/gpu/drm/amd/amdkfd/knod/knod_ipsec.h | 596 +++
.../testing/selftests/drivers/net/knod/config | 1 +
8 files changed, 9011 insertions(+)
create mode 100644 drivers/gpu/drm/amd/amdkfd/knod/aesgcm_shader.h
create mode 100644 drivers/gpu/drm/amd/amdkfd/knod/ipsec_fused_gfx10.h
create mode 100644 drivers/gpu/drm/amd/amdkfd/knod/ipsec_fused_gfx9.h
create mode 100644 drivers/gpu/drm/amd/amdkfd/knod/knod_ipsec.c
create mode 100644 drivers/gpu/drm/amd/amdkfd/knod/knod_ipsec.h
diff --git a/drivers/gpu/drm/amd/amdkfd/Kconfig b/drivers/gpu/drm/amd/amdkfd/Kconfig
index 708aa5fc051b..67efae53e395 100644
--- a/drivers/gpu/drm/amd/amdkfd/Kconfig
+++ b/drivers/gpu/drm/amd/amdkfd/Kconfig
@@ -60,3 +60,12 @@ config HSA_AMD_KNOD_BPF
the verdict path (PASS/DROP/TX) off the host CPU.
If unsure, say N.
+
+config HSA_AMD_KNOD_IPSEC
+ tristate "KNOD IPsec (xfrm) full-packet offload"
+ depends on HSA_AMD_KNOD && XFRM_OFFLOAD && INET_ESP
+ help
+ GPU-accelerated IPsec ESP full-packet offload via KNOD.
+ Accepts xfrm SAs configured with XFRM_DEV_OFFLOAD_PACKET and
+ performs ESP parse, SA lookup, anti-replay, AES-GCM decrypt and
+ verdict writeback on the GPU.
diff --git a/drivers/gpu/drm/amd/amdkfd/Makefile b/drivers/gpu/drm/amd/amdkfd/Makefile
index 4df3850e1466..c5d801ddabb7 100644
--- a/drivers/gpu/drm/amd/amdkfd/Makefile
+++ b/drivers/gpu/drm/amd/amdkfd/Makefile
@@ -77,3 +77,6 @@ AMDKFD_FILES += $(AMDKFD_PATH)/kfd_knod.o
endif
obj-$(CONFIG_HSA_AMD_KNOD_BPF) += $(AMDKFD_PATH)/knod/knod_bpf.o
+
+
+obj-$(CONFIG_HSA_AMD_KNOD_IPSEC) += $(AMDKFD_PATH)/knod/knod_ipsec.o
diff --git a/drivers/gpu/drm/amd/amdkfd/knod/aesgcm_shader.h b/drivers/gpu/drm/amd/amdkfd/knod/aesgcm_shader.h
new file mode 100644
index 000000000000..ae5276edb9fd
--- /dev/null
+++ b/drivers/gpu/drm/amd/amdkfd/knod/aesgcm_shader.h
@@ -0,0 +1,984 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (c) 2021 Taehee Yoo <ap420073@gmail.com>
+ * Copyright (c) 2021 Hoyeon Lee <hoyeon.rhee@gmail.com>
+ */
+
+/*
+ * AES-GCM GPU shader emit helpers for GFX9 (Vega) / GFX10 (RDNA)
+ *
+ * Per-step emitters (AES rounds, encrypt block, GHASH gfmul, T-table
+ * lookup) shared by the IPsec fused shaders.
+ */
+
+#ifndef AESGCM_SHADER_H_
+#define AESGCM_SHADER_H_
+
+#include <linux/types.h>
+#include <asm/byteorder.h>
+#include "knod_amdgpu_insn.h"
+
+/* ======================================================================
+ * Emit pattern macros (file-local)
+ *
+ * _E(fn, ...) - emit instruction, advance n by instruction size
+ * _BR(fn, ...) - emit branch, save position, advance n
+ * ======================================================================
+ */
+
+#define _E(fn, ...) (n += fn(__VA_ARGS__) / 4)
+#define _BR(fn, ...) ({ int _p = n; n += fn(__VA_ARGS__) / 4; _p; })
+
+/* ======================================================================
+ * AES T-table Round Helper
+ *
+ * Emits one AES round using T-table lookups from LDS.
+ *
+ * T-table layout in LDS (4KB total):
+ * T0: offset 0..1023 (256 x 4 bytes)
+ * T1: offset 1024..2047
+ * T2: offset 2048..3071
+ * T3: offset 3072..4095
+ *
+ * AES round function:
+ * new[0] = T0[s0.b0] ^ T1[s1.b1] ^ T2[s2.b2] ^ T3[s3.b3] ^ rk[0]
+ * new[1] = T0[s1.b0] ^ T1[s2.b1] ^ T2[s3.b2] ^ T3[s0.b3] ^ rk[1]
+ * new[2] = T0[s2.b0] ^ T1[s3.b1] ^ T2[s0.b2] ^ T3[s1.b3] ^ rk[2]
+ * new[3] = T0[s3.b0] ^ T1[s0.b1] ^ T2[s1.b2] ^ T3[s2.b3] ^ rk[3]
+ * ======================================================================
+ */
+
+/*
+ * Emit code to extract a byte from a VGPR and compute LDS T-table address.
+ *
+ * @buf: instruction buffer
+ * @n: current position
+ * @vdst_addr: output VGPR for LDS address
+ * @vdst_val: output VGPR for loaded T-table value (ds_read destination)
+ * @v_state: input VGPR (state word)
+ * @byte_pos: byte position (0, 1, 2, 3)
+ * @table_base: LDS base offset for this table (0, 1024, 2048, 3072)
+ * @s_mask: SGPR holding 0xFF
+ * @v_tmp: temp VGPR for byte extraction
+ */
+static int emit_ttable_lookup_gfx9(u32 *buf, int n,
+ int v_addr, int v_val,
+ int v_state, int byte_pos,
+ int table_base, int s_mask, int v_tmp)
+{
+ /*
+ * Extract byte: result = (state >> (byte_pos*8)) & 0xFF
+ * Then: LDS addr = result * 4 + table_base
+ */
+ if (byte_pos == 0) {
+ /* byte 0: just mask, no shift needed */
+ /* v_and_b32: src0=SGPR(mask), vsrc1=VGPR(state) */
+ _E(emit_gfx9_v_and_b32_e32, I9(buf, n), P_V(v_tmp), P_S(s_mask),
+ P_V(v_state));
+ } else if (byte_pos == 3) {
+ /* byte 3: shift right 24, no mask needed */
+ /* v_lshrrev: vdst = vsrc1 >> src0 = v_state >> 24 */
+ _E(emit_gfx9_v_lshrrev_b32, I9(buf, n), P_V(v_tmp), P_I(24),
+ P_V(v_state));
+ } else {
+ /* byte 1 or 2: shift then mask */
+ _E(emit_gfx9_v_lshrrev_b32, I9(buf, n), P_V(v_tmp),
+ P_I(byte_pos * 8), P_V(v_state));
+ _E(emit_gfx9_v_and_b32_e32, I9(buf, n), P_V(v_tmp), P_S(s_mask),
+ P_V(v_tmp));
+ }
+
+ /* LDS addr = byte * 4 + table_base */
+ /* v_lshlrev: vdst = vsrc1 << src0 = v_tmp << 2 */
+ _E(emit_gfx9_v_lshlrev_b32, I9(buf, n), P_V(v_addr), P_I(2),
+ P_V(v_tmp));
+ if (table_base > 0)
+ _E(emit_gfx9_v_add_u32, I9(buf, n), P_V(v_addr),
+ P_L(table_base), P_V(v_addr));
+
+ /* Issue LDS read (don't wait yet -- caller batches reads) */
+ _E(emit_gfx9_ds_read_b32, I9(buf, n), v_val, v_addr, 0);
+
+ return n;
+}
+
+/*
+ * Emit one full AES round (rounds 1 to Nr-1).
+ *
+ * @buf, @n: instruction buffer and position
+ * @s0-s3: input state VGPRs
+ * @d0-d3: output state VGPRs (new state)
+ * @rk: SGPR base for round key (4 consecutive SGPRs)
+ * @s_mask: SGPR holding 0xFF
+ * @v_tmp: temp VGPR for byte extraction
+ * @v_addr: temp VGPR for LDS address
+ * @vt0-vt3: temp VGPRs for 4 T-table values per column
+ */
+static int emit_aes_round_gfx9(u32 *buf, int n,
+ int s0, int s1, int s2, int s3,
+ int d0, int d1, int d2, int d3,
+ int rk, int s_mask, int v_tmp, int v_addr,
+ int vt0, int vt1, int vt2, int vt3)
+{
+ /*
+ * Issue all 16 LDS reads (4 per column), then wait once.
+ * This maximizes LDS throughput by overlapping reads.
+ *
+ * Column 0: T0[s0.b0] ^ T1[s1.b1] ^ T2[s2.b2] ^ T3[s3.b3]
+ * Column 1: T0[s1.b0] ^ T1[s2.b1] ^ T2[s3.b2] ^ T3[s0.b3]
+ * Column 2: T0[s2.b0] ^ T1[s3.b1] ^ T2[s0.b2] ^ T3[s1.b3]
+ * Column 3: T0[s3.b0] ^ T1[s0.b1] ^ T2[s1.b2] ^ T3[s2.b3]
+ *
+ * We reuse d0-d3 and vt0-vt3 as temporaries for the 16 results.
+ * Process column by column to minimize register pressure.
+ */
+
+ /* ---- Column 0 ---- */
+ n = emit_ttable_lookup_gfx9(buf, n, v_addr, vt0, s0, 0, 0, s_mask,
+ v_tmp);
+ n = emit_ttable_lookup_gfx9(buf, n, v_addr, vt1, s1, 1, 1024, s_mask,
+ v_tmp);
+ n = emit_ttable_lookup_gfx9(buf, n, v_addr, vt2, s2, 2, 2048, s_mask,
+ v_tmp);
+ n = emit_ttable_lookup_gfx9(buf, n, v_addr, vt3, s3, 3, 3072, s_mask,
+ v_tmp);
+ _E(emit_gfx9_s_waitcnt, I9(buf, n), 0xF, 0); /* lgkmcnt=0 */
+
+ /* XOR: d0 = vt0 ^ vt1 ^ vt2 ^ vt3 ^ rk */
+ _E(emit_gfx9_v_xor_b32_e32, I9(buf, n), P_V(d0), P_V(vt0), P_V(vt1));
+ _E(emit_gfx9_v_xor_b32_e32, I9(buf, n), P_V(d0), P_V(d0), P_V(vt2));
+ _E(emit_gfx9_v_xor_b32_e32, I9(buf, n), P_V(d0), P_V(d0), P_V(vt3));
+ _E(emit_gfx9_v_xor_b32_e32, I9(buf, n), P_V(d0), P_S(rk), P_V(d0));
+
+ /* ---- Column 1 ---- */
+ n = emit_ttable_lookup_gfx9(buf, n, v_addr, vt0, s1, 0, 0, s_mask,
+ v_tmp);
+ n = emit_ttable_lookup_gfx9(buf, n, v_addr, vt1, s2, 1, 1024, s_mask,
+ v_tmp);
+ n = emit_ttable_lookup_gfx9(buf, n, v_addr, vt2, s3, 2, 2048, s_mask,
+ v_tmp);
+ n = emit_ttable_lookup_gfx9(buf, n, v_addr, vt3, s0, 3, 3072, s_mask,
+ v_tmp);
+ _E(emit_gfx9_s_waitcnt, I9(buf, n), 0xF, 0);
+
+ _E(emit_gfx9_v_xor_b32_e32, I9(buf, n), P_V(d1), P_V(vt0), P_V(vt1));
+ _E(emit_gfx9_v_xor_b32_e32, I9(buf, n), P_V(d1), P_V(d1), P_V(vt2));
+ _E(emit_gfx9_v_xor_b32_e32, I9(buf, n), P_V(d1), P_V(d1), P_V(vt3));
+ _E(emit_gfx9_v_xor_b32_e32, I9(buf, n), P_V(d1), P_S(rk + 1), P_V(d1));
+
+ /* ---- Column 2 ---- */
+ n = emit_ttable_lookup_gfx9(buf, n, v_addr, vt0, s2, 0, 0, s_mask,
+ v_tmp);
+ n = emit_ttable_lookup_gfx9(buf, n, v_addr, vt1, s3, 1, 1024, s_mask,
+ v_tmp);
+ n = emit_ttable_lookup_gfx9(buf, n, v_addr, vt2, s0, 2, 2048, s_mask,
+ v_tmp);
+ n = emit_ttable_lookup_gfx9(buf, n, v_addr, vt3, s1, 3, 3072, s_mask,
+ v_tmp);
+ _E(emit_gfx9_s_waitcnt, I9(buf, n), 0xF, 0);
+
+ _E(emit_gfx9_v_xor_b32_e32, I9(buf, n), P_V(d2), P_V(vt0), P_V(vt1));
+ _E(emit_gfx9_v_xor_b32_e32, I9(buf, n), P_V(d2), P_V(d2), P_V(vt2));
+ _E(emit_gfx9_v_xor_b32_e32, I9(buf, n), P_V(d2), P_V(d2), P_V(vt3));
+ _E(emit_gfx9_v_xor_b32_e32, I9(buf, n), P_V(d2), P_S(rk + 2), P_V(d2));
+
+ /* ---- Column 3 ---- */
+ n = emit_ttable_lookup_gfx9(buf, n, v_addr, vt0, s3, 0, 0, s_mask,
+ v_tmp);
+ n = emit_ttable_lookup_gfx9(buf, n, v_addr, vt1, s0, 1, 1024, s_mask,
+ v_tmp);
+ n = emit_ttable_lookup_gfx9(buf, n, v_addr, vt2, s1, 2, 2048, s_mask,
+ v_tmp);
+ n = emit_ttable_lookup_gfx9(buf, n, v_addr, vt3, s2, 3, 3072, s_mask,
+ v_tmp);
+ _E(emit_gfx9_s_waitcnt, I9(buf, n), 0xF, 0);
+
+ _E(emit_gfx9_v_xor_b32_e32, I9(buf, n), P_V(d3), P_V(vt0), P_V(vt1));
+ _E(emit_gfx9_v_xor_b32_e32, I9(buf, n), P_V(d3), P_V(d3), P_V(vt2));
+ _E(emit_gfx9_v_xor_b32_e32, I9(buf, n), P_V(d3), P_V(d3), P_V(vt3));
+ _E(emit_gfx9_v_xor_b32_e32, I9(buf, n), P_V(d3), P_S(rk + 3), P_V(d3));
+
+ return n;
+}
+
+/*
+ * Emit the last AES round (SubBytes + ShiftRows + AddRoundKey, no MixColumns).
+ *
+ * S-box is extracted from T0: S(x) = (T0[x] >> 8) & 0xFF
+ * (In standard AES T-table encoding: T0[x] = {2*S(x), S(x), S(x), 3*S(x)})
+ *
+ * Last round output:
+ * d[c] = S(s[c].b0) | (S(s[(c+1)%4].b1)<<8) |
+ * (S(s[(c+2)%4].b2)<<16) | (S(s[(c+3)%4].b3)<<24) ^ rk[c]
+ */
+static int emit_aes_last_round_gfx9(u32 *buf, int n,
+ int s0, int s1, int s2, int s3,
+ int d0, int d1, int d2, int d3,
+ int rk, int s_mask, int v_tmp, int v_addr,
+ int vt0, int vt1, int vt2, int vt3)
+{
+ /*
+ * For each column, look up T0 for all 4 bytes,
+ * extract S(x) = (T0[x] >> 8) & 0xFF, then assemble.
+ */
+ int cols[4][4] = {
+ {s0, s1, s2, s3}, /* column 0 */
+ {s1, s2, s3, s0}, /* column 1 */
+ {s2, s3, s0, s1}, /* column 2 */
+ {s3, s0, s1, s2}, /* column 3 */
+ };
+ int dsts[4] = {d0, d1, d2, d3};
+ int col;
+
+ for (col = 0; col < 4; col++) {
+ /* Look up T0 for all 4 bytes of this column */
+ n = emit_ttable_lookup_gfx9(buf, n, v_addr, vt0, cols[col][0],
+ 0, 0, s_mask, v_tmp);
+ n = emit_ttable_lookup_gfx9(buf, n, v_addr, vt1, cols[col][1],
+ 1, 0, s_mask, v_tmp);
+ n = emit_ttable_lookup_gfx9(buf, n, v_addr, vt2, cols[col][2],
+ 2, 0, s_mask, v_tmp);
+ n = emit_ttable_lookup_gfx9(buf, n, v_addr, vt3, cols[col][3],
+ 3, 0, s_mask, v_tmp);
+ _E(emit_gfx9_s_waitcnt, I9(buf, n), 0xF, 0);
+
+ /*
+ * Extract S(x) from T0[x]:
+ * S(x) = (T0[x] >> 8) & 0xFF
+ */
+ /* byte 0: S(x) in bits [15:8] of T0, place in bits [7:0] */
+ _E(emit_gfx9_v_lshrrev_b32, I9(buf, n), P_V(vt0), P_I(8),
+ P_V(vt0));
+ _E(emit_gfx9_v_and_b32_e32, I9(buf, n), P_V(vt0), P_S(s_mask),
+ P_V(vt0));
+
+ /* byte 1: S(x) in bits [15:8], place in bits [15:8] */
+ _E(emit_gfx9_v_and_b32_e32, I9(buf, n), P_V(vt1), P_L(0xFF00),
+ P_V(vt1));
+
+ /* byte 2: S(x) in bits [15:8], place in bits [23:16] */
+ _E(emit_gfx9_v_lshlrev_b32, I9(buf, n), P_V(vt2), P_I(8),
+ P_V(vt2));
+ _E(emit_gfx9_v_and_b32_e32, I9(buf, n), P_V(vt2), P_L(0xFF0000),
+ P_V(vt2));
+
+ /* byte 3: S(x) in bits [15:8], place in bits [31:24] */
+ _E(emit_gfx9_v_lshlrev_b32, I9(buf, n), P_V(vt3), P_I(16),
+ P_V(vt3));
+ _E(emit_gfx9_v_and_b32_e32, I9(buf, n), P_V(vt3),
+ P_L(0xFF000000), P_V(vt3));
+
+ /* Assemble: d = b0 | b1 | b2 | b3 ^ rk */
+ _E(emit_gfx9_v_or_b32_e32, I9(buf, n), P_V(dsts[col]), P_V(vt0),
+ P_V(vt1));
+ _E(emit_gfx9_v_or_b32_e32, I9(buf, n), P_V(dsts[col]),
+ P_V(dsts[col]), P_V(vt2));
+ _E(emit_gfx9_v_or_b32_e32, I9(buf, n), P_V(dsts[col]),
+ P_V(dsts[col]), P_V(vt3));
+ _E(emit_gfx9_v_xor_b32_e32, I9(buf, n), P_V(dsts[col]),
+ P_S(rk + col), P_V(dsts[col]));
+ }
+
+ return n;
+}
+
+/* ======================================================================
+ * GFX9 AES-GCM emit helpers
+ * ======================================================================
+ */
+
+/*
+ * Register allocation:
+ *
+ * SGPRs:
+ * s[0:3] system: private_segment_buffer
+ * s[4:5] system: dispatch_ptr
+ * s[6:7] system: queue_ptr
+ * s[8:9] system: kernarg_segment_ptr
+ * s[10:11] system: dispatch_id
+ * s[12:13] system: flat_scratch_init
+ * s14 system: private_segment_size
+ * s15 system: workgroup_id_x
+ * s16 system: workgroup_id_y (batch lane)
+ * s17 system: workgroup_id_z
+ *
+ * s[18:19] subparam base address (computed)
+ * s[20:21] input buffer address (from subparam)
+ * s[22:23] output buffer address (from subparam)
+ * s[24:25] round keys address (from subparam)
+ * s26 nbytes (from subparam)
+ * s27 temp / block count
+ * s[28:31] current round key (loaded per-round)
+ * s32 0xFF constant
+ * s[34:35] T-table VRAM address pair (temp)
+ * s[36:37] IV word 0-1 (from subparam.iv)
+ * s38 IV word 2 (from subparam.iv, 4 bytes)
+ * s[48:49] saved EXEC
+ *
+ * VGPRs:
+ * v0 workitem_id_x (tid)
+ * v[1:4] AES state A
+ * v[5:8] AES state B / T-table results
+ * v9 byte extraction temp
+ * v10 LDS address temp
+ * v11 block_id (global)
+ * v[12:13] 64-bit global memory address
+ * v[14:17] data words (plaintext/ciphertext)
+ * v18 temp
+ */
+
+#define SR_KEYS 24 /* s[24:25] */
+#define SR_NBLOCKS 27
+#define SR_RK 28 /* s[28:31] round key */
+#define SR_MASK 32 /* s32 = 0xFF */
+#define SR_LOOP_CTR 33 /* s33 = loop counter (Phase 4) */
+#define SR_T_ADDR 34 /* s[34:35] temp for T-table load */
+#define SR_IV0 36 /* s36 = IV word 0 */
+#define SR_IV1 37 /* s37 = IV word 1 */
+#define SR_IV2 38 /* s38 = IV word 2 */
+#define SR_NR_ROUNDS 39 /* s39 = nr_rounds from subparam */
+#define SR_EXEC_SAVE 48 /* s[48:49] */
+
+#define VR_TID 0 /* workitem_id_x */
+#define VR_S0 1 /* state A: v[1:4] */
+#define VR_S1 2
+#define VR_S2 3
+#define VR_S3 4
+#define VR_D0 5 /* state B / T-table: v[5:8] */
+#define VR_D1 6
+#define VR_D2 7
+#define VR_D3 8
+#define VR_TMP 9 /* byte extraction temp */
+#define VR_ADDR 10 /* LDS address temp */
+#define VR_BLK 11 /* block_id */
+#define VR_GA_LO 12 /* 64-bit global addr lo */
+#define VR_GA_HI 13 /* 64-bit global addr hi */
+#define VR_DATA0 14 /* plaintext/ciphertext v[14:17] */
+#define VR_DATA1 15
+#define VR_DATA2 16
+#define VR_DATA3 17
+#define VR_TMP2 18
+/* Phase 5.5: saved AES(K, J0) result for ICV finalization */
+#define VR_J0_0 19
+#define VR_J0_1 20
+#define VR_J0_2 21
+#define VR_J0_3 22
+
+/* Phase 7 GHASH: additional SGPR for exec save during sub-masking */
+#define SR_GHASH_EXEC 40 /* s[40:41] */
+#define SR_BSWAP 44 /* s44 = 0x00010203 (bswap32 selector for v_perm_b32) */
+#define SR_RK2 56 /* s[56:59] alternate round key for double-buffered AES */
+
+/*
+ * sizeof(knod_aesgcm_subparam) = 80
+ * Offsets within subparam:
+ * 0: u64 in
+ * 8: u64 out
+ * 16: u64 keys
+ * 24: u64 h_table
+ * 32: u64 aad
+ * 40: u64 tag
+ * 48: u32 nbytes
+ * 52: u32 aad_len
+ * 56: u8 iv[12] (3 words at offsets 56, 60, 64 -- last only 4 bytes)
+ * 68: u32 op
+ * 72: u32 nr_rounds (10 or 14)
+ * 76: u32 _pad
+ *
+ * T-table addresses in knod_aesgcm_param (after sub[128]):
+ * 128 * 80 = 10240: u64 t0
+ * 10248: u64 t1
+ * 10256: u64 t2
+ * 10264: u64 t3
+ */
+#define SUBPARAM_SIZE 80
+
+#define OFF_T0 (AESGCM_MAX_DIM_Y * SUBPARAM_SIZE)
+
+/* ======================================================================
+ * GFX9 AES Block Encrypt Helper
+ *
+ * Encrypts the 128-bit block in v[VR_S0:VR_S3] using T-tables in LDS.
+ * Expects SR_KEYS = round keys GPU addr (will be advanced).
+ * Expects SR_NR_ROUNDS = number of AES rounds.
+ * Result in v[VR_S0:VR_S3].
+ * Clobbers: v[VR_D0:VR_D3], v[VR_DATA0:VR_DATA3], v[VR_TMP], v[VR_ADDR],
+ * s[SR_RK:SR_RK+3], s[SR_NBLOCKS], s[SR_LOOP_CTR], s[SR_KEYS+1]
+ * ======================================================================
+ */
+static int emit_aes_encrypt_block_gfx9(u32 *buf, int n)
+{
+ int br_loop, loop_top;
+
+ /* Round 0: XOR with first round key */
+ _E(emit_gfx9_s_load_dwordx4, I9(buf, n), P_S(SR_RK), P_S(SR_KEYS), 0);
+ _E(emit_gfx9_s_waitcnt_lgkmcnt, I9(buf, n));
+ _E(emit_gfx9_v_xor_b32_e32, I9(buf, n), P_V(VR_S0), P_S(SR_RK),
+ P_V(VR_S0));
+ _E(emit_gfx9_v_xor_b32_e32, I9(buf, n), P_V(VR_S1), P_S(SR_RK + 1),
+ P_V(VR_S1));
+ _E(emit_gfx9_v_xor_b32_e32, I9(buf, n), P_V(VR_S2), P_S(SR_RK + 2),
+ P_V(VR_S2));
+ _E(emit_gfx9_v_xor_b32_e32, I9(buf, n), P_V(VR_S3), P_S(SR_RK + 3),
+ P_V(VR_S3));
+
+ _E(emit_gfx9_s_add_u32, I9(buf, n), P_S(SR_KEYS), P_I(16),
+ P_S(SR_KEYS));
+ _E(emit_gfx9_s_addc_u32, I9(buf, n), P_S(SR_KEYS + 1), P_I(0),
+ P_S(SR_KEYS + 1));
+
+ /* pair_count = nr_rounds / 2 - 1 */
+ _E(emit_gfx9_s_lshr_b32, I9(buf, n), P_S(SR_NBLOCKS), P_S(SR_NR_ROUNDS),
+ P_I(1));
+ _E(emit_gfx9_s_add_u32, I9(buf, n), P_S(SR_NBLOCKS), P_L(0xFFFFFFFFu),
+ P_S(SR_NBLOCKS));
+
+ _E(emit_gfx9_s_mov_b32, I9(buf, n), P_S(SR_LOOP_CTR), P_I(0));
+
+ /* Prefetch round 1 key - overlaps with loop-entry overhead */
+ _E(emit_gfx9_s_load_dwordx4, I9(buf, n), P_S(SR_RK), P_S(SR_KEYS), 0);
+
+ loop_top = n;
+
+ /* Odd round: S -> D (SR_RK was prefetched) */
+ _E(emit_gfx9_s_waitcnt_lgkmcnt, I9(buf, n));
+ _E(emit_gfx9_s_add_u32, I9(buf, n), P_S(SR_KEYS), P_I(16),
+ P_S(SR_KEYS));
+ _E(emit_gfx9_s_addc_u32, I9(buf, n), P_S(SR_KEYS + 1), P_I(0),
+ P_S(SR_KEYS + 1));
+ _E(emit_gfx9_s_load_dwordx4, I9(buf, n), P_S(SR_RK2), P_S(SR_KEYS), 0);
+ n = emit_aes_round_gfx9(buf, n, VR_S0, VR_S1, VR_S2, VR_S3,
+ VR_D0, VR_D1, VR_D2, VR_D3,
+ SR_RK, SR_MASK, VR_TMP, VR_ADDR,
+ VR_DATA0, VR_DATA1, VR_DATA2, VR_DATA3);
+
+ /* Even round: D -> S (SR_RK2 was prefetched during odd round) */
+ _E(emit_gfx9_s_waitcnt_lgkmcnt, I9(buf, n));
+ _E(emit_gfx9_s_add_u32, I9(buf, n), P_S(SR_KEYS), P_I(16),
+ P_S(SR_KEYS));
+ _E(emit_gfx9_s_addc_u32, I9(buf, n), P_S(SR_KEYS + 1), P_I(0),
+ P_S(SR_KEYS + 1));
+ _E(emit_gfx9_s_load_dwordx4, I9(buf, n), P_S(SR_RK), P_S(SR_KEYS), 0);
+ n = emit_aes_round_gfx9(buf, n, VR_D0, VR_D1, VR_D2, VR_D3,
+ VR_S0, VR_S1, VR_S2, VR_S3,
+ SR_RK2, SR_MASK, VR_TMP, VR_ADDR,
+ VR_DATA0, VR_DATA1, VR_DATA2, VR_DATA3);
+
+ _E(emit_gfx9_s_add_u32, I9(buf, n), P_S(SR_LOOP_CTR), P_I(1),
+ P_S(SR_LOOP_CTR));
+ _E(emit_gfx9_s_cmp_lt_u32, I9(buf, n), P_S(SR_LOOP_CTR),
+ P_S(SR_NBLOCKS));
+ br_loop = _BR(emit_gfx9_s_cbranch_scc1, I9(buf, n), 0);
+ patch_branch(buf, br_loop, loop_top);
+
+ /* Final odd round: S -> D (SR_RK prefetched from last even) */
+ _E(emit_gfx9_s_waitcnt_lgkmcnt, I9(buf, n));
+ _E(emit_gfx9_s_add_u32, I9(buf, n), P_S(SR_KEYS), P_I(16),
+ P_S(SR_KEYS));
+ _E(emit_gfx9_s_addc_u32, I9(buf, n), P_S(SR_KEYS + 1), P_I(0),
+ P_S(SR_KEYS + 1));
+ _E(emit_gfx9_s_load_dwordx4, I9(buf, n), P_S(SR_RK2), P_S(SR_KEYS), 0);
+ n = emit_aes_round_gfx9(buf, n, VR_S0, VR_S1, VR_S2, VR_S3,
+ VR_D0, VR_D1, VR_D2, VR_D3,
+ SR_RK, SR_MASK, VR_TMP, VR_ADDR,
+ VR_DATA0, VR_DATA1, VR_DATA2, VR_DATA3);
+
+ /* Last round: D -> S (SR_RK2 prefetched during final odd) */
+ _E(emit_gfx9_s_waitcnt_lgkmcnt, I9(buf, n));
+ n = emit_aes_last_round_gfx9(buf, n, VR_D0, VR_D1, VR_D2, VR_D3,
+ VR_S0, VR_S1, VR_S2, VR_S3,
+ SR_RK2, SR_MASK, VR_TMP, VR_ADDR,
+ VR_DATA0, VR_DATA1, VR_DATA2, VR_DATA3);
+
+ return n;
+}
+
+/* ======================================================================
+ * GFX9 GF(2^128) Multiply Helper
+ *
+ * Computes Z = X * Y in GF(2^128) with GCM polynomial.
+ *
+ * Input: X in v[VR_DATA0:VR_DATA3], Y in v[VR_D0:VR_D3]
+ * Output: Z in v[VR_S0:VR_S3]
+ * Clobbers: v[VR_TMP], v[VR_TMP2], v[VR_ADDR] (holds 0xE1000000),
+ * v[VR_DATA0:VR_DATA3] (shifted), v[VR_D0:VR_D3] (shifted),
+ * s[SR_LOOP_CTR]
+ *
+ * GCM bit ordering: bit 0 = MSB of first byte.
+ * Algorithm: Shoup's method (right-shift V, test MSB of X).
+ * Z = 0, V = Y
+ * for i = 0..127:
+ * if MSB(X) set: Z ^= V
+ * lsb = V[3] & 1
+ * V >>= 1 (128-bit right shift)
+ * if lsb: V[0] ^= 0xE1000000
+ * X <<= 1 (128-bit left shift)
+ * ======================================================================
+ */
+static int emit_gfmul_128_gfx9(u32 *buf, int n)
+{
+ int loop_top, br_loop;
+
+ /* Z = 0 */
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(VR_S0), P_I(0));
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(VR_S1), P_I(0));
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(VR_S2), P_I(0));
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(VR_S3), P_I(0));
+
+ /* Preload reduction constant into VR_ADDR (v10) */
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(VR_ADDR), P_L(0xE1000000));
+
+ /* Loop counter */
+ _E(emit_gfx9_s_mov_b32, I9(buf, n), P_S(SR_LOOP_CTR), P_I(0));
+
+ loop_top = n;
+
+ /* Step 1: Test MSB of X[0] via signed compare (bit 31 set = negative) */
+ _E(emit_gfx9_v_cmp_gt_i32, I9(buf, n), P_I(0), P_V(VR_DATA0));
+
+ /*
+ * Step 2: Conditional Z ^= V.
+ * v_cndmask selects V[i] or 0 based on VCC, then XOR into Z.
+ * v_cndmask_b32: vdst = VCC ? vsrc1 : src0
+ * We want: VR_TMP2 = VCC ? VR_D0 : 0
+ * So src0=0, vsrc1=VR_D0
+ */
+ _E(emit_gfx9_v_cndmask_b32_e32, I9(buf, n), P_V(VR_TMP2), P_I(0),
+ P_V(VR_D0));
+ _E(emit_gfx9_v_xor_b32_e32, I9(buf, n), P_V(VR_S0), P_V(VR_S0),
+ P_V(VR_TMP2));
+ _E(emit_gfx9_v_cndmask_b32_e32, I9(buf, n), P_V(VR_TMP2), P_I(0),
+ P_V(VR_D1));
+ _E(emit_gfx9_v_xor_b32_e32, I9(buf, n), P_V(VR_S1), P_V(VR_S1),
+ P_V(VR_TMP2));
+ _E(emit_gfx9_v_cndmask_b32_e32, I9(buf, n), P_V(VR_TMP2), P_I(0),
+ P_V(VR_D2));
+ _E(emit_gfx9_v_xor_b32_e32, I9(buf, n), P_V(VR_S2), P_V(VR_S2),
+ P_V(VR_TMP2));
+ _E(emit_gfx9_v_cndmask_b32_e32, I9(buf, n), P_V(VR_TMP2), P_I(0),
+ P_V(VR_D3));
+ _E(emit_gfx9_v_xor_b32_e32, I9(buf, n), P_V(VR_S3), P_V(VR_S3),
+ P_V(VR_TMP2));
+
+ /*
+ * Step 3: Save LSB of V[3] for reduction.
+ */
+ _E(emit_gfx9_v_and_b32_e32, I9(buf, n), P_V(VR_TMP), P_I(1), P_V(VR_D3));
+
+ /*
+ * Step 4: 128-bit right shift V (v[D0:D3]).
+ * v_alignbit_b32(dst, hi, lo, 1) = {hi,lo} >> 1 (lower 32 bits).
+ */
+ _E(emit_gfx9_v_alignbit_b32, I9(buf, n), P_V(VR_D3), P_V(VR_D2),
+ P_V(VR_D3), P_I(1));
+ _E(emit_gfx9_v_alignbit_b32, I9(buf, n), P_V(VR_D2), P_V(VR_D1),
+ P_V(VR_D2), P_I(1));
+ _E(emit_gfx9_v_alignbit_b32, I9(buf, n), P_V(VR_D1), P_V(VR_D0),
+ P_V(VR_D1), P_I(1));
+ _E(emit_gfx9_v_lshrrev_b32, I9(buf, n), P_V(VR_D0), P_I(1), P_V(VR_D0));
+
+ /*
+ * Step 5: Conditional reduction V[0] ^= 0xE1000000.
+ * If saved LSB was 1, XOR with reduction polynomial.
+ * 0xE1000000 is preloaded in VR_ADDR (v10).
+ */
+ _E(emit_gfx9_v_cmp_ne_u32, I9(buf, n), P_I(0), P_V(VR_TMP));
+ /* VR_TMP2 = VCC ? VR_ADDR : 0 */
+ _E(emit_gfx9_v_cndmask_b32_e32, I9(buf, n), P_V(VR_TMP2), P_I(0),
+ P_V(VR_ADDR));
+ _E(emit_gfx9_v_xor_b32_e32, I9(buf, n), P_V(VR_D0), P_V(VR_D0),
+ P_V(VR_TMP2));
+
+ /*
+ * Step 6: 128-bit left shift X (v[DATA0:DATA3]).
+ * X[0] = (X[0] << 1) | (X[1] >> 31)
+ * X[1] = (X[1] << 1) | (X[2] >> 31)
+ * X[2] = (X[2] << 1) | (X[3] >> 31)
+ * X[3] = X[3] << 1
+ */
+ _E(emit_gfx9_v_alignbit_b32, I9(buf, n), P_V(VR_DATA0), P_V(VR_DATA0),
+ P_V(VR_DATA1), P_I(31));
+ _E(emit_gfx9_v_alignbit_b32, I9(buf, n), P_V(VR_DATA1), P_V(VR_DATA1),
+ P_V(VR_DATA2), P_I(31));
+ _E(emit_gfx9_v_alignbit_b32, I9(buf, n), P_V(VR_DATA2), P_V(VR_DATA2),
+ P_V(VR_DATA3), P_I(31));
+ _E(emit_gfx9_v_lshlrev_b32, I9(buf, n), P_V(VR_DATA3), P_I(1),
+ P_V(VR_DATA3));
+
+ /* Loop control: 128 iterations */
+ _E(emit_gfx9_s_add_u32, I9(buf, n), P_S(SR_LOOP_CTR), P_I(1),
+ P_S(SR_LOOP_CTR));
+ _E(emit_gfx9_s_cmp_lt_u32, I9(buf, n), P_S(SR_LOOP_CTR), P_L(128));
+ br_loop = _BR(emit_gfx9_s_cbranch_scc1, I9(buf, n), 0);
+ patch_branch(buf, br_loop, loop_top);
+
+ return n;
+}
+
+/* ======================================================================
+ * GFX10 (RDNA) AES-GCM emit helpers
+ *
+ * Same as GFX9 but with GFX10 encodings; GLOBAL saddr mode (scalar base
+ * + VGPR offset) avoids VOP3B v_add_co_u32 for 64-bit addresses.
+ * ======================================================================
+ */
+
+/* ---- GFX10 T-table lookup ---- */
+
+static int emit_ttable_lookup_gfx10(u32 *buf, int n,
+ int v_addr, int v_val,
+ int v_state, int byte_pos,
+ int table_base, int s_mask,
+ int v_tmp)
+{
+ if (byte_pos == 0) {
+ _E(emit_gfx10_v_and_b32_e32, I10(buf, n), P_V(v_tmp),
+ P_S(s_mask), P_V(v_state));
+ } else if (byte_pos == 3) {
+ _E(emit_gfx10_v_lshrrev_b32, I10(buf, n), P_V(v_tmp), P_I(24),
+ P_V(v_state));
+ } else {
+ _E(emit_gfx10_v_lshrrev_b32, I10(buf, n), P_V(v_tmp),
+ P_I(byte_pos * 8), P_V(v_state));
+ _E(emit_gfx10_v_and_b32_e32, I10(buf, n), P_V(v_tmp),
+ P_S(s_mask), P_V(v_tmp));
+ }
+
+ _E(emit_gfx10_v_lshlrev_b32, I10(buf, n), P_V(v_addr), P_I(2),
+ P_V(v_tmp));
+ if (table_base > 0)
+ _E(emit_gfx10_v_add_nc_u32, I10(buf, n), P_V(v_addr),
+ P_L(table_base), P_V(v_addr));
+
+ _E(emit_gfx10_ds_read_b32, I10(buf, n), v_val, v_addr);
+
+ return n;
+}
+
+/* ---- GFX10 AES round ---- */
+
+static int emit_aes_round_gfx10(u32 *buf, int n,
+ int s0, int s1, int s2, int s3,
+ int d0, int d1, int d2, int d3,
+ int rk, int s_mask, int v_tmp, int v_addr,
+ int vt0, int vt1, int vt2, int vt3)
+{
+ /* Column 0: T0[s0.b0] ^ T1[s1.b1] ^ T2[s2.b2] ^ T3[s3.b3] */
+ n = emit_ttable_lookup_gfx10(buf, n, v_addr, vt0, s0, 0, 0, s_mask,
+ v_tmp);
+ n = emit_ttable_lookup_gfx10(buf, n, v_addr, vt1, s1, 1, 1024, s_mask,
+ v_tmp);
+ n = emit_ttable_lookup_gfx10(buf, n, v_addr, vt2, s2, 2, 2048, s_mask,
+ v_tmp);
+ n = emit_ttable_lookup_gfx10(buf, n, v_addr, vt3, s3, 3, 3072, s_mask,
+ v_tmp);
+ _E(emit_gfx10_s_waitcnt, I10(buf, n), 0x3F, 0);
+
+ _E(emit_gfx10_v_xor_b32_e32, I10(buf, n), P_V(d0), P_V(vt0), P_V(vt1));
+ _E(emit_gfx10_v_xor_b32_e32, I10(buf, n), P_V(d0), P_V(d0), P_V(vt2));
+ _E(emit_gfx10_v_xor_b32_e32, I10(buf, n), P_V(d0), P_V(d0), P_V(vt3));
+ _E(emit_gfx10_v_xor_b32_e32, I10(buf, n), P_V(d0), P_S(rk), P_V(d0));
+
+ /* Column 1: T0[s1.b0] ^ T1[s2.b1] ^ T2[s3.b2] ^ T3[s0.b3] */
+ n = emit_ttable_lookup_gfx10(buf, n, v_addr, vt0, s1, 0, 0, s_mask,
+ v_tmp);
+ n = emit_ttable_lookup_gfx10(buf, n, v_addr, vt1, s2, 1, 1024, s_mask,
+ v_tmp);
+ n = emit_ttable_lookup_gfx10(buf, n, v_addr, vt2, s3, 2, 2048, s_mask,
+ v_tmp);
+ n = emit_ttable_lookup_gfx10(buf, n, v_addr, vt3, s0, 3, 3072, s_mask,
+ v_tmp);
+ _E(emit_gfx10_s_waitcnt, I10(buf, n), 0x3F, 0);
+
+ _E(emit_gfx10_v_xor_b32_e32, I10(buf, n), P_V(d1), P_V(vt0), P_V(vt1));
+ _E(emit_gfx10_v_xor_b32_e32, I10(buf, n), P_V(d1), P_V(d1), P_V(vt2));
+ _E(emit_gfx10_v_xor_b32_e32, I10(buf, n), P_V(d1), P_V(d1), P_V(vt3));
+ _E(emit_gfx10_v_xor_b32_e32, I10(buf, n), P_V(d1), P_S(rk + 1),
+ P_V(d1));
+
+ /* Column 2 */
+ n = emit_ttable_lookup_gfx10(buf, n, v_addr, vt0, s2, 0, 0, s_mask,
+ v_tmp);
+ n = emit_ttable_lookup_gfx10(buf, n, v_addr, vt1, s3, 1, 1024, s_mask,
+ v_tmp);
+ n = emit_ttable_lookup_gfx10(buf, n, v_addr, vt2, s0, 2, 2048, s_mask,
+ v_tmp);
+ n = emit_ttable_lookup_gfx10(buf, n, v_addr, vt3, s1, 3, 3072, s_mask,
+ v_tmp);
+ _E(emit_gfx10_s_waitcnt, I10(buf, n), 0x3F, 0);
+
+ _E(emit_gfx10_v_xor_b32_e32, I10(buf, n), P_V(d2), P_V(vt0), P_V(vt1));
+ _E(emit_gfx10_v_xor_b32_e32, I10(buf, n), P_V(d2), P_V(d2), P_V(vt2));
+ _E(emit_gfx10_v_xor_b32_e32, I10(buf, n), P_V(d2), P_V(d2), P_V(vt3));
+ _E(emit_gfx10_v_xor_b32_e32, I10(buf, n), P_V(d2), P_S(rk + 2),
+ P_V(d2));
+
+ /* Column 3 */
+ n = emit_ttable_lookup_gfx10(buf, n, v_addr, vt0, s3, 0, 0, s_mask,
+ v_tmp);
+ n = emit_ttable_lookup_gfx10(buf, n, v_addr, vt1, s0, 1, 1024, s_mask,
+ v_tmp);
+ n = emit_ttable_lookup_gfx10(buf, n, v_addr, vt2, s1, 2, 2048, s_mask,
+ v_tmp);
+ n = emit_ttable_lookup_gfx10(buf, n, v_addr, vt3, s2, 3, 3072, s_mask,
+ v_tmp);
+ _E(emit_gfx10_s_waitcnt, I10(buf, n), 0x3F, 0);
+
+ _E(emit_gfx10_v_xor_b32_e32, I10(buf, n), P_V(d3), P_V(vt0), P_V(vt1));
+ _E(emit_gfx10_v_xor_b32_e32, I10(buf, n), P_V(d3), P_V(d3), P_V(vt2));
+ _E(emit_gfx10_v_xor_b32_e32, I10(buf, n), P_V(d3), P_V(d3), P_V(vt3));
+ _E(emit_gfx10_v_xor_b32_e32, I10(buf, n), P_V(d3), P_S(rk + 3),
+ P_V(d3));
+
+ return n;
+}
+
+/* ---- GFX10 AES last round ---- */
+
+static int emit_aes_last_round_gfx10(u32 *buf, int n,
+ int s0, int s1, int s2, int s3,
+ int d0, int d1, int d2, int d3,
+ int rk, int s_mask, int v_tmp, int v_addr,
+ int vt0, int vt1, int vt2, int vt3)
+{
+ int cols[4][4] = {
+ {s0, s1, s2, s3},
+ {s1, s2, s3, s0},
+ {s2, s3, s0, s1},
+ {s3, s0, s1, s2},
+ };
+ int dsts[4] = {d0, d1, d2, d3};
+ int col;
+
+ for (col = 0; col < 4; col++) {
+ n = emit_ttable_lookup_gfx10(buf, n, v_addr, vt0, cols[col][0],
+ 0, 0, s_mask, v_tmp);
+ n = emit_ttable_lookup_gfx10(buf, n, v_addr, vt1, cols[col][1],
+ 1, 0, s_mask, v_tmp);
+ n = emit_ttable_lookup_gfx10(buf, n, v_addr, vt2, cols[col][2],
+ 2, 0, s_mask, v_tmp);
+ n = emit_ttable_lookup_gfx10(buf, n, v_addr, vt3, cols[col][3],
+ 3, 0, s_mask, v_tmp);
+ _E(emit_gfx10_s_waitcnt, I10(buf, n), 0x3F, 0);
+
+ /* S(x) = (T0[x] >> 8) & 0xFF */
+ _E(emit_gfx10_v_lshrrev_b32, I10(buf, n), P_V(vt0), P_I(8),
+ P_V(vt0));
+ _E(emit_gfx10_v_and_b32_e32, I10(buf, n), P_V(vt0), P_S(s_mask),
+ P_V(vt0));
+
+ _E(emit_gfx10_v_and_b32_e32, I10(buf, n), P_V(vt1), P_L(0xFF00),
+ P_V(vt1));
+
+ _E(emit_gfx10_v_lshlrev_b32, I10(buf, n), P_V(vt2), P_I(8),
+ P_V(vt2));
+ _E(emit_gfx10_v_and_b32_e32, I10(buf, n), P_V(vt2),
+ P_L(0xFF0000), P_V(vt2));
+
+ _E(emit_gfx10_v_lshlrev_b32, I10(buf, n), P_V(vt3), P_I(16),
+ P_V(vt3));
+ _E(emit_gfx10_v_and_b32_e32, I10(buf, n), P_V(vt3),
+ P_L(0xFF000000), P_V(vt3));
+
+ _E(emit_gfx10_v_or_b32_e32, I10(buf, n), P_V(dsts[col]),
+ P_V(vt0), P_V(vt1));
+ _E(emit_gfx10_v_or_b32_e32, I10(buf, n), P_V(dsts[col]),
+ P_V(dsts[col]), P_V(vt2));
+ _E(emit_gfx10_v_or_b32_e32, I10(buf, n), P_V(dsts[col]),
+ P_V(dsts[col]), P_V(vt3));
+ _E(emit_gfx10_v_xor_b32_e32, I10(buf, n), P_V(dsts[col]),
+ P_S(rk + col), P_V(dsts[col]));
+ }
+
+ return n;
+}
+
+/* ======================================================================
+ * GFX10 AES Encrypt Block Helper
+ *
+ * Same as emit_aes_encrypt_block_gfx9 but with GFX10 instructions.
+ * Input: plaintext in v[VR_S0:VR_S3]
+ * Output: ciphertext in v[VR_S0:VR_S3]
+ * Requires: SR_KEYS, SR_NR_ROUNDS set. T-tables in LDS.
+ * ======================================================================
+ */
+static int emit_aes_encrypt_block_gfx10(u32 *buf, int n)
+{
+ int br_loop, loop_top;
+
+ /* Round 0: XOR with first round key */
+ _E(emit_gfx10_s_load_dwordx4, I10(buf, n), P_S(SR_RK), P_S(SR_KEYS), 0);
+ _E(emit_gfx10_s_waitcnt_lgkmcnt, I10(buf, n));
+ _E(emit_gfx10_v_xor_b32_e32, I10(buf, n), P_V(VR_S0), P_S(SR_RK),
+ P_V(VR_S0));
+ _E(emit_gfx10_v_xor_b32_e32, I10(buf, n), P_V(VR_S1), P_S(SR_RK + 1),
+ P_V(VR_S1));
+ _E(emit_gfx10_v_xor_b32_e32, I10(buf, n), P_V(VR_S2), P_S(SR_RK + 2),
+ P_V(VR_S2));
+ _E(emit_gfx10_v_xor_b32_e32, I10(buf, n), P_V(VR_S3), P_S(SR_RK + 3),
+ P_V(VR_S3));
+
+ _E(emit_gfx10_s_add_u32, I10(buf, n), P_S(SR_KEYS), P_I(16),
+ P_S(SR_KEYS));
+ _E(emit_gfx10_s_addc_u32, I10(buf, n), P_S(SR_KEYS + 1), P_I(0),
+ P_S(SR_KEYS + 1));
+
+ /* pair_count = nr_rounds / 2 - 1 */
+ _E(emit_gfx10_s_lshr_b32, I10(buf, n), P_S(SR_NBLOCKS),
+ P_S(SR_NR_ROUNDS), P_I(1));
+ _E(emit_gfx10_s_add_u32, I10(buf, n), P_S(SR_NBLOCKS), P_L(0xFFFFFFFFu),
+ P_S(SR_NBLOCKS));
+
+ _E(emit_gfx10_s_mov_b32, I10(buf, n), P_S(SR_LOOP_CTR), P_I(0));
+
+ /* Prefetch round 1 key - overlaps with loop-entry overhead */
+ _E(emit_gfx10_s_load_dwordx4, I10(buf, n), P_S(SR_RK), P_S(SR_KEYS), 0);
+
+ loop_top = n;
+
+ /* Odd round: S -> D (SR_RK was prefetched) */
+ _E(emit_gfx10_s_waitcnt_lgkmcnt, I10(buf, n));
+ _E(emit_gfx10_s_add_u32, I10(buf, n), P_S(SR_KEYS), P_I(16),
+ P_S(SR_KEYS));
+ _E(emit_gfx10_s_addc_u32, I10(buf, n), P_S(SR_KEYS + 1), P_I(0),
+ P_S(SR_KEYS + 1));
+ _E(emit_gfx10_s_load_dwordx4, I10(buf, n), P_S(SR_RK2), P_S(SR_KEYS),
+ 0);
+ n = emit_aes_round_gfx10(buf, n, VR_S0, VR_S1, VR_S2, VR_S3,
+ VR_D0, VR_D1, VR_D2, VR_D3,
+ SR_RK, SR_MASK, VR_TMP, VR_ADDR,
+ VR_DATA0, VR_DATA1, VR_DATA2, VR_DATA3);
+
+ /* Even round: D -> S (SR_RK2 was prefetched during odd round) */
+ _E(emit_gfx10_s_waitcnt_lgkmcnt, I10(buf, n));
+ _E(emit_gfx10_s_add_u32, I10(buf, n), P_S(SR_KEYS), P_I(16),
+ P_S(SR_KEYS));
+ _E(emit_gfx10_s_addc_u32, I10(buf, n), P_S(SR_KEYS + 1), P_I(0),
+ P_S(SR_KEYS + 1));
+ _E(emit_gfx10_s_load_dwordx4, I10(buf, n), P_S(SR_RK), P_S(SR_KEYS), 0);
+ n = emit_aes_round_gfx10(buf, n, VR_D0, VR_D1, VR_D2, VR_D3,
+ VR_S0, VR_S1, VR_S2, VR_S3,
+ SR_RK2, SR_MASK, VR_TMP, VR_ADDR,
+ VR_DATA0, VR_DATA1, VR_DATA2, VR_DATA3);
+
+ _E(emit_gfx10_s_add_u32, I10(buf, n), P_S(SR_LOOP_CTR), P_I(1),
+ P_S(SR_LOOP_CTR));
+ _E(emit_gfx10_s_cmp_lt_u32, I10(buf, n), P_S(SR_LOOP_CTR),
+ P_S(SR_NBLOCKS));
+ br_loop = _BR(emit_gfx10_s_cbranch_scc1, I10(buf, n), 0);
+ patch_branch(buf, br_loop, loop_top);
+
+ /* Final odd round: S -> D (SR_RK prefetched from last even) */
+ _E(emit_gfx10_s_waitcnt_lgkmcnt, I10(buf, n));
+ _E(emit_gfx10_s_add_u32, I10(buf, n), P_S(SR_KEYS), P_I(16),
+ P_S(SR_KEYS));
+ _E(emit_gfx10_s_addc_u32, I10(buf, n), P_S(SR_KEYS + 1), P_I(0),
+ P_S(SR_KEYS + 1));
+ _E(emit_gfx10_s_load_dwordx4, I10(buf, n), P_S(SR_RK2), P_S(SR_KEYS),
+ 0);
+ n = emit_aes_round_gfx10(buf, n, VR_S0, VR_S1, VR_S2, VR_S3,
+ VR_D0, VR_D1, VR_D2, VR_D3,
+ SR_RK, SR_MASK, VR_TMP, VR_ADDR,
+ VR_DATA0, VR_DATA1, VR_DATA2, VR_DATA3);
+
+ /* Last round: D -> S (SR_RK2 prefetched during final odd) */
+ _E(emit_gfx10_s_waitcnt_lgkmcnt, I10(buf, n));
+ n = emit_aes_last_round_gfx10(buf, n, VR_D0, VR_D1, VR_D2, VR_D3,
+ VR_S0, VR_S1, VR_S2, VR_S3,
+ SR_RK2, SR_MASK, VR_TMP, VR_ADDR,
+ VR_DATA0, VR_DATA1, VR_DATA2, VR_DATA3);
+
+ return n;
+}
+
+/* ======================================================================
+ * GFX10 GF(2^128) Multiply Helper
+ *
+ * Same algorithm as GFX9 version but with GFX10 instructions.
+ * Input: X in v[VR_DATA0:VR_DATA3], Y in v[VR_D0:VR_D3]
+ * Output: Z in v[VR_S0:VR_S3]
+ * ======================================================================
+ */
+static int emit_gfmul_128_gfx10(u32 *buf, int n)
+{
+ int loop_top, br_loop;
+
+ /* Z = 0 */
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(VR_S0), P_I(0));
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(VR_S1), P_I(0));
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(VR_S2), P_I(0));
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(VR_S3), P_I(0));
+
+ /* Preload reduction constant into VR_ADDR (v10) */
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(VR_ADDR),
+ P_L(0xE1000000));
+
+ _E(emit_gfx10_s_mov_b32, I10(buf, n), P_S(SR_LOOP_CTR), P_I(0));
+
+ loop_top = n;
+
+ /* Step 1: Test MSB of X[0] via signed compare (bit 31 set = negative) */
+ _E(emit_gfx10_v_cmp_gt_i32, I10(buf, n), P_I(0), P_V(VR_DATA0));
+
+ /* Step 2: Conditional Z ^= V */
+ _E(emit_gfx10_v_cndmask_b32_e32, I10(buf, n), P_V(VR_TMP2), P_I(0),
+ P_V(VR_D0));
+ _E(emit_gfx10_v_xor_b32_e32, I10(buf, n), P_V(VR_S0), P_V(VR_S0),
+ P_V(VR_TMP2));
+ _E(emit_gfx10_v_cndmask_b32_e32, I10(buf, n), P_V(VR_TMP2), P_I(0),
+ P_V(VR_D1));
+ _E(emit_gfx10_v_xor_b32_e32, I10(buf, n), P_V(VR_S1), P_V(VR_S1),
+ P_V(VR_TMP2));
+ _E(emit_gfx10_v_cndmask_b32_e32, I10(buf, n), P_V(VR_TMP2), P_I(0),
+ P_V(VR_D2));
+ _E(emit_gfx10_v_xor_b32_e32, I10(buf, n), P_V(VR_S2), P_V(VR_S2),
+ P_V(VR_TMP2));
+ _E(emit_gfx10_v_cndmask_b32_e32, I10(buf, n), P_V(VR_TMP2), P_I(0),
+ P_V(VR_D3));
+ _E(emit_gfx10_v_xor_b32_e32, I10(buf, n), P_V(VR_S3), P_V(VR_S3),
+ P_V(VR_TMP2));
+
+ /* Step 3: Save LSB of V[3] */
+ _E(emit_gfx10_v_and_b32_e32, I10(buf, n), P_V(VR_TMP), P_I(1),
+ P_V(VR_D3));
+
+ /* Step 4: 128-bit right shift V */
+ _E(emit_gfx10_v_alignbit_b32, I10(buf, n), P_V(VR_D3), P_V(VR_D2),
+ P_V(VR_D3), P_I(1));
+ _E(emit_gfx10_v_alignbit_b32, I10(buf, n), P_V(VR_D2), P_V(VR_D1),
+ P_V(VR_D2), P_I(1));
+ _E(emit_gfx10_v_alignbit_b32, I10(buf, n), P_V(VR_D1), P_V(VR_D0),
+ P_V(VR_D1), P_I(1));
+ _E(emit_gfx10_v_lshrrev_b32, I10(buf, n), P_V(VR_D0), P_I(1),
+ P_V(VR_D0));
+
+ /* Step 5: Conditional reduction V[0] ^= 0xE1000000 */
+ _E(emit_gfx10_v_cmp_ne_u32, I10(buf, n), P_I(0), P_V(VR_TMP));
+ _E(emit_gfx10_v_cndmask_b32_e32, I10(buf, n), P_V(VR_TMP2), P_I(0),
+ P_V(VR_ADDR));
+ _E(emit_gfx10_v_xor_b32_e32, I10(buf, n), P_V(VR_D0), P_V(VR_D0),
+ P_V(VR_TMP2));
+
+ /* Step 6: 128-bit left shift X */
+ _E(emit_gfx10_v_alignbit_b32, I10(buf, n), P_V(VR_DATA0), P_V(VR_DATA0),
+ P_V(VR_DATA1), P_I(31));
+ _E(emit_gfx10_v_alignbit_b32, I10(buf, n), P_V(VR_DATA1), P_V(VR_DATA1),
+ P_V(VR_DATA2), P_I(31));
+ _E(emit_gfx10_v_alignbit_b32, I10(buf, n), P_V(VR_DATA2), P_V(VR_DATA2),
+ P_V(VR_DATA3), P_I(31));
+ _E(emit_gfx10_v_lshlrev_b32, I10(buf, n), P_V(VR_DATA3), P_I(1),
+ P_V(VR_DATA3));
+
+ /* Loop control: 128 iterations */
+ _E(emit_gfx10_s_add_u32, I10(buf, n), P_S(SR_LOOP_CTR), P_I(1),
+ P_S(SR_LOOP_CTR));
+ _E(emit_gfx10_s_cmp_lt_u32, I10(buf, n), P_S(SR_LOOP_CTR), P_L(128));
+ br_loop = _BR(emit_gfx10_s_cbranch_scc1, I10(buf, n), 0);
+ patch_branch(buf, br_loop, loop_top);
+
+ return n;
+}
+
+#undef _E
+#undef _BR
+
+#endif /* AESGCM_SHADER_H_ */
diff --git a/drivers/gpu/drm/amd/amdkfd/knod/ipsec_fused_gfx10.h b/drivers/gpu/drm/amd/amdkfd/knod/ipsec_fused_gfx10.h
new file mode 100644
index 000000000000..555bf2962f73
--- /dev/null
+++ b/drivers/gpu/drm/amd/amdkfd/knod/ipsec_fused_gfx10.h
@@ -0,0 +1,1796 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (c) 2021 Taehee Yoo <ap420073@gmail.com>
+ * Copyright (c) 2021 Hoyeon Lee <hoyeon.rhee@gmail.com>
+ */
+
+/*
+ * KNOD IPsec fused RX shader - GFX9 (Vega10/20).
+ *
+ * Full AES-GCM decrypt pipeline for inbound ESP packets:
+ * 1) ESP header parse -> SPI + seq extract
+ * 2) SA table linear scan -> resolve SPI to slot index
+ * 3) Cooperative T-table load (VRAM -> LDS, 256 threads)
+ * 4) AES-CTR decrypt ciphertext -> out_addr
+ * 5) Parallel GHASH over (AAD || ciphertext || len)
+ * 6) ICV verify (GHASH ^ AES(K,J0) vs received tag)
+ * 7) ESP trailer strip -> inner_len
+ * 8) Write verdict to bd->act, inner_len to bd->len
+ *
+ * Dispatch geometry:
+ * workgroup = (256, 1, 1) - 256 threads = 1 AES block per thread
+ * grid = (256, nr_pkts, 1)
+ * workgroup_id_y == packet index in the batch
+ *
+ * Anti-replay is NOT in the shader - CPU-side sliding window in NIC NAPI.
+ *
+ * Verdict encoding in bd->act high32:
+ * 0..NR_SA-1 - SA hit + ICV pass, value is slot_idx
+ * 0xFFFFFFFF - SA miss (no entry for this SPI)
+ * 0xFFFFFFFE - non-IPv4/IPv6 bypass (unknown L3 protocol)
+ * 0xFFFFFFFD - ICV mismatch (decrypt succeeded but tag wrong)
+ *
+ * bd->len is set to inner_len on success (decrypted payload minus ESP
+ * trailer and padding). On miss/bypass/ICV-fail, bd->len is left as-is.
+ */
+
+#ifndef KNOD_HELPERS_IPSEC_FUSED_GFX10_H_
+#define KNOD_HELPERS_IPSEC_FUSED_GFX10_H_
+
+#include <linux/types.h>
+#include "knod_amdgpu_insn.h"
+/* Provide AESGCM_MAX_DIM_Y so aesgcm_shader.h compiles (OFF_T0 macro).
+ * Only emit_aes_encrypt_block_gfx10 / emit_gfmul_128_gfx10 are used here;
+ * the full aesgcm_gen_shader_* functions are unreferenced.
+ */
+#ifndef AESGCM_MAX_DIM_Y
+#define AESGCM_MAX_DIM_Y 1024
+#endif
+#include "aesgcm_shader.h"
+
+/* SA entry constants - must match knod_ipsec.h */
+#define KNOD_IPSEC_SHADER_NR_SA 256
+#define KNOD_IPSEC_SHADER_SA_ENTRY_SZ 104
+
+/* SA entry field offsets (struct knod_ipsec_sa_entry) */
+#define SA_OFF_SPI 0
+#define SA_OFF_KEY_ADDR 16
+#define SA_OFF_HTABLE_ADDR 24
+#define SA_OFF_T_TABLES_ADDR 32
+#define SA_OFF_SALT 40
+#define SA_OFF_KEY_LEN 44
+#define SA_OFF_NR_ROUNDS 48
+#define SA_OFF_MODE 52 /* XFRM_MODE_TRANSPORT=0, TUNNEL=1 */
+#define SA_OFF_STATS_ADDR 88 /* per-SA GPU stats (u64 gpu addr) */
+
+/* ESP packet geometry (ETH=14, IPv4=20 / IPv6=40, no VLAN/opts).
+ * IPv4: ESP header starts at offset 34 (14+20).
+ * IPv6: ESP header starts at offset 54 (14+40).
+ * Within ESP header: SPI+0, seq+4, IV+8, ctext+16.
+ * The shader dynamically computes offsets based on IP version.
+ */
+#define ESP_HDR_OFF_V4 34 /* ETH(14) + IPv4(20) */
+#define ESP_HDR_OFF_V6 54 /* ETH(14) + IPv6(40) */
+#define ESP_REL_SPI 0
+#define ESP_REL_SEQ 4
+#define ESP_REL_IV 8
+#define ESP_REL_CTEXT 16 /* SPI(4)+seq(4)+IV(8) */
+#define ESP_ICV_LEN 16
+
+/* Fixed IPv4 layout offsets used by the crypto KAT */
+#define ESP_SPI_OFF 34
+#define ESP_SEQ_OFF 38
+#define ESP_IV_OFF 42
+#define ESP_CTEXT_OFF 50
+
+/* Fused sub[] offsets within kernarg (sub[i] = kernarg + 40 + i*32) */
+#define SUB_BASE_OFF 40
+#define SUB_STRIDE 32
+#define SUB_OFF_PKT_ADDR 0
+#define SUB_OFF_OUT_ADDR 8
+#define SUB_OFF_BD_ADDR 16
+#define SUB_OFF_PKT_LEN 24
+#define SUB_OFF_RESULT_SEQ 28
+
+/* ICV-fail sentinel (distinct from MISS=0xFFFFFFFF and BYPASS=0xFFFFFFFE) */
+#define VERDICT_ICV_FAIL 0xFFFFFFFDu
+
+/* High VGPRs for saving pre-crypto IPsec state (above AES v0-v22 range) */
+#define VR_SAVE_SLOT 30
+#define VR_SAVE_BD_LO 31
+#define VR_SAVE_BD_HI 32
+#define VR_SAVE_PKT_LO 33
+#define VR_SAVE_PKT_HI 34
+#define VR_SAVE_PKTLEN 35
+#define VR_SAVE_OUT_LO 36
+#define VR_SAVE_OUT_HI 37
+#define VR_SAVE_SEQ 38
+#define VR_SAVE_SPI 39
+#define VR_SAVE_STATS_LO 40 /* per-SA stats GPU addr low */
+#define VR_SAVE_STATS_HI 41 /* per-SA stats GPU addr high */
+/* ESP header offset: 34(v4) or 54(v6) */
+#define VR_SAVE_ESP_OFF 42
+/* Ciphertext prefetch destination - free v23-v27, outside AES v0-v22 range */
+#define VR_PREFETCH0 23
+#define VR_PREFETCH1 24
+#define VR_PREFETCH2 25
+#define VR_PREFETCH3 26
+/* extra dword for GFX10 unaligned fix */
+#define VR_PREFETCH4 27
+
+/* Extra SGPRs for IPsec-specific state that survives into AES phases.
+ * These must NOT collide with SR_* from aesgcm_shader.h (s18-s49, s56-s59).
+ * s50-s55 are IPsec-specific. s56-s59 = SR_RK2 (AES round key double-buffer).
+ */
+#define SR_CTEXT_LEN 50 /* ciphertext length in bytes */
+#define SR_NBLOCKS_GCM 51 /* ceil(ctext_len/16) */
+#define SR_HTABLE_LO 52 /* H-power table GPU addr */
+#define SR_HTABLE_HI 53
+#define SR_TOTAL_GHASH_BLK 54 /* nblocks + 2 (AAD + ctext + len) */
+#define SR_SA_MODE 55 /* XFRM_MODE_TRANSPORT=0, TUNNEL=1 */
+
+/* File-local emit helpers */
+#ifndef _KNOD_IPSEC_EMIT
+#define _KNOD_IPSEC_EMIT
+#define _E(fn, ...) (n += fn(__VA_ARGS__) / 4)
+#define _BR(fn, ...) ({ int _p = n; n += fn(__VA_ARGS__) / 4; _p; })
+#endif
+
+/*
+ * KNOD_IPSEC_GFX10_DIAG_STUB - graduated diagnostic stubs.
+ * Uncomment exactly one level to bisect the SQC inst-fault:
+ *
+ * Level 1: bare s_endpgm - tests KD, entry offset, BO mapping.
+ * Level 2: compute bd_addr from kernarg, store 0xDEAD0001, endpgm.
+ * Tests SGPR layout, VOP2/VOP1 ALU, GLOBAL load/store.
+ * Level 3: full Phase 0 (parse + SA scan) + write result, endpgm.
+ * Tests branches, patch_branch, VOP3B carry, SOPC.
+ *
+ * Leave all commented out for the real shader.
+ */
+/* #define KNOD_IPSEC_GFX10_DIAG_STUB 1 */
+/* #define KNOD_IPSEC_GFX10_DIAG_STUB 7 */
+/* #define KNOD_IPSEC_GFX10_DIAG_STUB 2 */
+/* #define KNOD_IPSEC_GFX10_DIAG_STUB 3 */
+/* #define KNOD_IPSEC_GFX10_DIAG_STUB 4 */
+/* #define KNOD_IPSEC_GFX10_DIAG_STUB 5 */
+
+static inline int kfd_ipsec_gen_fused_shader_gfx10(void *vbuf)
+{
+ int br_skip12, br_skip14, br_skip15, br_skip18, br_skip16, br_skip13;
+ int br_skip_aad, br_skip_ctext, br_skip_len;
+ int loop_top, br_match, br_loop, br_end;
+ int br_no_sdma, br_no_copy, br_not_last;
+ int br_ipv4, br_bypass, br_crypto_end;
+ int br_ipv6, br_v6_to_common;
+ int br_not_transport, li;
+ const int L3_TMP_BASE = 14; /* v14..v23 */
+ u32 *buf = (u32 *)vbuf;
+ int br_crypto_done;
+ int br_execz_ctr;
+ int br_execz2;
+ int br_icv_bad;
+ int br_icv_ok;
+ int br_tid0;
+ int br_skip;
+ int br_ok;
+ int level;
+ int n = 0;
+
+#if defined(KNOD_IPSEC_GFX10_DIAG_STUB) && KNOD_IPSEC_GFX10_DIAG_STUB == 1
+ /* LEVEL 1: bare s_endpgm - if this faults, KD or BO mapping is
+ * wrong
+ */
+ _E(emit_gfx10_s_endpgm, I10(buf, n));
+ while (n % 256)
+ _E(emit_gfx10_s_code_end, I10(buf, n));
+ pr_info("knod_ipsec: GFX10 DIAG STUB level 1 (bare endpgm), %d bytes\n",
+ n * 4);
+ return n * 4;
+#endif
+
+#if defined(KNOD_IPSEC_GFX10_DIAG_STUB) && KNOD_IPSEC_GFX10_DIAG_STUB == 2
+ /* LEVEL 2: load bd_addr from kernarg, write 0xDEAD0001 to bd+8.
+ * Tests: s8/s9 kernarg ptr, s16 workgroup_id_y, VOP1/VOP2 ALU,
+ * GLOBAL_LOAD_DWORDX2, GLOBAL_STORE_DWORD, s_waitcnt.
+ */
+ _E(emit_gfx10_s_waitcnt_vmcnt_lgkmcnt, I10(buf, n));
+ /* v1 = sub offset = 40 + wg_id_y * 32 */
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(1), P_S(16));
+ _E(emit_gfx10_v_lshlrev_b32, I10(buf, n), P_V(1), P_I(5), P_V(1));
+ _E(emit_gfx10_v_add_nc_u32, I10(buf, n), P_V(1), P_L(SUB_BASE_OFF),
+ P_V(1));
+ /* v[3:4] = &sub[wg_id_y] */
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(2), P_S(9));
+ _E(emit_gfx10_v_add_co_u32, I10(buf, n), P_V(3), P_S(8), P_V(1));
+ _E(emit_gfx10_v_add_co_ci_u32_e32, I10(buf, n), P_V(4), P_I(0), P_V(2));
+ /* v[5:6] = sub[].bd_addr */
+ _E(emit_gfx10_global_load_dwordx2, I10(buf, n), P_V(5), P_V(3),
+ SUB_OFF_BD_ADDR);
+ _E(emit_gfx10_s_waitcnt_vmcnt, I10(buf, n));
+ /* v0 = 0 for lane check: only lane 0 writes */
+ _E(emit_gfx10_v_cmp_eq_u32, I10(buf, n), P_I(0), P_V(0));
+ br_skip = _BR(emit_gfx10_s_cbranch_vccz, I10(buf, n), 0);
+ /* write 0xDEAD0001 to bd->act (offset +8) */
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(7),
+ P_L(0xDEAD0001u));
+ _E(emit_gfx10_global_store_dword, I10(buf, n), P_V(5), P_V(7), 8);
+ _E(emit_gfx10_s_waitcnt_vmcnt, I10(buf, n));
+ patch_branch(buf, br_skip, n);
+ _E(emit_gfx10_s_endpgm, I10(buf, n));
+ while (n % 256)
+ _E(emit_gfx10_s_code_end, I10(buf, n));
+ pr_info("knod_ipsec: GFX10 DIAG STUB level 2 (bd write), %d bytes\n",
+ n * 4);
+ return n * 4;
+#endif
+
+#if defined(KNOD_IPSEC_GFX10_DIAG_STUB) && KNOD_IPSEC_GFX10_DIAG_STUB == 3
+ /* LEVEL 3: s_dcache_inv (SMEM 8-byte) + Level 2 body.
+ * If this faults but Level 2 passed, SMEM encoding is the culprit.
+ */
+ _E(emit_gfx10_s_dcache_inv, I10(buf, n));
+ _E(emit_gfx10_s_waitcnt_lgkmcnt, I10(buf, n));
+ /* --- Level 2 body below --- */
+ _E(emit_gfx10_s_waitcnt_vmcnt_lgkmcnt, I10(buf, n));
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(1), P_S(16));
+ _E(emit_gfx10_v_lshlrev_b32, I10(buf, n), P_V(1), P_I(5), P_V(1));
+ _E(emit_gfx10_v_add_nc_u32, I10(buf, n), P_V(1), P_L(SUB_BASE_OFF),
+ P_V(1));
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(2), P_S(9));
+ _E(emit_gfx10_v_add_co_u32, I10(buf, n), P_V(3), P_S(8), P_V(1));
+ _E(emit_gfx10_v_add_co_ci_u32_e32, I10(buf, n), P_V(4), P_I(0), P_V(2));
+ _E(emit_gfx10_global_load_dwordx2, I10(buf, n), P_V(5), P_V(3),
+ SUB_OFF_BD_ADDR);
+ _E(emit_gfx10_s_waitcnt_vmcnt, I10(buf, n));
+ _E(emit_gfx10_v_cmp_eq_u32, I10(buf, n), P_I(0), P_V(0));
+ br_skip = _BR(emit_gfx10_s_cbranch_vccz, I10(buf, n), 0);
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(7),
+ P_L(0xDEAD0003u));
+ _E(emit_gfx10_global_store_dword, I10(buf, n), P_V(5), P_V(7), 8);
+ _E(emit_gfx10_s_waitcnt_vmcnt, I10(buf, n));
+ patch_branch(buf, br_skip, n);
+ _E(emit_gfx10_s_endpgm, I10(buf, n));
+ while (n % 256)
+ _E(emit_gfx10_s_code_end, I10(buf, n));
+ pr_info("knod_ipsec: GFX10 DIAG STUB level 3 (SMEM + bd write), %d bytes\n",
+ n * 4);
+ return n * 4;
+#endif
+
+#if defined(KNOD_IPSEC_GFX10_DIAG_STUB) && KNOD_IPSEC_GFX10_DIAG_STUB == 4
+ /* LEVEL 4: SOP1 + SOP2 + SOPC + SMEM + Level 2 body.
+ * Tests the three scalar encoding formats not covered by Levels 1-3.
+ * SOP1: s_mov_b32 (encoding 0x17D)
+ * SOP2: s_add_u32, s_lshr_b32 (encoding 0x2)
+ * SOPC: s_cmp_eq_u32 + s_cbranch_scc1 (encoding 0x17E)
+ */
+ _E(emit_gfx10_s_dcache_inv, I10(buf, n));
+ _E(emit_gfx10_s_waitcnt_lgkmcnt, I10(buf, n));
+
+ /* SOP1: s_mov_b32 s28, 0xCAFE0004 (literal) */
+ _E(emit_gfx10_s_mov_b32, I10(buf, n), P_S(28), P_L(0xCAFE0004u));
+ /* SOP2: s_add_u32 s28, s28, 1 (inline const) */
+ _E(emit_gfx10_s_add_u32, I10(buf, n), P_S(28), P_S(28), P_I(1));
+ /* SOP2: s_lshr_b32 s28, s28, 0 (nop shift) */
+ _E(emit_gfx10_s_lshr_b32, I10(buf, n), P_S(28), P_S(28), P_I(0));
+ /* SOPC: s_cmp_eq_u32 s28, 0xCAFE0005 - should set SCC=1 */
+ _E(emit_gfx10_s_cmp_eq_u32, I10(buf, n), P_S(28), P_L(0xCAFE0005u));
+ br_ok = _BR(emit_gfx10_s_cbranch_scc1, I10(buf, n), 0);
+ /* SCC=0 path: write 0xBAD00004 as error marker */
+ _E(emit_gfx10_s_mov_b32, I10(buf, n), P_S(28), P_L(0xBAD00004u));
+ _E(emit_gfx10_s_endpgm, I10(buf, n));
+ patch_branch(buf, br_ok, n);
+
+ /* --- Level 2 body: bd write --- */
+ _E(emit_gfx10_s_waitcnt_vmcnt_lgkmcnt, I10(buf, n));
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(1), P_S(16));
+ _E(emit_gfx10_v_lshlrev_b32, I10(buf, n), P_V(1), P_I(5), P_V(1));
+ _E(emit_gfx10_v_add_nc_u32, I10(buf, n), P_V(1), P_L(SUB_BASE_OFF),
+ P_V(1));
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(2), P_S(9));
+ _E(emit_gfx10_v_add_co_u32, I10(buf, n), P_V(3), P_S(8), P_V(1));
+ _E(emit_gfx10_v_add_co_ci_u32_e32, I10(buf, n), P_V(4), P_I(0), P_V(2));
+ _E(emit_gfx10_global_load_dwordx2, I10(buf, n), P_V(5), P_V(3),
+ SUB_OFF_BD_ADDR);
+ _E(emit_gfx10_s_waitcnt_vmcnt, I10(buf, n));
+ _E(emit_gfx10_v_cmp_eq_u32, I10(buf, n), P_I(0), P_V(0));
+ br_skip = _BR(emit_gfx10_s_cbranch_vccz, I10(buf, n), 0);
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(7),
+ P_L(0xDEAD0004u));
+ _E(emit_gfx10_global_store_dword, I10(buf, n), P_V(5), P_V(7), 8);
+ _E(emit_gfx10_s_waitcnt_vmcnt, I10(buf, n));
+ patch_branch(buf, br_skip, n);
+ _E(emit_gfx10_s_endpgm, I10(buf, n));
+ while (n % 256)
+ _E(emit_gfx10_s_code_end, I10(buf, n));
+ pr_info("knod_ipsec: GFX10 DIAG STUB level 4 (SOP1/SOP2/SOPC + bd), %d bytes\n",
+ n * 4);
+ return n * 4;
+#endif
+
+ /* ================================================================
+ * Phase 0: Parse ESP header + SA table lookup
+ *
+ * s_dcache_inv: flush K$ so s_load reads fresh round keys.
+ * ================================================================
+ */
+ _E(emit_gfx10_s_dcache_inv, I10(buf, n));
+ _E(emit_gfx10_s_waitcnt_lgkmcnt, I10(buf, n));
+ _E(emit_gfx10_s_mov_b32, I10(buf, n), P_S(SR_BSWAP), P_L(0x00010203));
+
+ /* v1 = 40 + wg_id_y*32 = offset of sub[wg_id_y] within kernarg */
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(1), P_S(16));
+ _E(emit_gfx10_v_lshlrev_b32, I10(buf, n), P_V(1), P_I(5), P_V(1));
+ _E(emit_gfx10_v_add_nc_u32, I10(buf, n), P_V(1), P_L(SUB_BASE_OFF),
+ P_V(1));
+
+ /* v[3:4] = kernarg_ptr + v1 = &sub[wg_id_y] */
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(2), P_S(9));
+ _E(emit_gfx10_v_add_co_u32, I10(buf, n), P_V(3), P_S(8), P_V(1));
+ _E(emit_gfx10_v_add_co_ci_u32_e32, I10(buf, n), P_V(4), P_I(0), P_V(2));
+
+ /* Load sub[].pkt_addr -> v[9:10], sub[].bd_addr -> v[5:6] */
+ _E(emit_gfx10_global_load_dwordx2, I10(buf, n), P_V(9), P_V(3),
+ SUB_OFF_PKT_ADDR);
+ _E(emit_gfx10_global_load_dwordx2, I10(buf, n), P_V(5), P_V(3),
+ SUB_OFF_BD_ADDR);
+ _E(emit_gfx10_s_waitcnt_vmcnt, I10(buf, n));
+
+ /* Seed v[11:12] with pkt_addr as a safe default BEFORE the IP
+ * version branch. The bypass path (non-v4/v6 packets like ARP)
+ * unconditionally branches past the v[11:12] setup at line ~195
+ * and later Phase 1 does a global_load at v[11:12]+ESP_REL_SEQ to
+ * read the ESP sequence number. Without this seed v[11:12] would
+ * hold uninitialised VGPR state (wave launch garbage), producing
+ * a fault at ~0x{random}_00000000. For valid v4/v6 packets the
+ * common path below overwrites v[11:12] with pkt+esp_hdr_off so
+ * this seed is harmless.
+ */
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(11), P_V(9));
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(12), P_V(10));
+
+ /* IP version gate: load dword at pkt+12 to get first byte of L3
+ * header (byte[14]). Extract version nibble -> s28.
+ */
+ _E(emit_gfx10_global_load_dword, I10(buf, n), P_V(15), P_V(9), 12);
+ _E(emit_gfx10_s_waitcnt_vmcnt, I10(buf, n));
+
+ _E(emit_gfx10_v_readfirstlane_b32, I10(buf, n), 28, 15);
+ _E(emit_gfx10_s_lshr_b32, I10(buf, n), P_S(28), P_S(28), P_I(20));
+ _E(emit_gfx10_s_and_b32_p, I10(buf, n), P_S(28), P_I(0xF), P_S(28));
+
+ /* Check IPv4 (version==4) */
+ _E(emit_gfx10_s_cmp_eq_u32, I10(buf, n), P_S(28), P_I(4));
+ br_ipv4 = _BR(emit_gfx10_s_cbranch_scc1, I10(buf, n), 0);
+
+ /* Check IPv6 (version==6) */
+ _E(emit_gfx10_s_cmp_eq_u32, I10(buf, n), P_S(28), P_I(6));
+ br_ipv6 = _BR(emit_gfx10_s_cbranch_scc1, I10(buf, n), 0);
+
+ /* Bypass: neither IPv4 nor IPv6 */
+ _E(emit_gfx10_s_mov_b32, I10(buf, n), P_S(26), P_L(0xFFFFFFFEu));
+ br_bypass = _BR(emit_gfx10_s_branch, I10(buf, n), 0);
+
+ /* IPv6 landing: esp_hdr_off = 54 */
+ patch_branch(buf, br_ipv6, n);
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(VR_SAVE_ESP_OFF),
+ P_L(ESP_HDR_OFF_V6));
+ br_v6_to_common = _BR(emit_gfx10_s_branch, I10(buf, n), 0);
+
+ /* IPv4 landing: esp_hdr_off = 34 */
+ patch_branch(buf, br_ipv4, n);
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(VR_SAVE_ESP_OFF),
+ P_L(ESP_HDR_OFF_V4));
+
+ /* Common path: both IPv4 and IPv6 converge here */
+ patch_branch(buf, br_v6_to_common, n);
+
+ /* v[11:12] = pkt_addr + esp_hdr_off (dynamic) */
+ _E(emit_gfx10_v_add_co_u32, I10(buf, n), P_V(11),
+ P_V(VR_SAVE_ESP_OFF), P_V(9));
+ _E(emit_gfx10_v_add_co_ci_u32_e32, I10(buf, n), P_V(12), P_I(0),
+ P_V(10));
+
+ /* v13 = *(u32*)(pkt + esp_hdr_off) - SPI in big-endian.
+ *
+ * GFX10 (RDNA2) quirk: global_load_dword silently clears EA's
+ * low 2 bits, so loading at v[11:12] = pkt + 34 (v4) or pkt + 54
+ * (v6) would actually read pkt + 32 / pkt + 52. Both ESP offsets
+ * are 2 mod 4, so the shift to reconstruct the target dword is
+ * always 16 bits. Use dwordx2 (HW still clears low 2 bits, but
+ * we get enough data) + v_alignbit_b32 to extract bytes
+ * [addr..addr+3] from the 8-byte window.
+ */
+ _E(emit_gfx10_global_load_dwordx2, I10(buf, n), P_V(20), P_V(11), 0);
+ _E(emit_gfx10_s_waitcnt_vmcnt, I10(buf, n));
+ /* v_alignbit_b32 D, HIGH, LOW, shift: D = ({HIGH, LOW} >> shift)[31:0].
+ * tmp_lo = v20 (memory-low dword), tmp_hi = v21 (memory-high dword).
+ */
+ _E(emit_gfx10_v_alignbit_b32, I10(buf, n), P_V(13), P_V(21), P_V(20),
+ P_I(16));
+
+ /* Byteswap SPI: v13 = bswap32(v13) via v_perm_b32 */
+ _E(emit_gfx10_v_perm_b32, I10(buf, n), P_V(13), P_V(13), P_V(13),
+ P_S(SR_BSWAP));
+
+ /* SA table linear scan (VMEM path for K$ coherence).
+ * s22 = target SPI, s[24:25] = sa_table_addr, s23 = counter,
+ * s26 = result (slot_idx or 0xFFFFFFFF), v[16:17] = running ptr.
+ */
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(16), P_S(8));
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(17), P_S(9));
+ _E(emit_gfx10_global_load_dwordx2, I10(buf, n), P_V(18), P_V(16), 0);
+ _E(emit_gfx10_s_waitcnt_vmcnt, I10(buf, n));
+ _E(emit_gfx10_v_readfirstlane_b32, I10(buf, n), 24, 18);
+ _E(emit_gfx10_v_readfirstlane_b32, I10(buf, n), 25, 19);
+
+ _E(emit_gfx10_v_readfirstlane_b32, I10(buf, n), 22, 13);
+ _E(emit_gfx10_s_mov_b32, I10(buf, n), P_S(23), P_I(0));
+ _E(emit_gfx10_s_mov_b32, I10(buf, n), P_S(26), P_L(0xFFFFFFFFu));
+
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(16), P_S(24));
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(17), P_S(25));
+
+ loop_top = n;
+ _E(emit_gfx10_global_load_dword, I10(buf, n), P_V(20), P_V(16),
+ SA_OFF_SPI);
+ _E(emit_gfx10_s_waitcnt_vmcnt, I10(buf, n));
+ _E(emit_gfx10_v_readfirstlane_b32, I10(buf, n), 27, 20);
+
+ _E(emit_gfx10_v_add_co_u32, I10(buf, n), P_V(16),
+ P_L(KNOD_IPSEC_SHADER_SA_ENTRY_SZ), P_V(16));
+ _E(emit_gfx10_v_add_co_ci_u32_e32, I10(buf, n), P_V(17), P_I(0),
+ P_V(17));
+ _E(emit_gfx10_s_add_u32, I10(buf, n), P_S(23), P_I(1), P_S(23));
+ _E(emit_gfx10_s_nop, I10(buf, n));
+
+ _E(emit_gfx10_s_cmp_eq_u32, I10(buf, n), P_S(27), P_S(22));
+ br_match = _BR(emit_gfx10_s_cbranch_scc1, I10(buf, n), 0);
+
+ _E(emit_gfx10_s_cmp_lt_u32, I10(buf, n), P_S(23),
+ P_L(KNOD_IPSEC_SHADER_NR_SA));
+ br_loop = _BR(emit_gfx10_s_cbranch_scc1, I10(buf, n), 0);
+ patch_branch(buf, br_loop, loop_top);
+
+ br_end = _BR(emit_gfx10_s_branch, I10(buf, n), 0);
+
+ /* Match: s26 = s23 - 1 */
+ patch_branch(buf, br_match, n);
+ _E(emit_gfx10_s_sub_u32_p, I10(buf, n), P_S(26), P_S(23), P_I(1));
+
+ patch_branch(buf, br_end, n);
+
+ patch_branch(buf, br_bypass, n);
+
+#if defined(KNOD_IPSEC_GFX10_DIAG_STUB) && KNOD_IPSEC_GFX10_DIAG_STUB == 5
+ /* LEVEL 5: early exit after real Phase 0.
+ * s26 = slot_idx (or 0xFFFFFFFF if no match).
+ * Write s26 to bd->act via v[5:6] (bd_addr loaded during Phase 0).
+ */
+ _E(emit_gfx10_v_cmp_eq_u32, I10(buf, n), P_I(0), P_V(0));
+ br_skip = _BR(emit_gfx10_s_cbranch_vccz, I10(buf, n), 0);
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(7),
+ P_L(0xDEAD0005u));
+ _E(emit_gfx10_global_store_dword, I10(buf, n), P_V(5), P_V(7), 8);
+ _E(emit_gfx10_s_waitcnt_vmcnt, I10(buf, n));
+ patch_branch(buf, br_skip, n);
+ _E(emit_gfx10_s_endpgm, I10(buf, n));
+ while (n % 256)
+ _E(emit_gfx10_s_code_end, I10(buf, n));
+ pr_info("knod_ipsec: GFX10 DIAG STUB level 5 (Phase 0 + exit), %d bytes\n",
+ n * 4);
+ return n * 4;
+#endif
+
+ /* ================================================================
+ * Phase 1: Save pre-crypto state + load extra sub[] fields
+ *
+ * Move IPsec-specific values to v30+ so v1-v22 and s18-s49 are
+ * free for AES-GCM helpers from aesgcm_shader.h.
+ * ================================================================
+ */
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(VR_SAVE_SLOT), P_S(26));
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(VR_SAVE_BD_LO), P_V(5));
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(VR_SAVE_BD_HI), P_V(6));
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(VR_SAVE_PKT_LO), P_V(9));
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(VR_SAVE_PKT_HI), P_V(10));
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(VR_SAVE_SPI), P_V(13));
+
+ /* Load sub[].out_addr -> v[VR_SAVE_OUT_LO:VR_SAVE_OUT_HI] */
+ _E(emit_gfx10_global_load_dwordx2, I10(buf, n), P_V(VR_SAVE_OUT_LO),
+ P_V(3), SUB_OFF_OUT_ADDR);
+ /* Load sub[].pkt_len -> v[VR_SAVE_PKTLEN] */
+ _E(emit_gfx10_global_load_dword, I10(buf, n), P_V(VR_SAVE_PKTLEN),
+ P_V(3), SUB_OFF_PKT_LEN);
+ _E(emit_gfx10_s_waitcnt_vmcnt, I10(buf, n));
+
+ /* Load ESP seq number: pkt + esp_hdr_off + 4, BE -> bswap ->
+ * v[VR_SAVE_SEQ].
+ * v[11:12] still holds pkt_addr + esp_hdr_off from Phase 0.
+ *
+ * GFX10 unaligned load fix: esp_hdr_off is 34(v4) or 54(v6),
+ * both == 2 mod 4. EA = pkt+38 clips to pkt+36. Load dwordx2
+ * from the clipped addr, then v_alignbit_b32 shift=16 to
+ * reconstruct the target dword. v[20:21] are free scratch.
+ */
+ _E(emit_gfx10_global_load_dwordx2, I10(buf, n), P_V(20), P_V(11),
+ ESP_REL_SEQ);
+ _E(emit_gfx10_s_waitcnt_vmcnt, I10(buf, n));
+ _E(emit_gfx10_v_alignbit_b32, I10(buf, n), P_V(VR_SAVE_SEQ), P_V(21),
+ P_V(20), P_I(16));
+
+ _E(emit_gfx10_v_perm_b32, I10(buf, n), P_V(VR_SAVE_SEQ),
+ P_V(VR_SAVE_SEQ), P_V(VR_SAVE_SEQ), P_S(SR_BSWAP));
+
+ /* Write bswapped seq back into sub[].result_seq for CPU finish worker.
+ * v[3:4] still points to &sub[wg_id_y].
+ */
+ _E(emit_gfx10_global_store_dword, I10(buf, n), P_V(3),
+ P_V(VR_SAVE_SEQ), SUB_OFF_RESULT_SEQ);
+
+ /* ================================================================
+ * Phase 2: Branch on miss/bypass - skip crypto entirely
+ * ================================================================
+ */
+ _E(emit_gfx10_v_readfirstlane_b32, I10(buf, n), 26, VR_SAVE_SLOT);
+ _E(emit_gfx10_s_cmp_ge_u32, I10(buf, n), P_S(26),
+ P_L(KNOD_IPSEC_SHADER_NR_SA));
+ br_crypto_end = _BR(emit_gfx10_s_cbranch_scc1, I10(buf, n), 0);
+
+ /* ================================================================
+ * Phase 3: Load SA entry fields for the matched slot
+ *
+ * entry_addr = sa_table_addr + slot_idx * SA_ENTRY_SIZE
+ * Load: key_gpu_addr, salt, nr_rounds, t_tables_gpu_addr,
+ * htable_gpu_addr
+ * ================================================================
+ */
+ /* s27 = slot_idx * SA_ENTRY_SIZE (scalar mul) */
+ _E(emit_gfx10_s_mul_i32, I10(buf, n), P_S(27), P_S(26),
+ P_L(KNOD_IPSEC_SHADER_SA_ENTRY_SZ));
+ /* s[24:25] = sa_table_addr (already there from Phase 0 scan) */
+ _E(emit_gfx10_s_add_u32, I10(buf, n), P_S(24), P_S(24), P_S(27));
+ _E(emit_gfx10_s_addc_u32, I10(buf, n), P_S(25), P_S(25), P_I(0));
+
+ /* Use VMEM for coherence: stage entry addr into VGPR pair */
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(VR_GA_LO), P_S(24));
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(VR_GA_HI), P_S(25));
+
+ /* Batched SA entry loads: issue all 4 loads to different
+ * VGPR destinations, then single waitcnt. v1-v8 (S0-S3,
+ * D0-D3) are free at Phase 3 - not used until Phase 7.
+ *
+ * Layout:
+ * dwordx4 @+16 -> v[1:4]: key_lo, key_hi, htable_lo, htable_hi
+ * dwordx4 @+32 -> v[5:8]: ttables_lo, ttables_hi, salt, key_len
+ * dwordx2 @+48 -> v[14:15]: nr_rounds, mode
+ * dwordx2 @+88 -> v[16:17]: stats_lo, stats_hi
+ */
+ _E(emit_gfx10_global_load_dwordx4, I10(buf, n), P_V(VR_S0),
+ P_V(VR_GA_LO), SA_OFF_KEY_ADDR);
+ _E(emit_gfx10_global_load_dwordx4, I10(buf, n), P_V(VR_D0),
+ P_V(VR_GA_LO), SA_OFF_T_TABLES_ADDR);
+ _E(emit_gfx10_global_load_dwordx2, I10(buf, n), P_V(VR_DATA0),
+ P_V(VR_GA_LO), SA_OFF_NR_ROUNDS);
+ _E(emit_gfx10_global_load_dwordx2, I10(buf, n), P_V(VR_DATA2),
+ P_V(VR_GA_LO), SA_OFF_STATS_ADDR);
+ _E(emit_gfx10_s_waitcnt_vmcnt, I10(buf, n));
+
+ /* key_addr: v1=lo, v2=hi */
+ _E(emit_gfx10_v_readfirstlane_b32, I10(buf, n), SR_KEYS, VR_S0);
+ _E(emit_gfx10_v_readfirstlane_b32, I10(buf, n), SR_KEYS + 1, VR_S1);
+ /* htable_addr: v3=lo, v4=hi */
+ _E(emit_gfx10_v_readfirstlane_b32, I10(buf, n), SR_HTABLE_LO, VR_S2);
+ _E(emit_gfx10_v_readfirstlane_b32, I10(buf, n), SR_HTABLE_HI, VR_S3);
+ /* t_tables_addr: v5=lo, v6=hi */
+ _E(emit_gfx10_v_readfirstlane_b32, I10(buf, n), SR_T_ADDR, VR_D0);
+ _E(emit_gfx10_v_readfirstlane_b32, I10(buf, n), SR_T_ADDR + 1, VR_D1);
+ /* salt: v7 */
+ _E(emit_gfx10_v_readfirstlane_b32, I10(buf, n), SR_IV0, VR_D2);
+ /* nr_rounds: v14, mode: v15 */
+ _E(emit_gfx10_v_readfirstlane_b32, I10(buf, n), SR_NR_ROUNDS, VR_DATA0);
+ _E(emit_gfx10_v_readfirstlane_b32, I10(buf, n), SR_SA_MODE, VR_DATA1);
+ /* stats_addr: v16=lo, v17=hi -> save VGPRs for Phase 10 */
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(VR_SAVE_STATS_LO),
+ P_V(VR_DATA2));
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(VR_SAVE_STATS_HI),
+ P_V(VR_DATA3));
+
+ /* ================================================================
+ * Phase 4: Build AES-GCM nonce
+ *
+ * nonce[12] = salt[4] || IV[8]
+ * salt is already in s[SR_IV0]. Load IV from pkt + esp_hdr_off + 8.
+ * Recompute ESP base from saved pkt_addr + VR_SAVE_ESP_OFF since
+ * v[11:12] were clobbered by Phase 3 SA loads (VR_GA_LO=12).
+ * IV goes to s[SR_IV1] (bytes 4-7) and s[SR_IV2] (bytes 8-11).
+ * ================================================================
+ */
+ _E(emit_gfx10_v_add_co_u32, I10(buf, n), P_V(VR_GA_LO),
+ P_V(VR_SAVE_ESP_OFF), P_V(VR_SAVE_PKT_LO));
+ _E(emit_gfx10_v_add_co_ci_u32_e32, I10(buf, n), P_V(VR_GA_HI),
+ P_I(0), P_V(VR_SAVE_PKT_HI));
+ /* GFX10 unaligned load fix: EA = pkt+42/62 == 2 mod 4, clips
+ * to pkt+40/60. Load dwordx4 (16B from clipped addr), then
+ * alignbit shift=16 to reconstruct IV[0:3] and IV[4:7].
+ */
+ _E(emit_gfx10_global_load_dwordx4, I10(buf, n), P_V(VR_DATA0),
+ P_V(VR_GA_LO), ESP_REL_IV);
+ _E(emit_gfx10_s_waitcnt_vmcnt, I10(buf, n));
+ _E(emit_gfx10_v_alignbit_b32, I10(buf, n), P_V(VR_DATA0),
+ P_V(VR_DATA1), P_V(VR_DATA0), P_I(16));
+ _E(emit_gfx10_v_alignbit_b32, I10(buf, n), P_V(VR_DATA1),
+ P_V(VR_DATA2), P_V(VR_DATA1), P_I(16));
+ _E(emit_gfx10_v_readfirstlane_b32, I10(buf, n), SR_IV1, VR_DATA0);
+ _E(emit_gfx10_v_readfirstlane_b32, I10(buf, n), SR_IV2, VR_DATA1);
+
+ /* ================================================================
+ * Phase 5: Compute ciphertext bounds
+ *
+ * ctext_off = esp_hdr_off + 16 (SPI+seq+IV)
+ * ctext_len = pkt_len - ctext_off - ICV_LEN
+ * nblocks = (ctext_len + 15) >> 4
+ *
+ * s28 is free here (last used in version gate) - use as scratch.
+ * ================================================================
+ */
+ _E(emit_gfx10_v_readfirstlane_b32, I10(buf, n), 28, VR_SAVE_ESP_OFF);
+ _E(emit_gfx10_s_add_u32, I10(buf, n), P_S(28),
+ P_I(ESP_REL_CTEXT + ESP_ICV_LEN),
+ P_S(28)); /* s28 = ctext_off + ICV_LEN = overhead to subtract */
+ _E(emit_gfx10_v_readfirstlane_b32, I10(buf, n), SR_CTEXT_LEN,
+ VR_SAVE_PKTLEN);
+ _E(emit_gfx10_s_sub_u32_p, I10(buf, n), P_S(SR_CTEXT_LEN),
+ P_S(SR_CTEXT_LEN), P_S(28));
+ /* s[SR_NBLOCKS_GCM] = (ctext_len + 15) >> 4 */
+ _E(emit_gfx10_s_add_u32, I10(buf, n), P_S(SR_NBLOCKS_GCM), P_I(15),
+ P_S(SR_CTEXT_LEN));
+ _E(emit_gfx10_s_lshr_b32, I10(buf, n), P_S(SR_NBLOCKS_GCM),
+ P_S(SR_NBLOCKS_GCM), P_I(4));
+ /* total GHASH blocks = 1(AAD) + nblocks(ctext) + 1(len) = nblocks + 2
+ */
+ _E(emit_gfx10_s_add_u32, I10(buf, n), P_S(SR_TOTAL_GHASH_BLK),
+ P_I(2), P_S(SR_NBLOCKS_GCM));
+
+ /* ================================================================
+ * Phase 6: Cooperative T-table load (VRAM -> LDS)
+ *
+ * All 256 threads load from SA's t_tables_gpu_addr. Each thread
+ * loads one u32 per table (4 tables x 256 entries = 4KB).
+ * ================================================================
+ */
+ _E(emit_gfx10_s_mov_b32, I10(buf, n), P_S(SR_MASK), P_L(0xFF));
+
+ /* v[VR_TMP] = tid * 4 (byte offset within each 1KB table) */
+ _E(emit_gfx10_v_lshlrev_b32, I10(buf, n), P_V(VR_TMP), P_I(2),
+ P_V(VR_TID));
+
+ /* T0: VRAM[t_tables + tid*4] -> LDS[tid*4] */
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(VR_GA_LO),
+ P_S(SR_T_ADDR));
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(VR_GA_HI),
+ P_S(SR_T_ADDR + 1));
+ _E(emit_gfx10_v_add_co_u32, I10(buf, n), P_V(VR_GA_LO), P_V(VR_GA_LO),
+ P_V(VR_TMP));
+ _E(emit_gfx10_v_add_co_ci_u32_e32, I10(buf, n), P_V(VR_GA_HI), P_I(0),
+ P_V(VR_GA_HI));
+ /* GFX10 GLOBAL offset is 12-bit signed (-2048..+2047).
+ * Offsets 2048 and 3072 overflow, so advance the base VGPR
+ * after the first two loads.
+ */
+ _E(emit_gfx10_global_load_dword, I10(buf, n), P_V(VR_DATA0),
+ P_V(VR_GA_LO), 0);
+ _E(emit_gfx10_global_load_dword, I10(buf, n), P_V(VR_DATA1),
+ P_V(VR_GA_LO), 1024);
+ _E(emit_gfx10_v_add_co_u32, I10(buf, n), P_V(VR_GA_LO), P_L(2048),
+ P_V(VR_GA_LO));
+ _E(emit_gfx10_v_add_co_ci_u32_e32, I10(buf, n), P_V(VR_GA_HI), P_I(0),
+ P_V(VR_GA_HI));
+ _E(emit_gfx10_global_load_dword, I10(buf, n), P_V(VR_DATA2),
+ P_V(VR_GA_LO), 0);
+ _E(emit_gfx10_global_load_dword, I10(buf, n), P_V(VR_DATA3),
+ P_V(VR_GA_LO), 1024);
+ _E(emit_gfx10_s_waitcnt_vmcnt, I10(buf, n));
+
+ /* Write to LDS: T0 at +0, T1 at +1024, T2 at +2048, T3 at +3072 */
+ _E(emit_gfx10_ds_write_b32, I10(buf, n), VR_TMP, VR_DATA0);
+ _E(emit_gfx10_v_add_nc_u32, I10(buf, n), P_V(VR_ADDR), P_L(1024),
+ P_V(VR_TMP));
+ _E(emit_gfx10_ds_write_b32, I10(buf, n), VR_ADDR, VR_DATA1);
+ _E(emit_gfx10_v_add_nc_u32, I10(buf, n), P_V(VR_ADDR), P_L(2048),
+ P_V(VR_TMP));
+ _E(emit_gfx10_ds_write_b32, I10(buf, n), VR_ADDR, VR_DATA2);
+ _E(emit_gfx10_v_add_nc_u32, I10(buf, n), P_V(VR_ADDR), P_L(3072),
+ P_V(VR_TMP));
+ _E(emit_gfx10_ds_write_b32, I10(buf, n), VR_ADDR, VR_DATA3);
+
+ _E(emit_gfx10_s_waitcnt_lgkmcnt, I10(buf, n));
+ _E(emit_gfx10_s_barrier, I10(buf, n));
+
+#if defined(KNOD_IPSEC_GFX10_DIAG_STUB) && KNOD_IPSEC_GFX10_DIAG_STUB == 6
+ /* LEVEL 6: early exit after Phase 6 (T-table -> LDS + barrier).
+ * Tests Phase 1-6: SA loads, nonce build, DS writes, s_barrier.
+ * bd_addr is in v[VR_SAVE_BD_LO:VR_SAVE_BD_HI] = v[31:32].
+ */
+ _E(emit_gfx10_v_cmp_eq_u32, I10(buf, n), P_I(0), P_V(0));
+ br_skip = _BR(emit_gfx10_s_cbranch_vccz, I10(buf, n), 0);
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(7),
+ P_L(0xDEAD0006u));
+ _E(emit_gfx10_global_store_dword, I10(buf, n), P_V(VR_SAVE_BD_LO),
+ P_V(7), 8);
+ _E(emit_gfx10_s_waitcnt_vmcnt, I10(buf, n));
+ patch_branch(buf, br_skip, n);
+ _E(emit_gfx10_s_endpgm, I10(buf, n));
+ while (n % 256)
+ _E(emit_gfx10_s_code_end, I10(buf, n));
+ pr_info("knod_ipsec: GFX10 DIAG STUB level 6 (Phase 0-6 + exit), %d bytes\n",
+ n * 4);
+ return n * 4;
+#endif
+
+ /* ================================================================
+ * Phase 7: AES-CTR decrypt
+ *
+ * Each thread handles block_id = tid. Only threads with tid <
+ * nblocks are active. Counter = nonce[12] || bswap32(tid+2).
+ * AES-encrypt the counter -> keystream. XOR with ciphertext ->
+ * plaintext. Store to out_addr + tid*16.
+ * ================================================================
+ */
+ /* VCC = (nblocks > tid) i.e. tid < nblocks - selects active CTR lanes
+ */
+ _E(emit_gfx10_v_cmp_gt_u32, I10(buf, n), P_S(SR_NBLOCKS_GCM),
+ P_V(VR_TID));
+ _E(emit_gfx10_s_and_saveexec_b64, I10(buf, n), SR_EXEC_SAVE,
+ 106 /* VCC_LO */);
+ br_execz_ctr = _BR(emit_gfx10_s_cbranch_execz, I10(buf, n), 0);
+
+ /* Save SR_KEYS for reload after this block encrypt */
+ _E(emit_gfx10_s_mov_b32, I10(buf, n), P_S(SR_T_ADDR), P_S(SR_KEYS));
+ _E(emit_gfx10_s_mov_b32, I10(buf, n), P_S(SR_T_ADDR + 1),
+ P_S(SR_KEYS + 1));
+
+ /* Prefetch ciphertext into v[23:27] before AES.
+ * GFX10 unaligned fix: ctext addr == 2 mod 4, so load
+ * dwordx4 (clips to aligned) + extra dword at +16.
+ * The ~200+ cycle AES encrypt hides the VMEM latency.
+ * v23-v27 are not touched by AES rounds (which use
+ * v1-v10 only). VR_GA/VR_BLK are also AES-safe.
+ */
+ _E(emit_gfx10_v_lshlrev_b32, I10(buf, n), P_V(VR_BLK), P_I(4),
+ P_V(VR_TID));
+ _E(emit_gfx10_v_add_co_u32, I10(buf, n), P_V(VR_GA_LO),
+ P_V(VR_SAVE_ESP_OFF), P_V(VR_SAVE_PKT_LO));
+ _E(emit_gfx10_v_add_co_ci_u32_e32, I10(buf, n), P_V(VR_GA_HI),
+ P_I(0), P_V(VR_SAVE_PKT_HI));
+ _E(emit_gfx10_v_add_co_u32, I10(buf, n), P_V(VR_GA_LO),
+ P_I(ESP_REL_CTEXT), P_V(VR_GA_LO));
+ _E(emit_gfx10_v_add_co_ci_u32_e32, I10(buf, n), P_V(VR_GA_HI),
+ P_I(0), P_V(VR_GA_HI));
+ _E(emit_gfx10_v_add_co_u32, I10(buf, n), P_V(VR_GA_LO),
+ P_V(VR_GA_LO), P_V(VR_BLK));
+ _E(emit_gfx10_v_add_co_ci_u32_e32, I10(buf, n), P_V(VR_GA_HI),
+ P_I(0), P_V(VR_GA_HI));
+ _E(emit_gfx10_global_load_dwordx4, I10(buf, n), P_V(VR_PREFETCH0),
+ P_V(VR_GA_LO), 0);
+ _E(emit_gfx10_global_load_dword, I10(buf, n), P_V(VR_PREFETCH4),
+ P_V(VR_GA_LO), 16);
+
+ /* Build AES counter block in v[VR_S0:VR_S3]:
+ * VR_S0 = nonce[0:3] = salt (SR_IV0)
+ * VR_S1 = nonce[4:7] = IV[0:3] (SR_IV1)
+ * VR_S2 = nonce[8:11] = IV[4:7] (SR_IV2)
+ * VR_S3 = bswap32(tid + 2)
+ */
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(VR_S0), P_S(SR_IV0));
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(VR_S1), P_S(SR_IV1));
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(VR_S2), P_S(SR_IV2));
+
+ /* v[VR_S3] = bswap32(tid + 2) */
+ _E(emit_gfx10_v_add_nc_u32, I10(buf, n), P_V(VR_S3), P_I(2),
+ P_V(VR_TID));
+ _E(emit_gfx10_v_perm_b32, I10(buf, n), P_V(VR_S3), P_V(VR_S3),
+ P_V(VR_S3), P_S(SR_BSWAP));
+
+ /* AES encrypt the counter block -> result in v[VR_S0:VR_S3] */
+#if defined(KNOD_IPSEC_GFX10_DIAG_STUB) && KNOD_IPSEC_GFX10_DIAG_STUB == 7
+ /* LEVEL 7: exit just before emit_aes_encrypt_block_gfx10.
+ * If this passes but Level 8 (after encrypt) faults,
+ * the AES block cipher GFX10 code is the culprit.
+ */
+ _E(emit_gfx10_v_cmp_eq_u32, I10(buf, n), P_I(0), P_V(0));
+ br_skip = _BR(emit_gfx10_s_cbranch_vccz, I10(buf, n), 0);
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(7),
+ P_L(0xDEAD0007u));
+ _E(emit_gfx10_global_store_dword, I10(buf, n),
+ P_V(VR_SAVE_BD_LO), P_V(7), 8);
+ _E(emit_gfx10_s_waitcnt_vmcnt, I10(buf, n));
+ patch_branch(buf, br_skip, n);
+ _E(emit_gfx10_s_endpgm, I10(buf, n));
+ while (n % 256)
+ _E(emit_gfx10_s_code_end, I10(buf, n));
+ pr_info("knod_ipsec: GFX10 DIAG STUB level 7 (before AES encrypt), %d bytes\n",
+ n * 4);
+ return n * 4;
+#endif
+ n = emit_aes_encrypt_block_gfx10(buf, n);
+
+#if defined(KNOD_IPSEC_GFX10_DIAG_STUB) && KNOD_IPSEC_GFX10_DIAG_STUB == 8
+ /* LEVEL 8: exit right after first emit_aes_encrypt_block_gfx10.
+ * If this faults, the illegal insn is inside the AES block cipher.
+ */
+ _E(emit_gfx10_v_cmp_eq_u32, I10(buf, n), P_I(0), P_V(0));
+ br_skip = _BR(emit_gfx10_s_cbranch_vccz, I10(buf, n), 0);
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(7),
+ P_L(0xDEAD0008u));
+ _E(emit_gfx10_global_store_dword, I10(buf, n),
+ P_V(VR_SAVE_BD_LO), P_V(7), 8);
+ _E(emit_gfx10_s_waitcnt_vmcnt, I10(buf, n));
+ patch_branch(buf, br_skip, n);
+ _E(emit_gfx10_s_endpgm, I10(buf, n));
+ while (n % 256)
+ _E(emit_gfx10_s_code_end, I10(buf, n));
+ pr_info("knod_ipsec: GFX10 DIAG STUB level 8 (after AES encrypt), %d bytes\n",
+ n * 4);
+ return n * 4;
+#endif
+
+ /* Ciphertext arrived during AES - drain vmcnt */
+ _E(emit_gfx10_s_waitcnt_vmcnt, I10(buf, n));
+
+ /* GFX10 unaligned fix: 4x alignbit on prefetched data */
+ _E(emit_gfx10_v_alignbit_b32, I10(buf, n), P_V(VR_PREFETCH0),
+ P_V(VR_PREFETCH1), P_V(VR_PREFETCH0), P_I(16));
+ _E(emit_gfx10_v_alignbit_b32, I10(buf, n), P_V(VR_PREFETCH1),
+ P_V(VR_PREFETCH2), P_V(VR_PREFETCH1), P_I(16));
+ _E(emit_gfx10_v_alignbit_b32, I10(buf, n), P_V(VR_PREFETCH2),
+ P_V(VR_PREFETCH3), P_V(VR_PREFETCH2), P_I(16));
+ _E(emit_gfx10_v_alignbit_b32, I10(buf, n), P_V(VR_PREFETCH3),
+ P_V(VR_PREFETCH4), P_V(VR_PREFETCH3), P_I(16));
+
+ /* XOR keystream with ciphertext -> plaintext */
+ _E(emit_gfx10_v_xor_b32_e32, I10(buf, n), P_V(VR_PREFETCH0),
+ P_V(VR_S0), P_V(VR_PREFETCH0));
+ _E(emit_gfx10_v_xor_b32_e32, I10(buf, n), P_V(VR_PREFETCH1),
+ P_V(VR_S1), P_V(VR_PREFETCH1));
+ _E(emit_gfx10_v_xor_b32_e32, I10(buf, n), P_V(VR_PREFETCH2),
+ P_V(VR_S2), P_V(VR_PREFETCH2));
+ _E(emit_gfx10_v_xor_b32_e32, I10(buf, n), P_V(VR_PREFETCH3),
+ P_V(VR_S3), P_V(VR_PREFETCH3));
+
+ /* Store plaintext to out_addr + tid*16 */
+ _E(emit_gfx10_v_add_co_u32, I10(buf, n), P_V(VR_GA_LO),
+ P_V(VR_BLK), P_V(VR_SAVE_OUT_LO));
+ _E(emit_gfx10_v_add_co_ci_u32_e32, I10(buf, n), P_V(VR_GA_HI),
+ P_I(0), P_V(VR_SAVE_OUT_HI));
+ _E(emit_gfx10_global_store_dwordx4, I10(buf, n), P_V(VR_GA_LO),
+ P_V(VR_PREFETCH0), 0);
+
+ patch_branch(buf, br_execz_ctr, n);
+
+ /* ================================================================
+ * Phase 7.5: Compute AES(K, J0) for ICV finalization
+ *
+ * J0 = nonce[12] || 0x00000001 (BE). Only thread 0 needs this
+ * but all active lanes can compute it; we just save the result.
+ * ================================================================
+ */
+ /* Restore SR_KEYS (consumed by encrypt_block) */
+ _E(emit_gfx10_s_mov_b32, I10(buf, n), P_S(SR_KEYS), P_S(SR_T_ADDR));
+ _E(emit_gfx10_s_mov_b32, I10(buf, n), P_S(SR_KEYS + 1),
+ P_S(SR_T_ADDR + 1));
+
+ /* Restore full EXEC for J0 encrypt (all 256 threads) */
+ _E(emit_gfx10_s_or_b64, I10(buf, n), 126 /* EXEC_LO */, SR_EXEC_SAVE,
+ SR_EXEC_SAVE);
+
+ /* J0 block: nonce || bswap32(1) = nonce || 0x01000000 */
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(VR_S0), P_S(SR_IV0));
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(VR_S1), P_S(SR_IV1));
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(VR_S2), P_S(SR_IV2));
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(VR_S3), P_L(0x01000000u));
+
+ n = emit_aes_encrypt_block_gfx10(buf, n);
+
+ /* Save AES(K, J0) -> v[VR_J0_0:VR_J0_3] */
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(VR_J0_0), P_V(VR_S0));
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(VR_J0_1), P_V(VR_S1));
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(VR_J0_2), P_V(VR_S2));
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(VR_J0_3), P_V(VR_S3));
+
+ _E(emit_gfx10_s_barrier, I10(buf, n));
+
+#if defined(KNOD_IPSEC_GFX10_DIAG_STUB) && KNOD_IPSEC_GFX10_DIAG_STUB == 9
+ /* LEVEL 9: exit after Phase 7.5 (J0 encrypt + barrier).
+ * Tests Phase 7 ctext XOR+store, Phase 7.5 second AES encrypt,
+ * s_or_b64 EXEC restore, s_barrier.
+ */
+ _E(emit_gfx10_v_cmp_eq_u32, I10(buf, n), P_I(0), P_V(0));
+ br_skip = _BR(emit_gfx10_s_cbranch_vccz, I10(buf, n), 0);
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(7),
+ P_L(0xDEAD0009u));
+ _E(emit_gfx10_global_store_dword, I10(buf, n),
+ P_V(VR_SAVE_BD_LO), P_V(7), 8);
+ _E(emit_gfx10_s_waitcnt_vmcnt, I10(buf, n));
+ patch_branch(buf, br_skip, n);
+ _E(emit_gfx10_s_endpgm, I10(buf, n));
+ while (n % 256)
+ _E(emit_gfx10_s_code_end, I10(buf, n));
+ pr_info("knod_ipsec: GFX10 DIAG STUB level 9 (Phase 7.5 + exit), %d bytes\n",
+ n * 4);
+ return n * 4;
+#endif
+
+ /* ================================================================
+ * Phase 8: Parallel GHASH
+ *
+ * GHASH input blocks (total_blocks = nblocks + 2):
+ * tid 0 -> AAD: SPI(4B,BE)||seq(4B,BE)||0s (16B)
+ * tid 1..nblocks -> ciphertext block (tid-1)
+ * tid nblocks+1 -> len: AAD_bitlen(64b)||ctext_bitlen(64b)
+ * tid > nblocks+1 -> zero (does not participate)
+ *
+ * Each thread loads its block -> v[VR_DATA0:VR_DATA3] (big-endian
+ * for GF multiply), loads H^(total-tid) -> v[VR_D0:VR_D3], runs
+ * GF multiply -> v[VR_S0:VR_S3], then tree-reduces via LDS XOR.
+ * ================================================================
+ */
+ /* Prefetch H^(total-tid) from H-power table before data selection.
+ * The ~60 ALU instructions in the data selection block below
+ * cover the VMEM latency. Result lands in VR_D0:D3, which
+ * data selection does not touch. VR_TMP/VR_GA are consumed
+ * here then free for reuse by the ctext section.
+ */
+ _E(emit_gfx10_v_sub_nc_u32, I10(buf, n), P_V(VR_TMP),
+ P_S(SR_TOTAL_GHASH_BLK), P_V(VR_TID));
+ _E(emit_gfx10_v_add_nc_u32, I10(buf, n), P_V(VR_TMP),
+ P_L(0xFFFFFFFF), P_V(VR_TMP));
+ _E(emit_gfx10_v_max_i32, I10(buf, n), P_V(VR_TMP), P_I(0), P_V(VR_TMP));
+ _E(emit_gfx10_v_lshlrev_b32, I10(buf, n), P_V(VR_TMP), P_I(4),
+ P_V(VR_TMP));
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(VR_GA_LO),
+ P_S(SR_HTABLE_LO));
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(VR_GA_HI),
+ P_S(SR_HTABLE_HI));
+ _E(emit_gfx10_v_add_co_u32, I10(buf, n), P_V(VR_GA_LO),
+ P_V(VR_GA_LO), P_V(VR_TMP));
+ _E(emit_gfx10_v_add_co_ci_u32_e32, I10(buf, n), P_V(VR_GA_HI),
+ P_I(0), P_V(VR_GA_HI));
+ _E(emit_gfx10_global_load_dwordx4, I10(buf, n), P_V(VR_D0),
+ P_V(VR_GA_LO), 0);
+
+ /* EXEC-based per-lane data selection. Default = zero, then
+ * each case narrows EXEC to matching lanes and writes data.
+ * This avoids scalar VCC branching (s_cbranch_vccnz) which
+ * makes the entire wave take one path, not individual lanes.
+ */
+ /* Default: all threads get zero (non-participating) */
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(VR_DATA0), P_I(0));
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(VR_DATA1), P_I(0));
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(VR_DATA2), P_I(0));
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(VR_DATA3), P_I(0));
+
+ /* ---- AAD: tid == 0 ---- */
+ _E(emit_gfx10_v_cmp_eq_u32, I10(buf, n), P_I(0), P_V(VR_TID));
+ _E(emit_gfx10_s_and_saveexec_b64, I10(buf, n), SR_GHASH_EXEC,
+ 106 /* VCC */);
+ br_skip_aad = _BR(emit_gfx10_s_cbranch_execz, I10(buf, n), 0);
+
+ /* VR_SAVE_SPI/SEQ are already in BE register convention:
+ * raw LE load from packet (BE wire bytes) + bswap = byte[0]
+ * in bits[31:24]. No second bswap needed - use directly.
+ */
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(VR_DATA0),
+ P_V(VR_SAVE_SPI));
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(VR_DATA1),
+ P_V(VR_SAVE_SEQ));
+ /* DATA2, DATA3 already 0 */
+
+ patch_branch(buf, br_skip_aad, n);
+ _E(emit_gfx10_s_mov_b64, I10(buf, n), 126 /* EXEC */,
+ SR_GHASH_EXEC);
+
+#if defined(KNOD_IPSEC_GFX10_DIAG_STUB) && KNOD_IPSEC_GFX10_DIAG_STUB == 12
+ _E(emit_gfx10_v_cmp_eq_u32, I10(buf, n), P_I(0), P_V(0));
+ br_skip12 = _BR(emit_gfx10_s_cbranch_vccz, I10(buf, n), 0);
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(7), P_L(0xDEAD000Cu));
+ _E(emit_gfx10_global_store_dword, I10(buf, n), P_V(VR_SAVE_BD_LO),
+ P_V(7), 8);
+ _E(emit_gfx10_s_waitcnt_vmcnt, I10(buf, n));
+ patch_branch(buf, br_skip12, n);
+ _E(emit_gfx10_s_endpgm, I10(buf, n));
+ while (n % 256)
+ _E(emit_gfx10_s_code_end, I10(buf, n));
+ return n * 4;
+#endif
+ /* ---- Ctext: 1 <= tid <= nblocks ---- */
+ /* block_idx = tid - 1 (unsigned; tid==0 -> 0xFFFFFFFF > nblocks) */
+ _E(emit_gfx10_v_add_nc_u32, I10(buf, n), P_V(VR_TMP), P_L(0xFFFFFFFF),
+ P_V(VR_TID));
+ _E(emit_gfx10_v_cmp_gt_u32, I10(buf, n), P_S(SR_NBLOCKS_GCM),
+ P_V(VR_TMP));
+ _E(emit_gfx10_s_and_saveexec_b64, I10(buf, n), SR_GHASH_EXEC,
+ 106 /* VCC */);
+ br_skip_ctext = _BR(emit_gfx10_s_cbranch_execz, I10(buf, n), 0);
+
+#if defined(KNOD_IPSEC_GFX10_DIAG_STUB) && KNOD_IPSEC_GFX10_DIAG_STUB == 14
+ _E(emit_gfx10_s_mov_b64, I10(buf, n), 126 /* EXEC */,
+ SR_GHASH_EXEC);
+ _E(emit_gfx10_v_cmp_eq_u32, I10(buf, n), P_I(0), P_V(0));
+ br_skip14 = _BR(emit_gfx10_s_cbranch_vccz, I10(buf, n), 0);
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(7), P_L(0xDEAD000Eu));
+ _E(emit_gfx10_global_store_dword, I10(buf, n), P_V(VR_SAVE_BD_LO),
+ P_V(7), 8);
+ _E(emit_gfx10_s_waitcnt_vmcnt, I10(buf, n));
+ patch_branch(buf, br_skip14, n);
+ _E(emit_gfx10_s_endpgm, I10(buf, n));
+ while (n % 256)
+ _E(emit_gfx10_s_code_end, I10(buf, n));
+ return n * 4;
+#endif
+ /* Load ctext block: pkt + esp_hdr_off + 16 + block_idx*16 */
+ _E(emit_gfx10_v_lshlrev_b32, I10(buf, n), P_V(VR_TMP), P_I(4),
+ P_V(VR_TMP));
+ _E(emit_gfx10_v_add_co_u32, I10(buf, n), P_V(VR_GA_LO),
+ P_V(VR_SAVE_ESP_OFF), P_V(VR_SAVE_PKT_LO));
+ _E(emit_gfx10_v_add_co_ci_u32_e32, I10(buf, n), P_V(VR_GA_HI),
+ P_I(0), P_V(VR_SAVE_PKT_HI));
+ _E(emit_gfx10_v_add_co_u32, I10(buf, n), P_V(VR_GA_LO),
+ P_I(ESP_REL_CTEXT), P_V(VR_GA_LO));
+ _E(emit_gfx10_v_add_co_ci_u32_e32, I10(buf, n), P_V(VR_GA_HI),
+ P_I(0), P_V(VR_GA_HI));
+ _E(emit_gfx10_v_add_co_u32, I10(buf, n), P_V(VR_GA_LO),
+ P_V(VR_GA_LO), P_V(VR_TMP));
+ _E(emit_gfx10_v_add_co_ci_u32_e32, I10(buf, n), P_V(VR_GA_HI),
+ P_I(0), P_V(VR_GA_HI));
+ /* GFX10 unaligned load fix: ctext addr == 2 mod 4.
+ * dwordx4 + extra dword + 4x alignbit. VR_BLK is free.
+ */
+ _E(emit_gfx10_global_load_dwordx4, I10(buf, n), P_V(VR_DATA0),
+ P_V(VR_GA_LO), 0);
+ _E(emit_gfx10_global_load_dword, I10(buf, n), P_V(VR_BLK),
+ P_V(VR_GA_LO), 16);
+ _E(emit_gfx10_s_waitcnt_vmcnt, I10(buf, n));
+ _E(emit_gfx10_v_alignbit_b32, I10(buf, n), P_V(VR_DATA0),
+ P_V(VR_DATA1), P_V(VR_DATA0), P_I(16));
+ _E(emit_gfx10_v_alignbit_b32, I10(buf, n), P_V(VR_DATA1),
+ P_V(VR_DATA2), P_V(VR_DATA1), P_I(16));
+ _E(emit_gfx10_v_alignbit_b32, I10(buf, n), P_V(VR_DATA2),
+ P_V(VR_DATA3), P_V(VR_DATA2), P_I(16));
+ _E(emit_gfx10_v_alignbit_b32, I10(buf, n), P_V(VR_DATA3),
+ P_V(VR_BLK), P_V(VR_DATA3), P_I(16));
+
+#if defined(KNOD_IPSEC_GFX10_DIAG_STUB) && KNOD_IPSEC_GFX10_DIAG_STUB == 15
+ _E(emit_gfx10_s_mov_b64, I10(buf, n), 126 /* EXEC */,
+ SR_GHASH_EXEC);
+ _E(emit_gfx10_v_cmp_eq_u32, I10(buf, n), P_I(0), P_V(0));
+ br_skip15 = _BR(emit_gfx10_s_cbranch_vccz, I10(buf, n), 0);
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(7), P_L(0xDEAD000Fu));
+ _E(emit_gfx10_global_store_dword, I10(buf, n), P_V(VR_SAVE_BD_LO),
+ P_V(7), 8);
+ _E(emit_gfx10_s_waitcnt_vmcnt, I10(buf, n));
+ patch_branch(buf, br_skip15, n);
+ _E(emit_gfx10_s_endpgm, I10(buf, n));
+ while (n % 256)
+ _E(emit_gfx10_s_code_end, I10(buf, n));
+ return n * 4;
+#endif
+ /* Zero trailing dwords in the last partial ctext block.
+ * The load above reads 16 raw bytes, but for the last block
+ * only (ctext_len % 16) bytes are ciphertext - the rest are
+ * ICV bytes which must NOT enter GHASH. ESP ctext is always
+ * 4-byte aligned so the partial count is 4, 8 or 12 - pure
+ * dword-level zeroing suffices, no byte masking needed.
+ *
+ * VR_TMP still holds block_idx * 16 from the address calc.
+ * remaining = ctext_len - block_idx*16. For full blocks
+ * (remaining >= 16) every v_cmp evaluates true -> no change.
+ */
+ _E(emit_gfx10_v_sub_nc_u32, I10(buf, n), P_V(VR_TMP),
+ P_S(SR_CTEXT_LEN), P_V(VR_TMP));
+ /* VR_TMP = remaining bytes in this block */
+
+ /* DATA3 (bytes 12-15): keep only if remaining > 12 */
+ _E(emit_gfx10_v_cmp_lt_u32, I10(buf, n), P_I(12), P_V(VR_TMP));
+#if defined(KNOD_IPSEC_GFX10_DIAG_STUB) && KNOD_IPSEC_GFX10_DIAG_STUB == 18
+ _E(emit_gfx10_s_mov_b64, I10(buf, n), 126 /* EXEC */,
+ SR_GHASH_EXEC);
+ _E(emit_gfx10_v_cmp_eq_u32, I10(buf, n), P_I(0), P_V(0));
+ br_skip18 = _BR(emit_gfx10_s_cbranch_vccz, I10(buf, n), 0);
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(7), P_L(0xDEAD0012u));
+ _E(emit_gfx10_global_store_dword, I10(buf, n), P_V(VR_SAVE_BD_LO),
+ P_V(7), 8);
+ _E(emit_gfx10_s_waitcnt_vmcnt, I10(buf, n));
+ patch_branch(buf, br_skip18, n);
+ _E(emit_gfx10_s_endpgm, I10(buf, n));
+ while (n % 256)
+ _E(emit_gfx10_s_code_end, I10(buf, n));
+ return n * 4;
+#endif
+ _E(emit_gfx10_v_cndmask_b32_e32, I10(buf, n),
+ P_V(VR_DATA3), P_I(0), P_V(VR_DATA3));
+
+ /* DATA2 (bytes 8-11): keep only if remaining > 8 */
+ _E(emit_gfx10_v_cmp_lt_u32, I10(buf, n), P_I(8), P_V(VR_TMP));
+ _E(emit_gfx10_v_cndmask_b32_e32, I10(buf, n),
+ P_V(VR_DATA2), P_I(0), P_V(VR_DATA2));
+
+ /* DATA1 (bytes 4-7): keep only if remaining > 4 */
+ _E(emit_gfx10_v_cmp_lt_u32, I10(buf, n), P_I(4), P_V(VR_TMP));
+ _E(emit_gfx10_v_cndmask_b32_e32, I10(buf, n),
+ P_V(VR_DATA1), P_I(0), P_V(VR_DATA1));
+
+ /* DATA0 (bytes 0-3): always valid (ESP 4-byte alignment) */
+
+#if defined(KNOD_IPSEC_GFX10_DIAG_STUB) && KNOD_IPSEC_GFX10_DIAG_STUB == 16
+ _E(emit_gfx10_s_mov_b64, I10(buf, n), 126 /* EXEC */,
+ SR_GHASH_EXEC);
+ _E(emit_gfx10_v_cmp_eq_u32, I10(buf, n), P_I(0), P_V(0));
+ br_skip16 = _BR(emit_gfx10_s_cbranch_vccz, I10(buf, n), 0);
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(7), P_L(0xDEAD0010u));
+ _E(emit_gfx10_global_store_dword, I10(buf, n), P_V(VR_SAVE_BD_LO),
+ P_V(7), 8);
+ _E(emit_gfx10_s_waitcnt_vmcnt, I10(buf, n));
+ patch_branch(buf, br_skip16, n);
+ _E(emit_gfx10_s_endpgm, I10(buf, n));
+ while (n % 256)
+ _E(emit_gfx10_s_code_end, I10(buf, n));
+ return n * 4;
+#endif
+ /* bswap each dword for GHASH (big-endian GF arithmetic) */
+ _E(emit_gfx10_v_perm_b32, I10(buf, n), P_V(VR_DATA0),
+ P_V(VR_DATA0), P_V(VR_DATA0), P_S(SR_BSWAP));
+ _E(emit_gfx10_v_perm_b32, I10(buf, n), P_V(VR_DATA1),
+ P_V(VR_DATA1), P_V(VR_DATA1), P_S(SR_BSWAP));
+ _E(emit_gfx10_v_perm_b32, I10(buf, n), P_V(VR_DATA2),
+ P_V(VR_DATA2), P_V(VR_DATA2), P_S(SR_BSWAP));
+ _E(emit_gfx10_v_perm_b32, I10(buf, n), P_V(VR_DATA3),
+ P_V(VR_DATA3), P_V(VR_DATA3), P_S(SR_BSWAP));
+
+ patch_branch(buf, br_skip_ctext, n);
+ _E(emit_gfx10_s_mov_b64, I10(buf, n), 126 /* EXEC */,
+ SR_GHASH_EXEC);
+
+#if defined(KNOD_IPSEC_GFX10_DIAG_STUB) && KNOD_IPSEC_GFX10_DIAG_STUB == 13
+ _E(emit_gfx10_v_cmp_eq_u32, I10(buf, n), P_I(0), P_V(0));
+ br_skip13 = _BR(emit_gfx10_s_cbranch_vccz, I10(buf, n), 0);
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(7), P_L(0xDEAD000Du));
+ _E(emit_gfx10_global_store_dword, I10(buf, n), P_V(VR_SAVE_BD_LO),
+ P_V(7), 8);
+ _E(emit_gfx10_s_waitcnt_vmcnt, I10(buf, n));
+ patch_branch(buf, br_skip13, n);
+ _E(emit_gfx10_s_endpgm, I10(buf, n));
+ while (n % 256)
+ _E(emit_gfx10_s_code_end, I10(buf, n));
+ return n * 4;
+#endif
+ /* ---- Len block: tid == nblocks + 1 ---- */
+ /* Compute nblocks+1 in s42 (scratch) */
+ _E(emit_gfx10_s_add_u32, I10(buf, n), P_S(42), P_I(1),
+ P_S(SR_NBLOCKS_GCM));
+ _E(emit_gfx10_v_cmp_eq_u32, I10(buf, n), P_S(42),
+ P_V(VR_TID));
+ _E(emit_gfx10_s_and_saveexec_b64, I10(buf, n), SR_GHASH_EXEC,
+ 106 /* VCC */);
+ br_skip_len = _BR(emit_gfx10_s_cbranch_execz, I10(buf, n), 0);
+
+ /* Length block format (GCM big-endian):
+ * DATA0 = AAD_bits[63:32] = 0
+ * DATA1 = AAD_bits[31:0] = 64 (8 bytes AAD x 8)
+ * DATA2 = ctext_bits[63:32] = 0
+ * DATA3 = ctext_bits[31:0] = ctext_len * 8
+ *
+ * No bswap32: length values are computed integers already
+ * in the correct big-endian register representation.
+ * bswap32 is only needed for data loaded from LE memory.
+ */
+ /* DATA0, DATA2 already 0 from default init */
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(VR_DATA1),
+ P_L(0x00000040u));
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(VR_DATA3),
+ P_S(SR_CTEXT_LEN));
+ _E(emit_gfx10_v_lshlrev_b32, I10(buf, n), P_V(VR_DATA3), P_I(3),
+ P_V(VR_DATA3));
+
+ patch_branch(buf, br_skip_len, n);
+ _E(emit_gfx10_s_mov_b64, I10(buf, n), 126 /* EXEC */,
+ SR_GHASH_EXEC);
+
+#if defined(KNOD_IPSEC_GFX10_DIAG_STUB) && KNOD_IPSEC_GFX10_DIAG_STUB == 11
+ _E(emit_gfx10_v_cmp_eq_u32, I10(buf, n), P_I(0), P_V(0));
+ br_skip = _BR(emit_gfx10_s_cbranch_vccz, I10(buf, n), 0);
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(7), P_L(0xDEAD000Bu));
+ _E(emit_gfx10_global_store_dword, I10(buf, n), P_V(VR_SAVE_BD_LO),
+ P_V(7), 8);
+ _E(emit_gfx10_s_waitcnt_vmcnt, I10(buf, n));
+ patch_branch(buf, br_skip, n);
+ _E(emit_gfx10_s_endpgm, I10(buf, n));
+ while (n % 256)
+ _E(emit_gfx10_s_code_end, I10(buf, n));
+ return n * 4;
+#endif
+ /* H-table data was prefetched before data selection; drain + bswap */
+ _E(emit_gfx10_s_waitcnt_vmcnt, I10(buf, n));
+ _E(emit_gfx10_v_perm_b32, I10(buf, n), P_V(VR_D0),
+ P_V(VR_D0), P_V(VR_D0), P_S(SR_BSWAP));
+ _E(emit_gfx10_v_perm_b32, I10(buf, n), P_V(VR_D1),
+ P_V(VR_D1), P_V(VR_D1), P_S(SR_BSWAP));
+ _E(emit_gfx10_v_perm_b32, I10(buf, n), P_V(VR_D2),
+ P_V(VR_D2), P_V(VR_D2), P_S(SR_BSWAP));
+ _E(emit_gfx10_v_perm_b32, I10(buf, n), P_V(VR_D3),
+ P_V(VR_D3), P_V(VR_D3), P_S(SR_BSWAP));
+
+ /* ---- GF(2^128) multiply: Z = DATA * H^k ---- */
+#if defined(KNOD_IPSEC_GFX10_DIAG_STUB) && KNOD_IPSEC_GFX10_DIAG_STUB == 10
+ _E(emit_gfx10_v_cmp_eq_u32, I10(buf, n), P_I(0), P_V(0));
+ br_skip = _BR(emit_gfx10_s_cbranch_vccz, I10(buf, n), 0);
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(7), P_L(0xDEAD000Au));
+ _E(emit_gfx10_global_store_dword, I10(buf, n), P_V(VR_SAVE_BD_LO),
+ P_V(7), 8);
+ _E(emit_gfx10_s_waitcnt_vmcnt, I10(buf, n));
+ patch_branch(buf, br_skip, n);
+ _E(emit_gfx10_s_endpgm, I10(buf, n));
+ while (n % 256)
+ _E(emit_gfx10_s_code_end, I10(buf, n));
+ return n * 4;
+#endif
+ n = emit_gfmul_128_gfx10(buf, n);
+ /* Result in v[VR_S0:VR_S3] */
+
+ /* ---- Tree reduction via LDS XOR (8 levels for 256 threads) ---- */
+ /* Write v[VR_S0:VR_S3] to LDS at tid * 16 */
+ _E(emit_gfx10_v_lshlrev_b32, I10(buf, n), P_V(VR_ADDR), P_I(4),
+ P_V(VR_TID));
+ _E(emit_gfx10_ds_write_b128, I10(buf, n), VR_ADDR, VR_S0);
+ _E(emit_gfx10_s_waitcnt_lgkmcnt, I10(buf, n));
+ _E(emit_gfx10_s_barrier, I10(buf, n));
+
+ for (level = 1; level <= 128; level <<= 1) {
+ /* if (tid & level) skip */
+ _E(emit_gfx10_v_and_b32_e32, I10(buf, n), P_V(VR_TMP),
+ P_L(level), P_V(VR_TID));
+ _E(emit_gfx10_v_cmp_eq_u32, I10(buf, n), P_I(0), P_V(VR_TMP));
+ _E(emit_gfx10_s_and_saveexec_b64, I10(buf, n), SR_GHASH_EXEC,
+ 106 /* VCC */);
+ br_skip = _BR(emit_gfx10_s_cbranch_execz, I10(buf, n), 0);
+
+ /* Compute both addresses up front */
+ _E(emit_gfx10_v_add_nc_u32, I10(buf, n), P_V(VR_TMP),
+ P_L(level), P_V(VR_TID));
+ _E(emit_gfx10_v_lshlrev_b32, I10(buf, n), P_V(VR_TMP),
+ P_I(4), P_V(VR_TMP));
+ _E(emit_gfx10_v_lshlrev_b32, I10(buf, n), P_V(VR_ADDR),
+ P_I(4), P_V(VR_TID));
+
+ /* Issue both reads, single wait */
+ _E(emit_gfx10_ds_read_b128, I10(buf, n), VR_D0, VR_TMP);
+ _E(emit_gfx10_ds_read_b128, I10(buf, n), VR_S0, VR_ADDR);
+ _E(emit_gfx10_s_waitcnt_lgkmcnt, I10(buf, n));
+
+ /* XOR */
+ _E(emit_gfx10_v_xor_b32_e32, I10(buf, n), P_V(VR_S0),
+ P_V(VR_S0), P_V(VR_D0));
+ _E(emit_gfx10_v_xor_b32_e32, I10(buf, n), P_V(VR_S1),
+ P_V(VR_S1), P_V(VR_D1));
+ _E(emit_gfx10_v_xor_b32_e32, I10(buf, n), P_V(VR_S2),
+ P_V(VR_S2), P_V(VR_D2));
+ _E(emit_gfx10_v_xor_b32_e32, I10(buf, n), P_V(VR_S3),
+ P_V(VR_S3), P_V(VR_D3));
+
+ /* Write back */
+ _E(emit_gfx10_ds_write_b128, I10(buf, n), VR_ADDR, VR_S0);
+ _E(emit_gfx10_s_waitcnt_lgkmcnt, I10(buf, n));
+
+ patch_branch(buf, br_skip, n);
+ /* Restore EXEC */
+ _E(emit_gfx10_s_mov_b64, I10(buf, n), 126 /* EXEC */,
+ SR_GHASH_EXEC);
+ _E(emit_gfx10_s_barrier, I10(buf, n));
+ }
+
+ /* Thread 0 now has the final GHASH in LDS[0..15]. Read it. */
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(VR_TMP), P_I(0));
+ _E(emit_gfx10_ds_read_b128, I10(buf, n), VR_S0, VR_TMP);
+ _E(emit_gfx10_s_waitcnt_lgkmcnt, I10(buf, n));
+
+ /* ================================================================
+ * Phase 9: ICV verify (thread 0 only)
+ *
+ * computed_tag = bswap(GHASH) XOR AES(K, J0)
+ * received_tag = last 16 bytes of ESP packet
+ * ================================================================
+ */
+ _E(emit_gfx10_v_cmp_eq_u32, I10(buf, n), P_I(0), P_V(VR_TID));
+ _E(emit_gfx10_s_and_saveexec_b64, I10(buf, n), SR_EXEC_SAVE, 106);
+ br_tid0 = _BR(emit_gfx10_s_cbranch_execz, I10(buf, n), 0);
+
+ /* bswap GHASH from big-endian to little-endian */
+ _E(emit_gfx10_v_perm_b32, I10(buf, n), P_V(VR_S0), P_V(VR_S0),
+ P_V(VR_S0), P_S(SR_BSWAP));
+ _E(emit_gfx10_v_perm_b32, I10(buf, n), P_V(VR_S1), P_V(VR_S1),
+ P_V(VR_S1), P_S(SR_BSWAP));
+ _E(emit_gfx10_v_perm_b32, I10(buf, n), P_V(VR_S2), P_V(VR_S2),
+ P_V(VR_S2), P_S(SR_BSWAP));
+ _E(emit_gfx10_v_perm_b32, I10(buf, n), P_V(VR_S3), P_V(VR_S3),
+ P_V(VR_S3), P_S(SR_BSWAP));
+
+ /* computed_tag = GHASH XOR AES(K, J0) */
+ _E(emit_gfx10_v_xor_b32_e32, I10(buf, n), P_V(VR_S0),
+ P_V(VR_S0), P_V(VR_J0_0));
+ _E(emit_gfx10_v_xor_b32_e32, I10(buf, n), P_V(VR_S1),
+ P_V(VR_S1), P_V(VR_J0_1));
+ _E(emit_gfx10_v_xor_b32_e32, I10(buf, n), P_V(VR_S2),
+ P_V(VR_S2), P_V(VR_J0_2));
+ _E(emit_gfx10_v_xor_b32_e32, I10(buf, n), P_V(VR_S3),
+ P_V(VR_S3), P_V(VR_J0_3));
+
+ /* Load received ICV: pkt + pkt_len - 16.
+ * GFX10 unaligned load fix: ICV addr == 2 mod 4.
+ * dwordx4 + extra dword + 4x alignbit. VR_TMP is
+ * free after address calc; use VR_BLK for 5th dword.
+ */
+ _E(emit_gfx10_v_add_nc_u32, I10(buf, n), P_V(VR_TMP),
+ P_L(0xFFFFFFF0u), P_V(VR_SAVE_PKTLEN)); /* pkt_len - 16 */
+ _E(emit_gfx10_v_add_co_u32, I10(buf, n), P_V(VR_GA_LO),
+ P_V(VR_TMP), P_V(VR_SAVE_PKT_LO));
+ _E(emit_gfx10_v_add_co_ci_u32_e32, I10(buf, n), P_V(VR_GA_HI),
+ P_I(0), P_V(VR_SAVE_PKT_HI));
+ _E(emit_gfx10_global_load_dwordx4, I10(buf, n), P_V(VR_DATA0),
+ P_V(VR_GA_LO), 0);
+ _E(emit_gfx10_global_load_dword, I10(buf, n), P_V(VR_BLK),
+ P_V(VR_GA_LO), 16);
+ _E(emit_gfx10_s_waitcnt_vmcnt, I10(buf, n));
+ _E(emit_gfx10_v_alignbit_b32, I10(buf, n), P_V(VR_DATA0),
+ P_V(VR_DATA1), P_V(VR_DATA0), P_I(16));
+ _E(emit_gfx10_v_alignbit_b32, I10(buf, n), P_V(VR_DATA1),
+ P_V(VR_DATA2), P_V(VR_DATA1), P_I(16));
+ _E(emit_gfx10_v_alignbit_b32, I10(buf, n), P_V(VR_DATA2),
+ P_V(VR_DATA3), P_V(VR_DATA2), P_I(16));
+ _E(emit_gfx10_v_alignbit_b32, I10(buf, n), P_V(VR_DATA3),
+ P_V(VR_BLK), P_V(VR_DATA3), P_I(16));
+
+ /* Compare: XOR each dword, OR together; if any non-zero -> fail */
+ _E(emit_gfx10_v_xor_b32_e32, I10(buf, n), P_V(VR_DATA0),
+ P_V(VR_DATA0), P_V(VR_S0));
+ _E(emit_gfx10_v_xor_b32_e32, I10(buf, n), P_V(VR_DATA1),
+ P_V(VR_DATA1), P_V(VR_S1));
+ _E(emit_gfx10_v_xor_b32_e32, I10(buf, n), P_V(VR_DATA2),
+ P_V(VR_DATA2), P_V(VR_S2));
+ _E(emit_gfx10_v_xor_b32_e32, I10(buf, n), P_V(VR_DATA3),
+ P_V(VR_DATA3), P_V(VR_S3));
+ _E(emit_gfx10_v_or_b32_e32, I10(buf, n), P_V(VR_DATA0),
+ P_V(VR_DATA0), P_V(VR_DATA1));
+ _E(emit_gfx10_v_or_b32_e32, I10(buf, n), P_V(VR_DATA0),
+ P_V(VR_DATA0), P_V(VR_DATA2));
+ _E(emit_gfx10_v_or_b32_e32, I10(buf, n), P_V(VR_DATA0),
+ P_V(VR_DATA0), P_V(VR_DATA3));
+
+ /* If VR_DATA0 != 0 -> ICV fail: overwrite verdict with sentinel */
+ _E(emit_gfx10_v_cmp_ne_u32, I10(buf, n), P_I(0), P_V(VR_DATA0));
+ br_icv_ok = _BR(emit_gfx10_s_cbranch_vccz, I10(buf, n), 0);
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(VR_SAVE_SLOT),
+ P_L(VERDICT_ICV_FAIL));
+ patch_branch(buf, br_icv_ok, n);
+
+ /* ================================================================
+ * Phase 10: ESP trailer strip + write verdict (thread 0)
+ *
+ * Decrypted tail: pad_len at out + ctext_len - 2
+ * next_hdr at out + ctext_len - 1
+ * inner_len = ctext_len - pad_len - 2
+ * ================================================================
+ */
+ /* Only strip if ICV passed (slot < NR_SA) */
+ _E(emit_gfx10_v_readfirstlane_b32, I10(buf, n), 27, VR_SAVE_SLOT);
+ _E(emit_gfx10_s_cmp_ge_u32, I10(buf, n), P_S(27),
+ P_L(KNOD_IPSEC_SHADER_NR_SA));
+ br_icv_bad = _BR(emit_gfx10_s_cbranch_scc1, I10(buf, n), 0);
+
+ /* Load last 4 bytes of decrypted payload: out + ctext_len - 4.
+ * VOP2 src1 must be VGPR, so move SGPR to VR_TMP first.
+ */
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(VR_TMP),
+ P_S(SR_CTEXT_LEN));
+ _E(emit_gfx10_v_add_nc_u32, I10(buf, n), P_V(VR_TMP),
+ P_L(0xFFFFFFFC), P_V(VR_TMP)); /* ctext_len - 4 */
+ _E(emit_gfx10_v_add_co_u32, I10(buf, n), P_V(VR_GA_LO),
+ P_V(VR_TMP), P_V(VR_SAVE_OUT_LO));
+ _E(emit_gfx10_v_add_co_ci_u32_e32, I10(buf, n), P_V(VR_GA_HI),
+ P_I(0), P_V(VR_SAVE_OUT_HI));
+ _E(emit_gfx10_global_load_dword, I10(buf, n), P_V(VR_DATA0),
+ P_V(VR_GA_LO), 0);
+ _E(emit_gfx10_s_waitcnt_vmcnt, I10(buf, n));
+
+ /* On LE: loaded dword has byte layout [b0,b1,b2,b3].
+ * We loaded from (ctext_len - 4), so:
+ * b2 = pad_len (at ctext_len - 2)
+ * b3 = next_hdr (at ctext_len - 1)
+ * pad_len = (dword >> 16) & 0xFF
+ */
+ _E(emit_gfx10_v_lshrrev_b32, I10(buf, n), P_V(VR_TMP),
+ P_I(16), P_V(VR_DATA0));
+ _E(emit_gfx10_v_and_b32_e32, I10(buf, n), P_V(VR_TMP),
+ P_L(0xFF), P_V(VR_TMP));
+
+ /* inner_len = ctext_len - pad_len - 2 */
+ _E(emit_gfx10_v_sub_nc_u32, I10(buf, n), P_V(VR_DATA1),
+ P_S(SR_CTEXT_LEN), P_V(VR_TMP));
+ _E(emit_gfx10_v_add_nc_u32, I10(buf, n), P_V(VR_DATA1),
+ P_L(0xFFFFFFFE), P_V(VR_DATA1)); /* -2 */
+
+ /* Write bd->len = inner_len (u16 at bd + 18) */
+ _E(emit_gfx10_v_add_co_u32, I10(buf, n), P_V(VR_GA_LO),
+ P_L(18), P_V(VR_SAVE_BD_LO));
+ _E(emit_gfx10_v_add_co_ci_u32_e32, I10(buf, n), P_V(VR_GA_HI),
+ P_I(0), P_V(VR_SAVE_BD_HI));
+ _E(emit_gfx10_global_store_short, I10(buf, n), P_V(VR_GA_LO),
+ P_V(VR_DATA1), 0);
+
+ /* Write bd->off = mode | (next_hdr << 8) (u16 at bd + 16).
+ * next_hdr = byte[3] of the ESP trailer dword (VR_DATA0).
+ * mode from s[SR_SA_MODE].
+ */
+ _E(emit_gfx10_v_lshrrev_b32, I10(buf, n), P_V(VR_TMP),
+ P_I(24), P_V(VR_DATA0));
+ _E(emit_gfx10_v_lshlrev_b32, I10(buf, n), P_V(VR_TMP),
+ P_I(8), P_V(VR_TMP));
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(VR_DATA2),
+ P_S(SR_SA_MODE));
+ _E(emit_gfx10_v_or_b32_e32, I10(buf, n), P_V(VR_TMP),
+ P_V(VR_DATA2), P_V(VR_TMP));
+ _E(emit_gfx10_v_add_co_u32, I10(buf, n), P_V(VR_GA_LO),
+ P_L(16), P_V(VR_SAVE_BD_LO));
+ _E(emit_gfx10_v_add_co_ci_u32_e32, I10(buf, n), P_V(VR_GA_HI),
+ P_I(0), P_V(VR_SAVE_BD_HI));
+ _E(emit_gfx10_global_store_short, I10(buf, n), P_V(VR_GA_LO),
+ P_V(VR_TMP), 0);
+
+ /* Per-SA GPU stats: atomically increment rx_packets
+ * and rx_bytes at stats_addr. VR_DATA1 still holds
+ * inner_len from the bd->len computation above.
+ *
+ * global_atomic_add_x2 uses v[data:data+1] as u64.
+ * Save inner_len to VR_TMP before clobbering DATA1.
+ *
+ * stats layout (knod_ipsec_sa_gpu_stats):
+ * +0: rx_packets (u64, LE)
+ * +8: rx_bytes (u64, LE)
+ */
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(VR_TMP),
+ P_V(VR_DATA1)); /* save inner_len */
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(VR_GA_LO),
+ P_V(VR_SAVE_STATS_LO));
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(VR_GA_HI),
+ P_V(VR_SAVE_STATS_HI));
+
+ /* rx_packets += 1: v[DATA0:DATA1] = {1, 0} */
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(VR_DATA0), P_I(1));
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(VR_DATA1), P_I(0));
+ _E(emit_gfx10_global_atomic_add_x2, I10(buf, n),
+ P_V(VR_DATA2), P_V(VR_GA_LO), P_V(VR_DATA0), 0, 0);
+
+ /* rx_bytes += inner_len: v[DATA0:DATA1] = {inner_len, 0} */
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(VR_DATA0),
+ P_V(VR_TMP));
+ /* DATA1 already 0 from above */
+ _E(emit_gfx10_global_atomic_add_x2, I10(buf, n),
+ P_V(VR_DATA2), P_V(VR_GA_LO), P_V(VR_DATA0), 8, 0);
+
+ /* ================================================================
+ * L3 header passthrough (transport mode only).
+ *
+ * Copy 20 B of the outer IPv4 L3 header from
+ * pkt + 14 (skip ETH) to out_addr - 20. In shader-GTT
+ * direct mode (knod_ipsec_sdma=0), out_addr - 20 is
+ * pass_buf_slot + 0 so the host-side finalise can skip
+ * the per-packet L3 SDMA copy entirely - the only
+ * remaining SDMA call on the transport IPv4 fast path.
+ *
+ * Tunnel-mode SAs (SR_SA_MODE != 0) skip this write
+ * because for tunnel the destination at slot+0 wants
+ * the *inner* packet, not an outer IP header.
+ *
+ * The VRAM staging path (knod_ipsec_sdma=1) also runs
+ * this copy, but the destination is the 20-byte
+ * headroom knod_ipsec.c reserves at the front of
+ * the decrypt pool; CPU finalise still SDMAs the real
+ * L3 header from the raw packet into the GTT pass_buf
+ * so the shader's write is harmless wasted work.
+ *
+ * Alignment: pkt + 14 is only 2-byte aligned (ETH hdr
+ * = 14 bytes != 4-byte multiple), so dword / dwordx4
+ * loads would fault. Use 10 x global_load_ushort at
+ * offsets 14,16,...,32, paired with 10 x store_short
+ * at slot + 0,2,...,18. 10 scratch VGPRs (v14..v23),
+ * all free by Phase 10 since AES-GCM / GHASH state is
+ * done. One waitcnt between loads and stores.
+ * ================================================================
+ */
+
+ _E(emit_gfx10_s_cmp_eq_u32, I10(buf, n),
+ P_S(SR_SA_MODE), P_I(0));
+ br_not_transport = _BR(emit_gfx10_s_cbranch_scc0,
+ I10(buf, n), 0);
+
+ /* src = pkt_addr + 14 */
+ _E(emit_gfx10_v_add_co_u32, I10(buf, n),
+ P_V(VR_GA_LO), P_I(14),
+ P_V(VR_SAVE_PKT_LO));
+ _E(emit_gfx10_v_add_co_ci_u32_e32, I10(buf, n),
+ P_V(VR_GA_HI), P_I(0),
+ P_V(VR_SAVE_PKT_HI));
+
+ /* 10 x 2-byte loads from src+0..+18 */
+ for (li = 0; li < 10; li++) {
+ _E(emit_gfx10_global_load_ushort,
+ I10(buf, n),
+ P_V(L3_TMP_BASE + li),
+ P_V(VR_GA_LO), li * 2);
+ }
+ _E(emit_gfx10_s_waitcnt_vmcnt, I10(buf, n));
+
+ /* dst = out_addr - 20.
+ *
+ * Two GFX9 landmines here:
+ *
+ * 1. v_add_co_u32 can't take a 32-bit
+ * literal src together with implicit VCC
+ * - same class as the v_cndmask literal
+ * restriction.
+ * 2. P_I(n) is a raw initializer that always
+ * sets type=INTEGER_0 and stores n in .v.
+ * For negative inline constants the
+ * encoder must flip to INTEGER_MINUS_1
+ * with v=~n, which P_I does NOT do.
+ * P_I(-1) therefore encodes as
+ * GFX9_SRC_INTEGER_0 + (-1) = 127, a
+ * bogus register that gave us random
+ * high-32 bits and page-faulted stores.
+ *
+ * Dodge both by materialising -20 into VR_TMP
+ * and -1 into VR_TMP2 via v_mov_b32 literals
+ * (VOP1, no VCC, literals fine), then pure
+ * VGPR+VGPR add_co / addc_co. Borrow flows
+ * through VCC as the carry-in to addc_co.
+ */
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n),
+ P_V(VR_TMP), P_L(0xFFFFFFECu));
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n),
+ P_V(VR_TMP2), P_L(0xFFFFFFFFu));
+ _E(emit_gfx10_v_add_co_u32, I10(buf, n),
+ P_V(VR_GA_LO),
+ P_V(VR_TMP), P_V(VR_SAVE_OUT_LO));
+ _E(emit_gfx10_v_add_co_ci_u32_e32, I10(buf, n),
+ P_V(VR_GA_HI),
+ P_V(VR_TMP2), P_V(VR_SAVE_OUT_HI));
+
+ /* 10 x 2-byte stores to dst+0..+18 */
+ for (li = 0; li < 10; li++) {
+ _E(emit_gfx10_global_store_short,
+ I10(buf, n),
+ P_V(VR_GA_LO),
+ P_V(L3_TMP_BASE + li),
+ li * 2);
+ }
+
+ patch_branch(buf, br_not_transport, n);
+
+ patch_branch(buf, br_icv_bad, n);
+
+ /* Write bd->act (u64 at bd + 8):
+ * high32 = slot_idx (or sentinel),
+ * low32 = KNOD_IPSEC_INFLIGHT so NIC NAPI recognises
+ * this slot as in-flight until the finish worker stamps
+ * the final PASS/DROP.
+ */
+ _E(emit_gfx10_v_add_co_u32, I10(buf, n), P_V(VR_GA_LO),
+ P_L(8), P_V(VR_SAVE_BD_LO));
+ _E(emit_gfx10_v_add_co_ci_u32_e32, I10(buf, n), P_V(VR_GA_HI),
+ P_I(0), P_V(VR_SAVE_BD_HI));
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(VR_DATA0),
+ P_L(KNOD_IPSEC_INFLIGHT));
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(VR_DATA1),
+ P_V(VR_SAVE_SLOT));
+ _E(emit_gfx10_global_store_dwordx2, I10(buf, n), P_V(VR_GA_LO),
+ P_V(VR_DATA0), 0);
+ _E(emit_gfx10_s_waitcnt_vmcnt, I10(buf, n));
+
+ patch_branch(buf, br_tid0, n);
+
+ /* ================================================================
+ * Phase 11: Miss/bypass path verdict - thread 0 only
+ *
+ * If we skipped crypto (Phase 2 branch), write the miss/bypass
+ * sentinel that's still in v[VR_SAVE_SLOT].
+ * ================================================================
+ */
+ br_crypto_done = _BR(emit_gfx10_s_branch, I10(buf, n), 0);
+
+ patch_branch(buf, br_crypto_end, n);
+
+ /* Thread 0 writes bd->act with miss/bypass sentinel */
+ _E(emit_gfx10_v_cmp_eq_u32, I10(buf, n), P_I(0), P_V(VR_TID));
+ _E(emit_gfx10_s_and_saveexec_b64, I10(buf, n), SR_EXEC_SAVE, 106);
+ br_execz2 = _BR(emit_gfx10_s_cbranch_execz, I10(buf, n), 0);
+
+ _E(emit_gfx10_v_add_co_u32, I10(buf, n), P_V(VR_GA_LO),
+ P_L(8), P_V(VR_SAVE_BD_LO));
+ _E(emit_gfx10_v_add_co_ci_u32_e32, I10(buf, n), P_V(VR_GA_HI),
+ P_I(0), P_V(VR_SAVE_BD_HI));
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(VR_DATA0),
+ P_L(KNOD_IPSEC_INFLIGHT));
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(VR_DATA1),
+ P_V(VR_SAVE_SLOT));
+ _E(emit_gfx10_global_store_dwordx2, I10(buf, n), P_V(VR_GA_LO),
+ P_V(VR_DATA0), 0);
+ _E(emit_gfx10_s_waitcnt_vmcnt, I10(buf, n));
+
+ patch_branch(buf, br_execz2, n);
+
+ patch_branch(buf, br_crypto_done, n);
+
+ /* ================================================================
+ * Phase 12: GPU-initiated SDMA dispatch (thread 0 only)
+ *
+ * Identical logic to GFX9 Phase 12. See ipsec_fused_gfx9.h
+ * for the full protocol description.
+ *
+ * GPU writes SDMA COPY_LINEAR packets only. FENCE, wptr update,
+ * and doorbell are left to the CPU.
+ *
+ * GFX10 differences:
+ * - v_add_nc_u32 / v_sub_nc_u32 (no-carry variants)
+ * - v_add_co_ci_u32_e32 for carry-in addition
+ * - s_or_b64 for 64-bit zero test (no s_or_b32 helper)
+ * - GFX10 global offset is 12-bit signed (all offsets <=56 OK)
+ * ================================================================
+ */
+ /* --- kernarg loads ---------------------------------------- */
+ _E(emit_gfx10_s_load_dwordx2, I10(buf, n),
+ P_S(24), P_S(8), 16); /* s[24:25] = sdma_ring_addr */
+ _E(emit_gfx10_s_load_dwordx2, I10(buf, n),
+ P_S(26), P_S(8), 32); /* s[26:27] = sdma_ctl_addr */
+ _E(emit_gfx10_s_waitcnt_lgkmcnt, I10(buf, n));
+
+ /* s_or_b64 sets SCC = (s[26:27] != 0) */
+ _E(emit_gfx10_s_or_b64, I10(buf, n), 28, 26, 26);
+ br_no_sdma = _BR(emit_gfx10_s_cbranch_scc0, I10(buf, n), 0);
+
+ /* --- load sdma_ctl into VGPRs ----------------------------- */
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(1), P_S(26));
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(2), P_S(27));
+
+ /* ctl+28: wptr_base_dw(4) ring_mask(4) nr_total_wg(4) copy_hdr(4)
+ * -> v[3:6]
+ */
+ _E(emit_gfx10_global_load_dwordx4, I10(buf, n),
+ P_V(3), P_V(1), 28);
+ _E(emit_gfx10_s_waitcnt_vmcnt, I10(buf, n));
+
+ /* v3=wptr_base_dw v4=ring_mask v5=nr_total_wg v6=copy_hdr */
+
+ _E(emit_gfx10_v_readfirstlane_b32, I10(buf, n),
+ 28, 5); /* s28 = nr_total_wg */
+
+ /* --- verdict check ---------------------------------------- */
+ _E(emit_gfx10_v_readfirstlane_b32, I10(buf, n),
+ 29, VR_SAVE_SLOT); /* s29 = verdict */
+ _E(emit_gfx10_s_cmp_ge_u32, I10(buf, n),
+ P_S(29), P_L(0xFFFFFFFEu)); /* MISS|BYPASS? */
+ br_no_copy = _BR(emit_gfx10_s_cbranch_scc0, I10(buf, n), 0);
+
+ /* === This WG needs SDMA copy === */
+
+ /* atomic_add(&ctl->claim_counter, 1, GLC=1) -> my_idx */
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(7), P_I(1));
+ _E(emit_gfx10_global_atomic_add, I10(buf, n),
+ P_V(7), P_V(1), P_V(7), 0, 1);
+ _E(emit_gfx10_s_waitcnt_vmcnt, I10(buf, n));
+ /* v7 = my_idx (old claim_counter) */
+
+ /* ring dword position: (wptr_base_dw + my_idx*7) & ring_mask */
+ _E(emit_gfx10_v_lshlrev_b32, I10(buf, n),
+ P_V(8), P_I(3), P_V(7)); /* my_idx * 8 */
+ _E(emit_gfx10_v_sub_nc_u32, I10(buf, n),
+ P_V(8), P_V(8), P_V(7)); /* my_idx * 7 */
+ _E(emit_gfx10_v_add_nc_u32, I10(buf, n),
+ P_V(8), P_V(3), P_V(8)); /* + wptr_base_dw */
+ _E(emit_gfx10_v_and_b32_e32, I10(buf, n),
+ P_V(8), P_V(4), P_V(8)); /* & ring_mask */
+ _E(emit_gfx10_v_lshlrev_b32, I10(buf, n),
+ P_V(8), P_I(2), P_V(8)); /* * 4 -> byte offset */
+
+ /* v[9:10] = sdma_ring_addr + byte_offset */
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(14), P_S(25));
+ _E(emit_gfx10_v_add_co_u32, I10(buf, n),
+ P_V(9), P_S(24), P_V(8));
+ _E(emit_gfx10_v_add_co_ci_u32_e32, I10(buf, n),
+ P_V(10), P_I(0), P_V(14));
+
+ /* DW0: copy_hdr */
+ _E(emit_gfx10_global_store_dword, I10(buf, n),
+ P_V(9), P_V(6), 0);
+
+ /* DW1: nbytes - 1 */
+ _E(emit_gfx10_v_add_nc_u32, I10(buf, n),
+ P_V(14), P_L(0xFFFFFFFFu), P_V(VR_SAVE_PKTLEN));
+ _E(emit_gfx10_global_store_dword, I10(buf, n),
+ P_V(9), P_V(14), 4);
+
+ /* DW2: 0 (sub-op parameter) */
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(14), P_I(0));
+ _E(emit_gfx10_global_store_dword, I10(buf, n),
+ P_V(9), P_V(14), 8);
+
+ /* DW3-4: src = pkt_addr (VRAM) */
+ _E(emit_gfx10_global_store_dword, I10(buf, n),
+ P_V(9), P_V(VR_SAVE_PKT_LO), 12);
+ _E(emit_gfx10_global_store_dword, I10(buf, n),
+ P_V(9), P_V(VR_SAVE_PKT_HI), 16);
+
+ /* DW5-6: dst = out_addr - 20 (GTT slot start) */
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n),
+ P_V(14), P_L(0xFFFFFFECu)); /* -20 */
+ _E(emit_gfx10_v_add_co_u32, I10(buf, n),
+ P_V(15), P_V(14), P_V(VR_SAVE_OUT_LO));
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n),
+ P_V(14), P_L(0xFFFFFFFFu)); /* -1 */
+ _E(emit_gfx10_v_add_co_ci_u32_e32, I10(buf, n),
+ P_V(16), P_V(14), P_V(VR_SAVE_OUT_HI));
+
+ _E(emit_gfx10_global_store_dword, I10(buf, n),
+ P_V(9), P_V(15), 20);
+ _E(emit_gfx10_global_store_dword, I10(buf, n),
+ P_V(9), P_V(16), 24);
+
+ _E(emit_gfx10_s_waitcnt_vmcnt, I10(buf, n));
+
+ /* --- done counter (all WGs) ------------------------------- */
+ patch_branch(buf, br_no_copy, n);
+
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(7), P_I(1));
+ _E(emit_gfx10_global_atomic_add, I10(buf, n),
+ P_V(7), P_V(1), P_V(7), 4, 1); /* ctl+4 = done_counter */
+ _E(emit_gfx10_s_waitcnt_vmcnt, I10(buf, n));
+
+ /* Last WG check: my_done + 1 == nr_total_wg? */
+ _E(emit_gfx10_v_add_nc_u32, I10(buf, n),
+ P_V(7), P_I(1), P_V(7));
+ _E(emit_gfx10_v_readfirstlane_b32, I10(buf, n), 29, 7);
+ _E(emit_gfx10_s_cmp_eq_u32, I10(buf, n), P_S(29), P_S(28));
+ br_not_last = _BR(emit_gfx10_s_cbranch_scc0, I10(buf, n), 0);
+
+ /* === Last WG - publish counters for CPU === */
+
+ /* Read final claim_counter (atomic add 0, GLC=1) */
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(7), P_I(0));
+ _E(emit_gfx10_global_atomic_add, I10(buf, n),
+ P_V(7), P_V(1), P_V(7), 0, 1);
+ _E(emit_gfx10_s_waitcnt_vmcnt, I10(buf, n));
+
+ /* Store final_sdma_count (ctl+52) and gpu_sdma_ready (ctl+48) */
+ _E(emit_gfx10_global_store_dword, I10(buf, n),
+ P_V(1), P_V(7), 52);
+ _E(emit_gfx10_v_mov_b32_e32, I10(buf, n), P_V(7), P_I(1));
+ _E(emit_gfx10_global_store_dword, I10(buf, n),
+ P_V(1), P_V(7), 48);
+ _E(emit_gfx10_s_waitcnt_vmcnt, I10(buf, n));
+
+ patch_branch(buf, br_not_last, n);
+ patch_branch(buf, br_no_sdma, n);
+
+ _E(emit_gfx10_s_endpgm, I10(buf, n));
+
+ /* GFX10 RDNA2 prefetches instructions aggressively past s_endpgm.
+ * Without an s_code_end cushion the SQC fetches garbage beyond the
+ * shader and raises an SQC(inst) page fault at a seemingly random
+ * address. Pad to a 256-dword boundary - same pattern the BPF GFX10
+ * emitter (knod_bpf.c) uses on working shaders.
+ */
+ while (n % 256)
+ _E(emit_gfx10_s_code_end, I10(buf, n));
+
+ return n * 4;
+}
+
+#endif /* KNOD_HELPERS_IPSEC_FUSED_GFX10_H_ */
diff --git a/drivers/gpu/drm/amd/amdkfd/knod/ipsec_fused_gfx9.h b/drivers/gpu/drm/amd/amdkfd/knod/ipsec_fused_gfx9.h
new file mode 100644
index 000000000000..5121ece1f2eb
--- /dev/null
+++ b/drivers/gpu/drm/amd/amdkfd/knod/ipsec_fused_gfx9.h
@@ -0,0 +1,1349 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (c) 2021 Taehee Yoo <ap420073@gmail.com>
+ * Copyright (c) 2021 Hoyeon Lee <hoyeon.rhee@gmail.com>
+ */
+
+/*
+ * KNOD IPsec fused RX shader - GFX9 (Vega10/20).
+ *
+ * Full AES-GCM decrypt pipeline for inbound ESP packets:
+ * 1) ESP header parse -> SPI + seq extract
+ * 2) SA table linear scan -> resolve SPI to slot index
+ * 3) Cooperative T-table load (VRAM -> LDS, 256 threads)
+ * 4) AES-CTR decrypt ciphertext -> out_addr
+ * 5) Parallel GHASH over (AAD || ciphertext || len)
+ * 6) ICV verify (GHASH ^ AES(K,J0) vs received tag)
+ * 7) ESP trailer strip -> inner_len
+ * 8) Write verdict to bd->act, inner_len to bd->len
+ *
+ * Dispatch geometry:
+ * workgroup = (256, 1, 1) - 256 threads = 1 AES block per thread
+ * grid = (256, nr_pkts, 1)
+ * workgroup_id_y == packet index in the batch
+ *
+ * Anti-replay is NOT in the shader - CPU-side sliding window in NIC NAPI.
+ *
+ * Verdict encoding in bd->act high32:
+ * 0..NR_SA-1 - SA hit + ICV pass, value is slot_idx
+ * 0xFFFFFFFF - SA miss (no entry for this SPI)
+ * 0xFFFFFFFE - non-IPv4/IPv6 bypass (unknown L3 protocol)
+ * 0xFFFFFFFD - ICV mismatch (decrypt succeeded but tag wrong)
+ *
+ * bd->len is set to inner_len on success (decrypted payload minus ESP
+ * trailer and padding). On miss/bypass/ICV-fail, bd->len is left as-is.
+ */
+
+#ifndef KNOD_HELPERS_IPSEC_FUSED_GFX9_H_
+#define KNOD_HELPERS_IPSEC_FUSED_GFX9_H_
+
+#include <linux/types.h>
+#include "knod_amdgpu_insn.h"
+/* Provide AESGCM_MAX_DIM_Y so aesgcm_shader.h compiles (OFF_T0 macro).
+ * Only emit_aes_encrypt_block_gfx9 / emit_gfmul_128_gfx9 are used here;
+ * the full aesgcm_gen_shader_* functions are unreferenced.
+ */
+#ifndef AESGCM_MAX_DIM_Y
+#define AESGCM_MAX_DIM_Y 1024
+#endif
+#include "aesgcm_shader.h"
+
+/* SA entry constants - must match knod_ipsec.h */
+#define KNOD_IPSEC_SHADER_NR_SA 256
+#define KNOD_IPSEC_SHADER_SA_ENTRY_SZ 104
+
+/* SA entry field offsets (struct knod_ipsec_sa_entry) */
+#define SA_OFF_SPI 0
+#define SA_OFF_KEY_ADDR 16
+#define SA_OFF_HTABLE_ADDR 24
+#define SA_OFF_T_TABLES_ADDR 32
+#define SA_OFF_SALT 40
+#define SA_OFF_KEY_LEN 44
+#define SA_OFF_NR_ROUNDS 48
+#define SA_OFF_MODE 52 /* XFRM_MODE_TRANSPORT=0, TUNNEL=1 */
+#define SA_OFF_STATS_ADDR 88 /* per-SA GPU stats (u64 gpu addr) */
+
+/* ESP packet geometry (ETH=14, IPv4=20 / IPv6=40, no VLAN/opts).
+ * IPv4: ESP header starts at offset 34 (14+20).
+ * IPv6: ESP header starts at offset 54 (14+40).
+ * Within ESP header: SPI+0, seq+4, IV+8, ctext+16.
+ * The shader dynamically computes offsets based on IP version.
+ */
+#define ESP_HDR_OFF_V4 34 /* ETH(14) + IPv4(20) */
+#define ESP_HDR_OFF_V6 54 /* ETH(14) + IPv6(40) */
+#define ESP_REL_SPI 0
+#define ESP_REL_SEQ 4
+#define ESP_REL_IV 8
+#define ESP_REL_CTEXT 16 /* SPI(4)+seq(4)+IV(8) */
+#define ESP_ICV_LEN 16
+
+/* Fixed IPv4 layout offsets used by the crypto KAT */
+#define ESP_SPI_OFF 34
+#define ESP_SEQ_OFF 38
+#define ESP_IV_OFF 42
+#define ESP_CTEXT_OFF 50
+
+/* Fused sub[] offsets within kernarg (sub[i] = kernarg + 40 + i*32) */
+#define SUB_BASE_OFF 40
+#define SUB_STRIDE 32
+#define SUB_OFF_PKT_ADDR 0
+#define SUB_OFF_OUT_ADDR 8
+#define SUB_OFF_BD_ADDR 16
+#define SUB_OFF_PKT_LEN 24
+#define SUB_OFF_RESULT_SEQ 28
+
+/* ICV-fail sentinel (distinct from MISS=0xFFFFFFFF and BYPASS=0xFFFFFFFE) */
+#define VERDICT_ICV_FAIL 0xFFFFFFFDu
+
+/* High VGPRs for saving pre-crypto IPsec state (above AES v0-v22 range) */
+#define VR_SAVE_SLOT 30
+#define VR_SAVE_BD_LO 31
+#define VR_SAVE_BD_HI 32
+#define VR_SAVE_PKT_LO 33
+#define VR_SAVE_PKT_HI 34
+#define VR_SAVE_PKTLEN 35
+#define VR_SAVE_OUT_LO 36
+#define VR_SAVE_OUT_HI 37
+#define VR_SAVE_SEQ 38
+#define VR_SAVE_SPI 39
+#define VR_SAVE_STATS_LO 40 /* per-SA stats GPU addr low */
+#define VR_SAVE_STATS_HI 41 /* per-SA stats GPU addr high */
+/* ESP header offset: 34(v4) or 54(v6) */
+#define VR_SAVE_ESP_OFF 42
+/* Ciphertext prefetch destination - free v23-v26, inside AES v0-v22 gap */
+#define VR_PREFETCH0 23
+#define VR_PREFETCH1 24
+#define VR_PREFETCH2 25
+#define VR_PREFETCH3 26
+
+/* Extra SGPRs for IPsec-specific state that survives into AES phases.
+ * These must NOT collide with SR_* from aesgcm_shader.h (s18-s49, s56-s59).
+ * s50-s55 are IPsec-specific. s56-s59 = SR_RK2 (AES round key double-buffer).
+ */
+#define SR_CTEXT_LEN 50 /* ciphertext length in bytes */
+#define SR_NBLOCKS_GCM 51 /* ceil(ctext_len/16) */
+#define SR_HTABLE_LO 52 /* H-power table GPU addr */
+#define SR_HTABLE_HI 53
+#define SR_TOTAL_GHASH_BLK 54 /* nblocks + 2 (AAD + ctext + len) */
+#define SR_SA_MODE 55 /* XFRM_MODE_TRANSPORT=0, TUNNEL=1 */
+
+/* File-local emit helpers */
+#ifndef _KNOD_IPSEC_EMIT
+#define _KNOD_IPSEC_EMIT
+#define _E(fn, ...) (n += fn(__VA_ARGS__) / 4)
+#define _BR(fn, ...) ({ int _p = n; n += fn(__VA_ARGS__) / 4; _p; })
+#endif
+
+static inline int kfd_ipsec_gen_fused_shader_gfx9(void *vbuf)
+{
+ int br_skip_aad, br_skip_ctext, br_skip_len;
+ int loop_top, br_match, br_loop, br_end;
+ int br_no_sdma, br_no_copy, br_not_last;
+ int br_ipv4, br_bypass, br_crypto_end;
+ const int L3_TMP_BASE = 14; /* v14..v23 */
+ int br_ipv6, br_v6_to_common;
+ int br_not_transport, li;
+ u32 *buf = (u32 *)vbuf;
+ int br_crypto_done;
+ int br_execz_ctr;
+ int br_execz2;
+ int br_icv_bad;
+ int br_icv_ok;
+ int br_tid0;
+ int level;
+ int n = 0;
+
+ /* ================================================================
+ * Phase 0: Parse ESP header + SA table lookup
+ *
+ * s_dcache_inv: flush K$ so s_load reads fresh round keys.
+ * ================================================================
+ */
+ _E(emit_gfx9_s_dcache_inv, I9(buf, n));
+ _E(emit_gfx9_s_waitcnt_lgkmcnt, I9(buf, n));
+
+ /* Cache bswap32 selector in SR_BSWAP for reuse across all phases */
+ _E(emit_gfx9_s_mov_b32, I9(buf, n), P_S(SR_BSWAP), P_L(0x00010203));
+
+ /* v1 = 40 + wg_id_y*32 = offset of sub[wg_id_y] within kernarg */
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(1), P_S(16));
+ _E(emit_gfx9_v_lshlrev_b32, I9(buf, n), P_V(1), P_I(5), P_V(1));
+ _E(emit_gfx9_v_add_u32, I9(buf, n), P_V(1), P_L(SUB_BASE_OFF), P_V(1));
+
+ /* v[3:4] = kernarg_ptr + v1 = &sub[wg_id_y] */
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(2), P_S(9));
+ _E(emit_gfx9_v_add_co_u32, I9(buf, n), P_V(3), P_S(8), P_V(1));
+ _E(emit_gfx9_v_addc_co_u32, I9(buf, n), P_V(4), P_I(0), P_V(2));
+
+ /* Load sub[].pkt_addr -> v[9:10], sub[].bd_addr -> v[5:6] */
+ _E(emit_gfx9_global_load_dwordx2, I9(buf, n), P_V(9), P_V(3),
+ SUB_OFF_PKT_ADDR);
+ _E(emit_gfx9_global_load_dwordx2, I9(buf, n), P_V(5), P_V(3),
+ SUB_OFF_BD_ADDR);
+ _E(emit_gfx9_s_waitcnt_vmcnt, I9(buf, n));
+
+ /* Seed v[11:12] with pkt_addr as a safe default BEFORE the IP
+ * version branch. The bypass path (non-v4/v6 packets like ARP)
+ * unconditionally branches past the v[11:12] setup at line ~195
+ * and later Phase 1 does a global_load at v[11:12]+ESP_REL_SEQ to
+ * read the ESP sequence number. Without this seed v[11:12] would
+ * hold uninitialised VGPR state (wave launch garbage), producing
+ * a fault at ~0x{random}_00000000. For valid v4/v6 packets the
+ * common path below overwrites v[11:12] with pkt+esp_hdr_off so
+ * this seed is harmless.
+ */
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(11), P_V(9));
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(12), P_V(10));
+
+ /* IP version gate: load dword at pkt+12 to get first byte of L3
+ * header (byte[14]). Extract version nibble -> s28.
+ */
+ _E(emit_gfx9_global_load_dword, I9(buf, n), P_V(15), P_V(9), 12);
+ _E(emit_gfx9_s_waitcnt_vmcnt, I9(buf, n));
+
+ _E(emit_gfx9_v_readfirstlane_b32, I9(buf, n), 28, 15);
+ _E(emit_gfx9_s_lshr_b32, I9(buf, n), P_S(28), P_S(28), P_I(20));
+ _E(emit_gfx9_s_and_b32_p, I9(buf, n), P_S(28), P_I(0xF), P_S(28));
+
+ /* Check IPv4 (version==4) */
+ _E(emit_gfx9_s_cmp_eq_u32, I9(buf, n), P_S(28), P_I(4));
+ br_ipv4 = _BR(emit_gfx9_s_cbranch_scc1, I9(buf, n), 0);
+
+ /* Check IPv6 (version==6) */
+ _E(emit_gfx9_s_cmp_eq_u32, I9(buf, n), P_S(28), P_I(6));
+ br_ipv6 = _BR(emit_gfx9_s_cbranch_scc1, I9(buf, n), 0);
+
+ /* Bypass: neither IPv4 nor IPv6 */
+ _E(emit_gfx9_s_mov_b32, I9(buf, n), P_S(26), P_L(0xFFFFFFFEu));
+ br_bypass = _BR(emit_gfx9_s_branch, I9(buf, n), 0);
+
+ /* IPv6 landing: esp_hdr_off = 54 */
+ patch_branch(buf, br_ipv6, n);
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(VR_SAVE_ESP_OFF),
+ P_L(ESP_HDR_OFF_V6));
+ br_v6_to_common = _BR(emit_gfx9_s_branch, I9(buf, n), 0);
+
+ /* IPv4 landing: esp_hdr_off = 34 */
+ patch_branch(buf, br_ipv4, n);
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(VR_SAVE_ESP_OFF),
+ P_L(ESP_HDR_OFF_V4));
+
+ /* Common path: both IPv4 and IPv6 converge here */
+ patch_branch(buf, br_v6_to_common, n);
+
+ /* v[11:12] = pkt_addr + esp_hdr_off (dynamic) */
+ _E(emit_gfx9_v_add_co_u32, I9(buf, n), P_V(11),
+ P_V(VR_SAVE_ESP_OFF), P_V(9));
+ _E(emit_gfx9_v_addc_co_u32, I9(buf, n), P_V(12), P_I(0), P_V(10));
+
+ /* v13 = *(u32*)(pkt + esp_hdr_off) - SPI in big-endian */
+ _E(emit_gfx9_global_load_dword, I9(buf, n), P_V(13), P_V(11), 0);
+ _E(emit_gfx9_s_waitcnt_vmcnt, I9(buf, n));
+
+ /* Byteswap SPI: v13 = bswap32(v13) via v_perm_b32 */
+ _E(emit_gfx9_v_perm_b32, I9(buf, n), P_V(13), P_V(13), P_V(13),
+ P_S(SR_BSWAP));
+
+ /* SA table linear scan (VMEM path for K$ coherence).
+ * s22 = target SPI, s[24:25] = sa_table_addr, s23 = counter,
+ * s26 = result (slot_idx or 0xFFFFFFFF), v[16:17] = running ptr.
+ */
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(16), P_S(8));
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(17), P_S(9));
+ _E(emit_gfx9_global_load_dwordx2, I9(buf, n), P_V(18), P_V(16), 0);
+ _E(emit_gfx9_s_waitcnt_vmcnt, I9(buf, n));
+ _E(emit_gfx9_v_readfirstlane_b32, I9(buf, n), 24, 18);
+ _E(emit_gfx9_v_readfirstlane_b32, I9(buf, n), 25, 19);
+
+ _E(emit_gfx9_v_readfirstlane_b32, I9(buf, n), 22, 13);
+ _E(emit_gfx9_s_mov_b32, I9(buf, n), P_S(23), P_I(0));
+ _E(emit_gfx9_s_mov_b32, I9(buf, n), P_S(26), P_L(0xFFFFFFFFu));
+
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(16), P_S(24));
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(17), P_S(25));
+
+ loop_top = n;
+ _E(emit_gfx9_global_load_dword, I9(buf, n), P_V(20), P_V(16),
+ SA_OFF_SPI);
+ _E(emit_gfx9_s_waitcnt_vmcnt, I9(buf, n));
+ _E(emit_gfx9_v_readfirstlane_b32, I9(buf, n), 27, 20);
+
+ _E(emit_gfx9_v_add_co_u32, I9(buf, n), P_V(16),
+ P_L(KNOD_IPSEC_SHADER_SA_ENTRY_SZ), P_V(16));
+ _E(emit_gfx9_v_addc_co_u32, I9(buf, n), P_V(17), P_I(0), P_V(17));
+ _E(emit_gfx9_s_add_u32, I9(buf, n), P_S(23), P_I(1), P_S(23));
+ _E(emit_gfx9_s_nop, I9(buf, n));
+
+ _E(emit_gfx9_s_cmp_eq_u32, I9(buf, n), P_S(27), P_S(22));
+ br_match = _BR(emit_gfx9_s_cbranch_scc1, I9(buf, n), 0);
+
+ _E(emit_gfx9_s_cmp_lt_u32, I9(buf, n), P_S(23),
+ P_L(KNOD_IPSEC_SHADER_NR_SA));
+ br_loop = _BR(emit_gfx9_s_cbranch_scc1, I9(buf, n), 0);
+ patch_branch(buf, br_loop, loop_top);
+
+ br_end = _BR(emit_gfx9_s_branch, I9(buf, n), 0);
+
+ /* Match: s26 = s23 - 1 */
+ patch_branch(buf, br_match, n);
+ _E(emit_gfx9_s_sub_u32_p, I9(buf, n), P_S(26), P_S(23), P_I(1));
+
+ patch_branch(buf, br_end, n);
+
+ patch_branch(buf, br_bypass, n);
+
+ /* ================================================================
+ * Phase 1: Save pre-crypto state + load extra sub[] fields
+ *
+ * Move IPsec-specific values to v30+ so v1-v22 and s18-s49 are
+ * free for AES-GCM helpers from aesgcm_shader.h.
+ * ================================================================
+ */
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(VR_SAVE_SLOT), P_S(26));
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(VR_SAVE_BD_LO), P_V(5));
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(VR_SAVE_BD_HI), P_V(6));
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(VR_SAVE_PKT_LO), P_V(9));
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(VR_SAVE_PKT_HI), P_V(10));
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(VR_SAVE_SPI), P_V(13));
+
+ /* Load sub[].out_addr -> v[VR_SAVE_OUT_LO:VR_SAVE_OUT_HI] */
+ _E(emit_gfx9_global_load_dwordx2, I9(buf, n), P_V(VR_SAVE_OUT_LO),
+ P_V(3), SUB_OFF_OUT_ADDR);
+ /* Load sub[].pkt_len -> v[VR_SAVE_PKTLEN] */
+ _E(emit_gfx9_global_load_dword, I9(buf, n), P_V(VR_SAVE_PKTLEN),
+ P_V(3), SUB_OFF_PKT_LEN);
+ _E(emit_gfx9_s_waitcnt_vmcnt, I9(buf, n));
+
+ /* Load ESP seq number: pkt + esp_hdr_off + 4, BE -> bswap ->
+ * v[VR_SAVE_SEQ].
+ * v[11:12] still holds pkt_addr + esp_hdr_off from Phase 0.
+ */
+ _E(emit_gfx9_global_load_dword, I9(buf, n), P_V(VR_SAVE_SEQ), P_V(11),
+ ESP_REL_SEQ);
+ _E(emit_gfx9_s_waitcnt_vmcnt, I9(buf, n));
+
+ _E(emit_gfx9_v_perm_b32, I9(buf, n), P_V(VR_SAVE_SEQ),
+ P_V(VR_SAVE_SEQ), P_V(VR_SAVE_SEQ), P_S(SR_BSWAP));
+
+ /* Write bswapped seq back into sub[].result_seq for CPU finish worker.
+ * v[3:4] still points to &sub[wg_id_y].
+ */
+ _E(emit_gfx9_global_store_dword, I9(buf, n), P_V(3),
+ P_V(VR_SAVE_SEQ), SUB_OFF_RESULT_SEQ);
+
+ /* ================================================================
+ * Phase 2: Branch on miss/bypass - skip crypto entirely
+ * ================================================================
+ */
+ _E(emit_gfx9_v_readfirstlane_b32, I9(buf, n), 26, VR_SAVE_SLOT);
+ _E(emit_gfx9_s_cmp_ge_u32, I9(buf, n), P_S(26),
+ P_L(KNOD_IPSEC_SHADER_NR_SA));
+ br_crypto_end = _BR(emit_gfx9_s_cbranch_scc1, I9(buf, n), 0);
+
+ /* ================================================================
+ * Phase 3: Load SA entry fields for the matched slot
+ *
+ * entry_addr = sa_table_addr + slot_idx * SA_ENTRY_SIZE
+ * Load: key_gpu_addr, salt, nr_rounds, t_tables_gpu_addr,
+ * htable_gpu_addr
+ * ================================================================
+ */
+ /* s27 = slot_idx * SA_ENTRY_SIZE (scalar mul) */
+ _E(emit_gfx9_s_mul_i32, I9(buf, n), P_S(27), P_S(26),
+ P_L(KNOD_IPSEC_SHADER_SA_ENTRY_SZ));
+ /* s[24:25] = sa_table_addr (already there from Phase 0 scan) */
+ _E(emit_gfx9_s_add_u32, I9(buf, n), P_S(24), P_S(24), P_S(27));
+ _E(emit_gfx9_s_addc_u32, I9(buf, n), P_S(25), P_S(25), P_I(0));
+
+ /* Use VMEM for coherence: stage entry addr into VGPR pair */
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(VR_GA_LO), P_S(24));
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(VR_GA_HI), P_S(25));
+
+ /* Batched SA entry loads: issue all 4 loads to different
+ * VGPR destinations, then single waitcnt. v1-v8 (S0-S3,
+ * D0-D3) are free at Phase 3 - not used until Phase 7.
+ *
+ * Layout:
+ * dwordx4 @+16 -> v[1:4]: key_lo, key_hi, htable_lo, htable_hi
+ * dwordx4 @+32 -> v[5:8]: ttables_lo, ttables_hi, salt, key_len
+ * dwordx2 @+48 -> v[14:15]: nr_rounds, mode
+ * dwordx2 @+88 -> v[16:17]: stats_lo, stats_hi
+ */
+ _E(emit_gfx9_global_load_dwordx4, I9(buf, n), P_V(VR_S0),
+ P_V(VR_GA_LO), SA_OFF_KEY_ADDR);
+ _E(emit_gfx9_global_load_dwordx4, I9(buf, n), P_V(VR_D0),
+ P_V(VR_GA_LO), SA_OFF_T_TABLES_ADDR);
+ _E(emit_gfx9_global_load_dwordx2, I9(buf, n), P_V(VR_DATA0),
+ P_V(VR_GA_LO), SA_OFF_NR_ROUNDS);
+ _E(emit_gfx9_global_load_dwordx2, I9(buf, n), P_V(VR_DATA2),
+ P_V(VR_GA_LO), SA_OFF_STATS_ADDR);
+ _E(emit_gfx9_s_waitcnt_vmcnt, I9(buf, n));
+
+ /* key_addr: v1=lo, v2=hi */
+ _E(emit_gfx9_v_readfirstlane_b32, I9(buf, n), SR_KEYS, VR_S0);
+ _E(emit_gfx9_v_readfirstlane_b32, I9(buf, n), SR_KEYS + 1, VR_S1);
+ /* htable_addr: v3=lo, v4=hi */
+ _E(emit_gfx9_v_readfirstlane_b32, I9(buf, n), SR_HTABLE_LO, VR_S2);
+ _E(emit_gfx9_v_readfirstlane_b32, I9(buf, n), SR_HTABLE_HI, VR_S3);
+ /* t_tables_addr: v5=lo, v6=hi */
+ _E(emit_gfx9_v_readfirstlane_b32, I9(buf, n), SR_T_ADDR, VR_D0);
+ _E(emit_gfx9_v_readfirstlane_b32, I9(buf, n), SR_T_ADDR + 1, VR_D1);
+ /* salt: v7 */
+ _E(emit_gfx9_v_readfirstlane_b32, I9(buf, n), SR_IV0, VR_D2);
+ /* nr_rounds: v14, mode: v15 */
+ _E(emit_gfx9_v_readfirstlane_b32, I9(buf, n), SR_NR_ROUNDS, VR_DATA0);
+ _E(emit_gfx9_v_readfirstlane_b32, I9(buf, n), SR_SA_MODE, VR_DATA1);
+ /* stats_addr: v16=lo, v17=hi -> save VGPRs for Phase 10 */
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(VR_SAVE_STATS_LO),
+ P_V(VR_DATA2));
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(VR_SAVE_STATS_HI),
+ P_V(VR_DATA3));
+
+ /* ================================================================
+ * Phase 4: Build AES-GCM nonce
+ *
+ * nonce[12] = salt[4] || IV[8]
+ * salt is already in s[SR_IV0]. Load IV from pkt + esp_hdr_off + 8.
+ * Recompute ESP base from saved pkt_addr + VR_SAVE_ESP_OFF since
+ * v[11:12] were clobbered by Phase 3 SA loads (VR_GA_LO=12).
+ * IV goes to s[SR_IV1] (bytes 4-7) and s[SR_IV2] (bytes 8-11).
+ * ================================================================
+ */
+ _E(emit_gfx9_v_add_co_u32, I9(buf, n), P_V(VR_GA_LO),
+ P_V(VR_SAVE_ESP_OFF), P_V(VR_SAVE_PKT_LO));
+ _E(emit_gfx9_v_addc_co_u32, I9(buf, n), P_V(VR_GA_HI),
+ P_I(0), P_V(VR_SAVE_PKT_HI));
+ _E(emit_gfx9_global_load_dwordx2, I9(buf, n), P_V(VR_DATA0),
+ P_V(VR_GA_LO), ESP_REL_IV);
+ _E(emit_gfx9_s_waitcnt_vmcnt, I9(buf, n));
+ _E(emit_gfx9_v_readfirstlane_b32, I9(buf, n), SR_IV1, VR_DATA0);
+ _E(emit_gfx9_v_readfirstlane_b32, I9(buf, n), SR_IV2, VR_DATA1);
+
+ /* ================================================================
+ * Phase 5: Compute ciphertext bounds
+ *
+ * ctext_off = esp_hdr_off + 16 (SPI+seq+IV)
+ * ctext_len = pkt_len - ctext_off - ICV_LEN
+ * nblocks = (ctext_len + 15) >> 4
+ *
+ * s28 is free here (last used in version gate) - use as scratch.
+ * ================================================================
+ */
+ _E(emit_gfx9_v_readfirstlane_b32, I9(buf, n), 28, VR_SAVE_ESP_OFF);
+ _E(emit_gfx9_s_add_u32, I9(buf, n), P_S(28),
+ P_I(ESP_REL_CTEXT + ESP_ICV_LEN),
+ P_S(28)); /* s28 = ctext_off + ICV_LEN = overhead to subtract */
+ _E(emit_gfx9_v_readfirstlane_b32, I9(buf, n), SR_CTEXT_LEN,
+ VR_SAVE_PKTLEN);
+ _E(emit_gfx9_s_sub_u32_p, I9(buf, n), P_S(SR_CTEXT_LEN),
+ P_S(SR_CTEXT_LEN), P_S(28));
+ /* s[SR_NBLOCKS_GCM] = (ctext_len + 15) >> 4 */
+ _E(emit_gfx9_s_add_u32, I9(buf, n), P_S(SR_NBLOCKS_GCM), P_I(15),
+ P_S(SR_CTEXT_LEN));
+ _E(emit_gfx9_s_lshr_b32, I9(buf, n), P_S(SR_NBLOCKS_GCM),
+ P_S(SR_NBLOCKS_GCM), P_I(4));
+ /* total GHASH blocks = 1(AAD) + nblocks(ctext) + 1(len) = nblocks + 2
+ */
+ _E(emit_gfx9_s_add_u32, I9(buf, n), P_S(SR_TOTAL_GHASH_BLK),
+ P_I(2), P_S(SR_NBLOCKS_GCM));
+
+ /* ================================================================
+ * Phase 6: Cooperative T-table load (VRAM -> LDS)
+ *
+ * All 256 threads load from SA's t_tables_gpu_addr. Each thread
+ * loads one u32 per table (4 tables x 256 entries = 4KB).
+ * ================================================================
+ */
+ _E(emit_gfx9_s_mov_b32, I9(buf, n), P_S(SR_MASK), P_L(0xFF));
+
+ /* v[VR_TMP] = tid * 4 (byte offset within each 1KB table) */
+ _E(emit_gfx9_v_lshlrev_b32, I9(buf, n), P_V(VR_TMP), P_I(2),
+ P_V(VR_TID));
+
+ /* T0: VRAM[t_tables + tid*4] -> LDS[tid*4] */
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(VR_GA_LO), P_S(SR_T_ADDR));
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(VR_GA_HI),
+ P_S(SR_T_ADDR + 1));
+ _E(emit_gfx9_v_add_co_u32, I9(buf, n), P_V(VR_GA_LO), P_V(VR_GA_LO),
+ P_V(VR_TMP));
+ _E(emit_gfx9_v_addc_co_u32, I9(buf, n), P_V(VR_GA_HI), P_I(0),
+ P_V(VR_GA_HI));
+ _E(emit_gfx9_global_load_dword, I9(buf, n), P_V(VR_DATA0),
+ P_V(VR_GA_LO), 0);
+ _E(emit_gfx9_global_load_dword, I9(buf, n), P_V(VR_DATA1),
+ P_V(VR_GA_LO), 1024);
+ _E(emit_gfx9_global_load_dword, I9(buf, n), P_V(VR_DATA2),
+ P_V(VR_GA_LO), 2048);
+ _E(emit_gfx9_global_load_dword, I9(buf, n), P_V(VR_DATA3),
+ P_V(VR_GA_LO), 3072);
+ _E(emit_gfx9_s_waitcnt_vmcnt, I9(buf, n));
+
+ /* Write to LDS: T0 at +0, T1 at +1024, T2 at +2048, T3 at +3072 */
+ _E(emit_gfx9_ds_write_b32, I9(buf, n), VR_TMP, VR_DATA0);
+ _E(emit_gfx9_v_add_u32, I9(buf, n), P_V(VR_ADDR), P_L(1024),
+ P_V(VR_TMP));
+ _E(emit_gfx9_ds_write_b32, I9(buf, n), VR_ADDR, VR_DATA1);
+ _E(emit_gfx9_v_add_u32, I9(buf, n), P_V(VR_ADDR), P_L(2048),
+ P_V(VR_TMP));
+ _E(emit_gfx9_ds_write_b32, I9(buf, n), VR_ADDR, VR_DATA2);
+ _E(emit_gfx9_v_add_u32, I9(buf, n), P_V(VR_ADDR), P_L(3072),
+ P_V(VR_TMP));
+ _E(emit_gfx9_ds_write_b32, I9(buf, n), VR_ADDR, VR_DATA3);
+
+ _E(emit_gfx9_s_waitcnt_lgkmcnt, I9(buf, n));
+ _E(emit_gfx9_s_barrier, I9(buf, n));
+
+ /* ================================================================
+ * Phase 7: AES-CTR decrypt
+ *
+ * Each thread handles block_id = tid. Only threads with tid <
+ * nblocks are active. Counter = nonce[12] || bswap32(tid+2).
+ * AES-encrypt the counter -> keystream. XOR with ciphertext ->
+ * plaintext. Store to out_addr + tid*16.
+ * ================================================================
+ */
+ /* VCC = (nblocks > tid) i.e. tid < nblocks - selects active CTR lanes
+ */
+ _E(emit_gfx9_v_cmp_gt_u32, I9(buf, n), P_S(SR_NBLOCKS_GCM),
+ P_V(VR_TID));
+ _E(emit_gfx9_s_and_saveexec_b64, I9(buf, n), SR_EXEC_SAVE,
+ 106 /* VCC_LO */);
+ br_execz_ctr = _BR(emit_gfx9_s_cbranch_execz, I9(buf, n), 0);
+
+ /* Save SR_KEYS for reload after this block encrypt */
+ _E(emit_gfx9_s_mov_b32, I9(buf, n), P_S(SR_T_ADDR), P_S(SR_KEYS));
+ _E(emit_gfx9_s_mov_b32, I9(buf, n), P_S(SR_T_ADDR + 1),
+ P_S(SR_KEYS + 1));
+
+ /* Prefetch ciphertext into v[23:26] before AES.
+ * The ~200+ cycle AES encrypt hides the VMEM latency.
+ * v23-v26 are not touched by AES rounds (which use
+ * v1-v18 only). VR_GA/VR_BLK are also AES-safe.
+ */
+ _E(emit_gfx9_v_lshlrev_b32, I9(buf, n), P_V(VR_BLK), P_I(4),
+ P_V(VR_TID));
+ _E(emit_gfx9_v_add_co_u32, I9(buf, n), P_V(VR_GA_LO),
+ P_V(VR_SAVE_ESP_OFF), P_V(VR_SAVE_PKT_LO));
+ _E(emit_gfx9_v_addc_co_u32, I9(buf, n), P_V(VR_GA_HI),
+ P_I(0), P_V(VR_SAVE_PKT_HI));
+ _E(emit_gfx9_v_add_co_u32, I9(buf, n), P_V(VR_GA_LO),
+ P_I(ESP_REL_CTEXT), P_V(VR_GA_LO));
+ _E(emit_gfx9_v_addc_co_u32, I9(buf, n), P_V(VR_GA_HI),
+ P_I(0), P_V(VR_GA_HI));
+ _E(emit_gfx9_v_add_co_u32, I9(buf, n), P_V(VR_GA_LO),
+ P_V(VR_GA_LO), P_V(VR_BLK));
+ _E(emit_gfx9_v_addc_co_u32, I9(buf, n), P_V(VR_GA_HI),
+ P_I(0), P_V(VR_GA_HI));
+ _E(emit_gfx9_global_load_dwordx4, I9(buf, n), P_V(VR_PREFETCH0),
+ P_V(VR_GA_LO), 0);
+
+ /* Build AES counter block in v[VR_S0:VR_S3]:
+ * VR_S0 = nonce[0:3] = salt (SR_IV0)
+ * VR_S1 = nonce[4:7] = IV[0:3] (SR_IV1)
+ * VR_S2 = nonce[8:11] = IV[4:7] (SR_IV2)
+ * VR_S3 = bswap32(tid + 2)
+ */
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(VR_S0), P_S(SR_IV0));
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(VR_S1), P_S(SR_IV1));
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(VR_S2), P_S(SR_IV2));
+
+ /* v[VR_S3] = bswap32(tid + 2) */
+ _E(emit_gfx9_v_add_u32, I9(buf, n), P_V(VR_S3), P_I(2), P_V(VR_TID));
+ _E(emit_gfx9_v_perm_b32, I9(buf, n), P_V(VR_S3), P_V(VR_S3),
+ P_V(VR_S3), P_S(SR_BSWAP));
+
+ /* AES encrypt the counter block -> result in v[VR_S0:VR_S3] */
+ n = emit_aes_encrypt_block_gfx9(buf, n);
+
+ /* Ciphertext arrived during AES - drain vmcnt */
+ _E(emit_gfx9_s_waitcnt_vmcnt, I9(buf, n));
+
+ /* XOR keystream with prefetched ciphertext -> plaintext */
+ _E(emit_gfx9_v_xor_b32_e32, I9(buf, n), P_V(VR_PREFETCH0),
+ P_V(VR_S0), P_V(VR_PREFETCH0));
+ _E(emit_gfx9_v_xor_b32_e32, I9(buf, n), P_V(VR_PREFETCH1),
+ P_V(VR_S1), P_V(VR_PREFETCH1));
+ _E(emit_gfx9_v_xor_b32_e32, I9(buf, n), P_V(VR_PREFETCH2),
+ P_V(VR_S2), P_V(VR_PREFETCH2));
+ _E(emit_gfx9_v_xor_b32_e32, I9(buf, n), P_V(VR_PREFETCH3),
+ P_V(VR_S3), P_V(VR_PREFETCH3));
+
+ /* Store plaintext to out_addr + tid*16 */
+ _E(emit_gfx9_v_add_co_u32, I9(buf, n), P_V(VR_GA_LO),
+ P_V(VR_BLK), P_V(VR_SAVE_OUT_LO));
+ _E(emit_gfx9_v_addc_co_u32, I9(buf, n), P_V(VR_GA_HI),
+ P_I(0), P_V(VR_SAVE_OUT_HI));
+ _E(emit_gfx9_global_store_dwordx4, I9(buf, n), P_V(VR_GA_LO),
+ P_V(VR_PREFETCH0), 0);
+
+ patch_branch(buf, br_execz_ctr, n);
+
+ /* ================================================================
+ * Phase 7.5: Compute AES(K, J0) for ICV finalization
+ *
+ * J0 = nonce[12] || 0x00000001 (BE). Only thread 0 needs this
+ * but all active lanes can compute it; we just save the result.
+ * ================================================================
+ */
+ /* Restore SR_KEYS (consumed by encrypt_block) */
+ _E(emit_gfx9_s_mov_b32, I9(buf, n), P_S(SR_KEYS), P_S(SR_T_ADDR));
+ _E(emit_gfx9_s_mov_b32, I9(buf, n), P_S(SR_KEYS + 1),
+ P_S(SR_T_ADDR + 1));
+
+ /* Restore full EXEC for J0 encrypt (all 256 threads) */
+ _E(emit_gfx9_s_or_b64, I9(buf, n), 126 /* EXEC_LO */, SR_EXEC_SAVE,
+ SR_EXEC_SAVE);
+
+ /* J0 block: nonce || bswap32(1) = nonce || 0x01000000 */
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(VR_S0), P_S(SR_IV0));
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(VR_S1), P_S(SR_IV1));
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(VR_S2), P_S(SR_IV2));
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(VR_S3), P_L(0x01000000u));
+
+ n = emit_aes_encrypt_block_gfx9(buf, n);
+
+ /* Save AES(K, J0) -> v[VR_J0_0:VR_J0_3] */
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(VR_J0_0), P_V(VR_S0));
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(VR_J0_1), P_V(VR_S1));
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(VR_J0_2), P_V(VR_S2));
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(VR_J0_3), P_V(VR_S3));
+
+ _E(emit_gfx9_s_barrier, I9(buf, n));
+
+ /* ================================================================
+ * Phase 8: Parallel GHASH
+ *
+ * GHASH input blocks (total_blocks = nblocks + 2):
+ * tid 0 -> AAD: SPI(4B,BE)||seq(4B,BE)||0s (16B)
+ * tid 1..nblocks -> ciphertext block (tid-1)
+ * tid nblocks+1 -> len: AAD_bitlen(64b)||ctext_bitlen(64b)
+ * tid > nblocks+1 -> zero (does not participate)
+ *
+ * Each thread loads its block -> v[VR_DATA0:VR_DATA3] (big-endian
+ * for GF multiply), loads H^(total-tid) -> v[VR_D0:VR_D3], runs
+ * GF multiply -> v[VR_S0:VR_S3], then tree-reduces via LDS XOR.
+ * ================================================================
+ */
+ /* Prefetch H^(total-tid) from H-power table before data selection.
+ * The ~60 ALU instructions in the data selection block below
+ * cover the VMEM latency. Result lands in VR_D0:D3, which
+ * data selection does not touch. VR_TMP/VR_GA are consumed
+ * here then free for reuse by the ctext section.
+ */
+ _E(emit_gfx9_v_sub_u32, I9(buf, n), P_V(VR_TMP),
+ P_S(SR_TOTAL_GHASH_BLK), P_V(VR_TID));
+ _E(emit_gfx9_v_add_u32, I9(buf, n), P_V(VR_TMP),
+ P_L(0xFFFFFFFF), P_V(VR_TMP));
+ _E(emit_gfx9_v_max_i32, I9(buf, n), P_V(VR_TMP), P_I(0), P_V(VR_TMP));
+ _E(emit_gfx9_v_lshlrev_b32, I9(buf, n), P_V(VR_TMP), P_I(4),
+ P_V(VR_TMP));
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(VR_GA_LO),
+ P_S(SR_HTABLE_LO));
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(VR_GA_HI),
+ P_S(SR_HTABLE_HI));
+ _E(emit_gfx9_v_add_co_u32, I9(buf, n), P_V(VR_GA_LO),
+ P_V(VR_GA_LO), P_V(VR_TMP));
+ _E(emit_gfx9_v_addc_co_u32, I9(buf, n), P_V(VR_GA_HI),
+ P_I(0), P_V(VR_GA_HI));
+ _E(emit_gfx9_global_load_dwordx4, I9(buf, n), P_V(VR_D0),
+ P_V(VR_GA_LO), 0);
+
+ /* EXEC-based per-lane data selection. Default = zero, then
+ * each case narrows EXEC to matching lanes and writes data.
+ * This avoids scalar VCC branching (s_cbranch_vccnz) which
+ * makes the entire wave take one path, not individual lanes.
+ */
+ /* Default: all threads get zero (non-participating) */
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(VR_DATA0), P_I(0));
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(VR_DATA1), P_I(0));
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(VR_DATA2), P_I(0));
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(VR_DATA3), P_I(0));
+
+ /* ---- AAD: tid == 0 ---- */
+ _E(emit_gfx9_v_cmp_eq_u32, I9(buf, n), P_I(0), P_V(VR_TID));
+ _E(emit_gfx9_s_and_saveexec_b64, I9(buf, n), SR_GHASH_EXEC,
+ 106 /* VCC */);
+ br_skip_aad = _BR(emit_gfx9_s_cbranch_execz, I9(buf, n), 0);
+
+ /* VR_SAVE_SPI/SEQ are already in BE register convention:
+ * raw LE load from packet (BE wire bytes) + bswap = byte[0]
+ * in bits[31:24]. No second bswap needed - use directly.
+ */
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(VR_DATA0),
+ P_V(VR_SAVE_SPI));
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(VR_DATA1),
+ P_V(VR_SAVE_SEQ));
+ /* DATA2, DATA3 already 0 */
+
+ patch_branch(buf, br_skip_aad, n);
+ _E(emit_gfx9_s_mov_b64, I9(buf, n), 126 /* EXEC */,
+ SR_GHASH_EXEC);
+
+ /* ---- Ctext: 1 <= tid <= nblocks ---- */
+ /* block_idx = tid - 1 (unsigned; tid==0 -> 0xFFFFFFFF > nblocks) */
+ _E(emit_gfx9_v_add_u32, I9(buf, n), P_V(VR_TMP), P_L(0xFFFFFFFF),
+ P_V(VR_TID));
+ _E(emit_gfx9_v_cmp_gt_u32, I9(buf, n), P_S(SR_NBLOCKS_GCM),
+ P_V(VR_TMP));
+ _E(emit_gfx9_s_and_saveexec_b64, I9(buf, n), SR_GHASH_EXEC,
+ 106 /* VCC */);
+ br_skip_ctext = _BR(emit_gfx9_s_cbranch_execz, I9(buf, n), 0);
+
+ /* Load ctext block: pkt + esp_hdr_off + 16 + block_idx*16 */
+ _E(emit_gfx9_v_lshlrev_b32, I9(buf, n), P_V(VR_TMP), P_I(4),
+ P_V(VR_TMP));
+ _E(emit_gfx9_v_add_co_u32, I9(buf, n), P_V(VR_GA_LO),
+ P_V(VR_SAVE_ESP_OFF), P_V(VR_SAVE_PKT_LO));
+ _E(emit_gfx9_v_addc_co_u32, I9(buf, n), P_V(VR_GA_HI),
+ P_I(0), P_V(VR_SAVE_PKT_HI));
+ _E(emit_gfx9_v_add_co_u32, I9(buf, n), P_V(VR_GA_LO),
+ P_I(ESP_REL_CTEXT), P_V(VR_GA_LO));
+ _E(emit_gfx9_v_addc_co_u32, I9(buf, n), P_V(VR_GA_HI),
+ P_I(0), P_V(VR_GA_HI));
+ _E(emit_gfx9_v_add_co_u32, I9(buf, n), P_V(VR_GA_LO),
+ P_V(VR_GA_LO), P_V(VR_TMP));
+ _E(emit_gfx9_v_addc_co_u32, I9(buf, n), P_V(VR_GA_HI),
+ P_I(0), P_V(VR_GA_HI));
+ _E(emit_gfx9_global_load_dwordx4, I9(buf, n), P_V(VR_DATA0),
+ P_V(VR_GA_LO), 0);
+ _E(emit_gfx9_s_waitcnt_vmcnt, I9(buf, n));
+
+ /* Zero trailing dwords in the last partial ctext block.
+ * The load above reads 16 raw bytes, but for the last block
+ * only (ctext_len % 16) bytes are ciphertext - the rest are
+ * ICV bytes which must NOT enter GHASH. ESP ctext is always
+ * 4-byte aligned so the partial count is 4, 8 or 12 - pure
+ * dword-level zeroing suffices, no byte masking needed.
+ *
+ * VR_TMP still holds block_idx * 16 from the address calc.
+ * remaining = ctext_len - block_idx*16. For full blocks
+ * (remaining >= 16) every v_cmp evaluates true -> no change.
+ */
+ _E(emit_gfx9_v_sub_u32, I9(buf, n), P_V(VR_TMP),
+ P_S(SR_CTEXT_LEN), P_V(VR_TMP));
+ /* VR_TMP = remaining bytes in this block */
+
+ /* DATA3 (bytes 12-15): keep only if remaining > 12 */
+ _E(emit_gfx9_v_cmp_lt_u32, I9(buf, n), P_I(12), P_V(VR_TMP));
+ _E(emit_gfx9_v_cndmask_b32_e32, I9(buf, n),
+ P_V(VR_DATA3), P_I(0), P_V(VR_DATA3));
+
+ /* DATA2 (bytes 8-11): keep only if remaining > 8 */
+ _E(emit_gfx9_v_cmp_lt_u32, I9(buf, n), P_I(8), P_V(VR_TMP));
+ _E(emit_gfx9_v_cndmask_b32_e32, I9(buf, n),
+ P_V(VR_DATA2), P_I(0), P_V(VR_DATA2));
+
+ /* DATA1 (bytes 4-7): keep only if remaining > 4 */
+ _E(emit_gfx9_v_cmp_lt_u32, I9(buf, n), P_I(4), P_V(VR_TMP));
+ _E(emit_gfx9_v_cndmask_b32_e32, I9(buf, n),
+ P_V(VR_DATA1), P_I(0), P_V(VR_DATA1));
+
+ /* DATA0 (bytes 0-3): always valid (ESP 4-byte alignment) */
+
+ /* bswap each dword for GHASH (big-endian GF arithmetic) */
+ _E(emit_gfx9_v_perm_b32, I9(buf, n), P_V(VR_DATA0),
+ P_V(VR_DATA0), P_V(VR_DATA0), P_S(SR_BSWAP));
+ _E(emit_gfx9_v_perm_b32, I9(buf, n), P_V(VR_DATA1),
+ P_V(VR_DATA1), P_V(VR_DATA1), P_S(SR_BSWAP));
+ _E(emit_gfx9_v_perm_b32, I9(buf, n), P_V(VR_DATA2),
+ P_V(VR_DATA2), P_V(VR_DATA2), P_S(SR_BSWAP));
+ _E(emit_gfx9_v_perm_b32, I9(buf, n), P_V(VR_DATA3),
+ P_V(VR_DATA3), P_V(VR_DATA3), P_S(SR_BSWAP));
+
+ patch_branch(buf, br_skip_ctext, n);
+ _E(emit_gfx9_s_mov_b64, I9(buf, n), 126 /* EXEC */,
+ SR_GHASH_EXEC);
+
+ /* ---- Len block: tid == nblocks + 1 ---- */
+ /* Compute nblocks+1 in s42 (scratch) */
+ _E(emit_gfx9_s_add_u32, I9(buf, n), P_S(42), P_I(1),
+ P_S(SR_NBLOCKS_GCM));
+ _E(emit_gfx9_v_cmp_eq_u32, I9(buf, n), P_S(42),
+ P_V(VR_TID));
+ _E(emit_gfx9_s_and_saveexec_b64, I9(buf, n), SR_GHASH_EXEC,
+ 106 /* VCC */);
+ br_skip_len = _BR(emit_gfx9_s_cbranch_execz, I9(buf, n), 0);
+
+ /* Length block format (GCM big-endian):
+ * DATA0 = AAD_bits[63:32] = 0
+ * DATA1 = AAD_bits[31:0] = 64 (8 bytes AAD x 8)
+ * DATA2 = ctext_bits[63:32] = 0
+ * DATA3 = ctext_bits[31:0] = ctext_len * 8
+ *
+ * No bswap32: length values are computed integers already
+ * in the correct big-endian register representation.
+ * bswap32 is only needed for data loaded from LE memory.
+ */
+ /* DATA0, DATA2 already 0 from default init */
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(VR_DATA1),
+ P_L(0x00000040u));
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(VR_DATA3),
+ P_S(SR_CTEXT_LEN));
+ _E(emit_gfx9_v_lshlrev_b32, I9(buf, n), P_V(VR_DATA3), P_I(3),
+ P_V(VR_DATA3));
+
+ patch_branch(buf, br_skip_len, n);
+ _E(emit_gfx9_s_mov_b64, I9(buf, n), 126 /* EXEC */,
+ SR_GHASH_EXEC);
+
+ /* H-table data was prefetched before data selection; drain + bswap */
+ _E(emit_gfx9_s_waitcnt_vmcnt, I9(buf, n));
+ _E(emit_gfx9_v_perm_b32, I9(buf, n), P_V(VR_D0),
+ P_V(VR_D0), P_V(VR_D0), P_S(SR_BSWAP));
+ _E(emit_gfx9_v_perm_b32, I9(buf, n), P_V(VR_D1),
+ P_V(VR_D1), P_V(VR_D1), P_S(SR_BSWAP));
+ _E(emit_gfx9_v_perm_b32, I9(buf, n), P_V(VR_D2),
+ P_V(VR_D2), P_V(VR_D2), P_S(SR_BSWAP));
+ _E(emit_gfx9_v_perm_b32, I9(buf, n), P_V(VR_D3),
+ P_V(VR_D3), P_V(VR_D3), P_S(SR_BSWAP));
+
+ /* ---- GF(2^128) multiply: Z = DATA * H^k ---- */
+ n = emit_gfmul_128_gfx9(buf, n);
+ /* Result in v[VR_S0:VR_S3] */
+
+ /* ---- Tree reduction via LDS XOR (8 levels for 256 threads) ---- */
+ /* Write v[VR_S0:VR_S3] to LDS at tid * 16 */
+ _E(emit_gfx9_v_lshlrev_b32, I9(buf, n), P_V(VR_ADDR), P_I(4),
+ P_V(VR_TID));
+ _E(emit_gfx9_ds_write_b128, I9(buf, n), VR_ADDR, VR_S0);
+ _E(emit_gfx9_s_waitcnt_lgkmcnt, I9(buf, n));
+ _E(emit_gfx9_s_barrier, I9(buf, n));
+
+ for (level = 1; level <= 128; level <<= 1) {
+ int br_skip;
+
+ /* if (tid & level) skip */
+ _E(emit_gfx9_v_and_b32_e32, I9(buf, n), P_V(VR_TMP),
+ P_L(level), P_V(VR_TID));
+ _E(emit_gfx9_v_cmp_eq_u32, I9(buf, n), P_I(0), P_V(VR_TMP));
+ _E(emit_gfx9_s_and_saveexec_b64, I9(buf, n), SR_GHASH_EXEC,
+ 106 /* VCC */);
+ br_skip = _BR(emit_gfx9_s_cbranch_execz, I9(buf, n), 0);
+
+ /* Compute both addresses up front */
+ _E(emit_gfx9_v_add_u32, I9(buf, n), P_V(VR_TMP),
+ P_L(level), P_V(VR_TID));
+ _E(emit_gfx9_v_lshlrev_b32, I9(buf, n), P_V(VR_TMP),
+ P_I(4), P_V(VR_TMP));
+ _E(emit_gfx9_v_lshlrev_b32, I9(buf, n), P_V(VR_ADDR),
+ P_I(4), P_V(VR_TID));
+
+ /* Issue both reads, single wait */
+ _E(emit_gfx9_ds_read_b128, I9(buf, n), VR_D0, VR_TMP);
+ _E(emit_gfx9_ds_read_b128, I9(buf, n), VR_S0, VR_ADDR);
+ _E(emit_gfx9_s_waitcnt_lgkmcnt, I9(buf, n));
+
+ /* XOR */
+ _E(emit_gfx9_v_xor_b32_e32, I9(buf, n), P_V(VR_S0),
+ P_V(VR_S0), P_V(VR_D0));
+ _E(emit_gfx9_v_xor_b32_e32, I9(buf, n), P_V(VR_S1),
+ P_V(VR_S1), P_V(VR_D1));
+ _E(emit_gfx9_v_xor_b32_e32, I9(buf, n), P_V(VR_S2),
+ P_V(VR_S2), P_V(VR_D2));
+ _E(emit_gfx9_v_xor_b32_e32, I9(buf, n), P_V(VR_S3),
+ P_V(VR_S3), P_V(VR_D3));
+
+ /* Write back */
+ _E(emit_gfx9_ds_write_b128, I9(buf, n), VR_ADDR, VR_S0);
+ _E(emit_gfx9_s_waitcnt_lgkmcnt, I9(buf, n));
+
+ patch_branch(buf, br_skip, n);
+ /* Restore EXEC */
+ _E(emit_gfx9_s_mov_b64, I9(buf, n), 126 /* EXEC */,
+ SR_GHASH_EXEC);
+ _E(emit_gfx9_s_barrier, I9(buf, n));
+ }
+
+ /* Thread 0 now has the final GHASH in LDS[0..15]. Read it. */
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(VR_TMP), P_I(0));
+ _E(emit_gfx9_ds_read_b128, I9(buf, n), VR_S0, VR_TMP);
+ _E(emit_gfx9_s_waitcnt_lgkmcnt, I9(buf, n));
+
+ /* ================================================================
+ * Phase 9: ICV verify (thread 0 only)
+ *
+ * computed_tag = bswap(GHASH) XOR AES(K, J0)
+ * received_tag = last 16 bytes of ESP packet
+ * ================================================================
+ */
+ _E(emit_gfx9_v_cmp_eq_u32, I9(buf, n), P_I(0), P_V(VR_TID));
+ _E(emit_gfx9_s_and_saveexec_b64, I9(buf, n), SR_EXEC_SAVE, 106);
+ br_tid0 = _BR(emit_gfx9_s_cbranch_execz, I9(buf, n), 0);
+
+ /* bswap GHASH from big-endian to little-endian */
+ _E(emit_gfx9_v_perm_b32, I9(buf, n), P_V(VR_S0), P_V(VR_S0),
+ P_V(VR_S0), P_S(SR_BSWAP));
+ _E(emit_gfx9_v_perm_b32, I9(buf, n), P_V(VR_S1), P_V(VR_S1),
+ P_V(VR_S1), P_S(SR_BSWAP));
+ _E(emit_gfx9_v_perm_b32, I9(buf, n), P_V(VR_S2), P_V(VR_S2),
+ P_V(VR_S2), P_S(SR_BSWAP));
+ _E(emit_gfx9_v_perm_b32, I9(buf, n), P_V(VR_S3), P_V(VR_S3),
+ P_V(VR_S3), P_S(SR_BSWAP));
+
+ /* computed_tag = GHASH XOR AES(K, J0) */
+ _E(emit_gfx9_v_xor_b32_e32, I9(buf, n), P_V(VR_S0),
+ P_V(VR_S0), P_V(VR_J0_0));
+ _E(emit_gfx9_v_xor_b32_e32, I9(buf, n), P_V(VR_S1),
+ P_V(VR_S1), P_V(VR_J0_1));
+ _E(emit_gfx9_v_xor_b32_e32, I9(buf, n), P_V(VR_S2),
+ P_V(VR_S2), P_V(VR_J0_2));
+ _E(emit_gfx9_v_xor_b32_e32, I9(buf, n), P_V(VR_S3),
+ P_V(VR_S3), P_V(VR_J0_3));
+
+ /* Load received ICV: pkt + pkt_len - 16 */
+ _E(emit_gfx9_v_add_u32, I9(buf, n), P_V(VR_TMP),
+ P_L(0xFFFFFFF0u), P_V(VR_SAVE_PKTLEN)); /* pkt_len - 16 */
+ _E(emit_gfx9_v_add_co_u32, I9(buf, n), P_V(VR_GA_LO),
+ P_V(VR_TMP), P_V(VR_SAVE_PKT_LO));
+ _E(emit_gfx9_v_addc_co_u32, I9(buf, n), P_V(VR_GA_HI),
+ P_I(0), P_V(VR_SAVE_PKT_HI));
+ _E(emit_gfx9_global_load_dwordx4, I9(buf, n), P_V(VR_DATA0),
+ P_V(VR_GA_LO), 0);
+ _E(emit_gfx9_s_waitcnt_vmcnt, I9(buf, n));
+
+ /* Compare: XOR each dword, OR together; if any non-zero -> fail */
+ _E(emit_gfx9_v_xor_b32_e32, I9(buf, n), P_V(VR_DATA0),
+ P_V(VR_DATA0), P_V(VR_S0));
+ _E(emit_gfx9_v_xor_b32_e32, I9(buf, n), P_V(VR_DATA1),
+ P_V(VR_DATA1), P_V(VR_S1));
+ _E(emit_gfx9_v_xor_b32_e32, I9(buf, n), P_V(VR_DATA2),
+ P_V(VR_DATA2), P_V(VR_S2));
+ _E(emit_gfx9_v_xor_b32_e32, I9(buf, n), P_V(VR_DATA3),
+ P_V(VR_DATA3), P_V(VR_S3));
+ _E(emit_gfx9_v_or_b32_e32, I9(buf, n), P_V(VR_DATA0),
+ P_V(VR_DATA0), P_V(VR_DATA1));
+ _E(emit_gfx9_v_or_b32_e32, I9(buf, n), P_V(VR_DATA0),
+ P_V(VR_DATA0), P_V(VR_DATA2));
+ _E(emit_gfx9_v_or_b32_e32, I9(buf, n), P_V(VR_DATA0),
+ P_V(VR_DATA0), P_V(VR_DATA3));
+
+ /* If VR_DATA0 != 0 -> ICV fail: overwrite verdict with sentinel */
+ _E(emit_gfx9_v_cmp_ne_u32, I9(buf, n), P_I(0), P_V(VR_DATA0));
+ br_icv_ok = _BR(emit_gfx9_s_cbranch_vccz, I9(buf, n), 0);
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(VR_SAVE_SLOT),
+ P_L(VERDICT_ICV_FAIL));
+ patch_branch(buf, br_icv_ok, n);
+
+ /* ================================================================
+ * Phase 10: ESP trailer strip + write verdict (thread 0)
+ *
+ * Decrypted tail: pad_len at out + ctext_len - 2
+ * next_hdr at out + ctext_len - 1
+ * inner_len = ctext_len - pad_len - 2
+ * ================================================================
+ */
+ /* Only strip if ICV passed (slot < NR_SA) */
+ _E(emit_gfx9_v_readfirstlane_b32, I9(buf, n), 27, VR_SAVE_SLOT);
+ _E(emit_gfx9_s_cmp_ge_u32, I9(buf, n), P_S(27),
+ P_L(KNOD_IPSEC_SHADER_NR_SA));
+ br_icv_bad = _BR(emit_gfx9_s_cbranch_scc1, I9(buf, n), 0);
+
+ /* Load last 4 bytes of decrypted payload: out + ctext_len - 4.
+ * VOP2 src1 must be VGPR, so move SGPR to VR_TMP first.
+ */
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(VR_TMP),
+ P_S(SR_CTEXT_LEN));
+ _E(emit_gfx9_v_add_u32, I9(buf, n), P_V(VR_TMP),
+ P_L(0xFFFFFFFC), P_V(VR_TMP)); /* ctext_len - 4 */
+ _E(emit_gfx9_v_add_co_u32, I9(buf, n), P_V(VR_GA_LO),
+ P_V(VR_TMP), P_V(VR_SAVE_OUT_LO));
+ _E(emit_gfx9_v_addc_co_u32, I9(buf, n), P_V(VR_GA_HI),
+ P_I(0), P_V(VR_SAVE_OUT_HI));
+ _E(emit_gfx9_global_load_dword, I9(buf, n), P_V(VR_DATA0),
+ P_V(VR_GA_LO), 0);
+ _E(emit_gfx9_s_waitcnt_vmcnt, I9(buf, n));
+
+ /* On LE: loaded dword has byte layout [b0,b1,b2,b3].
+ * We loaded from (ctext_len - 4), so:
+ * b2 = pad_len (at ctext_len - 2)
+ * b3 = next_hdr (at ctext_len - 1)
+ * pad_len = (dword >> 16) & 0xFF
+ */
+ _E(emit_gfx9_v_lshrrev_b32, I9(buf, n), P_V(VR_TMP),
+ P_I(16), P_V(VR_DATA0));
+ _E(emit_gfx9_v_and_b32_e32, I9(buf, n), P_V(VR_TMP),
+ P_L(0xFF), P_V(VR_TMP));
+
+ /* inner_len = ctext_len - pad_len - 2 */
+ _E(emit_gfx9_v_sub_u32, I9(buf, n), P_V(VR_DATA1),
+ P_S(SR_CTEXT_LEN), P_V(VR_TMP));
+ _E(emit_gfx9_v_add_u32, I9(buf, n), P_V(VR_DATA1),
+ P_L(0xFFFFFFFE), P_V(VR_DATA1)); /* -2 */
+
+ /* Write bd->len = inner_len (u16 at bd + 18) */
+ _E(emit_gfx9_v_add_co_u32, I9(buf, n), P_V(VR_GA_LO),
+ P_L(18), P_V(VR_SAVE_BD_LO));
+ _E(emit_gfx9_v_addc_co_u32, I9(buf, n), P_V(VR_GA_HI),
+ P_I(0), P_V(VR_SAVE_BD_HI));
+ _E(emit_gfx9_global_store_short, I9(buf, n), P_V(VR_GA_LO),
+ P_V(VR_DATA1), 0);
+
+ /* Write bd->off = mode | (next_hdr << 8) (u16 at bd + 16).
+ * next_hdr = byte[3] of the ESP trailer dword (VR_DATA0).
+ * mode from s[SR_SA_MODE].
+ */
+ _E(emit_gfx9_v_lshrrev_b32, I9(buf, n), P_V(VR_TMP),
+ P_I(24), P_V(VR_DATA0));
+ _E(emit_gfx9_v_lshlrev_b32, I9(buf, n), P_V(VR_TMP),
+ P_I(8), P_V(VR_TMP));
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(VR_DATA2),
+ P_S(SR_SA_MODE));
+ _E(emit_gfx9_v_or_b32_e32, I9(buf, n), P_V(VR_TMP),
+ P_V(VR_DATA2), P_V(VR_TMP));
+ _E(emit_gfx9_v_add_co_u32, I9(buf, n), P_V(VR_GA_LO),
+ P_L(16), P_V(VR_SAVE_BD_LO));
+ _E(emit_gfx9_v_addc_co_u32, I9(buf, n), P_V(VR_GA_HI),
+ P_I(0), P_V(VR_SAVE_BD_HI));
+ _E(emit_gfx9_global_store_short, I9(buf, n), P_V(VR_GA_LO),
+ P_V(VR_TMP), 0);
+
+ /* Per-SA GPU stats: atomically increment rx_packets
+ * and rx_bytes at stats_addr. VR_DATA1 still holds
+ * inner_len from the bd->len computation above.
+ *
+ * global_atomic_add_x2 uses v[data:data+1] as u64.
+ * Save inner_len to VR_TMP before clobbering DATA1.
+ *
+ * stats layout (knod_ipsec_sa_gpu_stats):
+ * +0: rx_packets (u64, LE)
+ * +8: rx_bytes (u64, LE)
+ */
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(VR_TMP),
+ P_V(VR_DATA1)); /* save inner_len */
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(VR_GA_LO),
+ P_V(VR_SAVE_STATS_LO));
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(VR_GA_HI),
+ P_V(VR_SAVE_STATS_HI));
+
+ /* rx_packets += 1: v[DATA0:DATA1] = {1, 0} */
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(VR_DATA0), P_I(1));
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(VR_DATA1), P_I(0));
+ _E(emit_gfx9_global_atomic_add_x2, I9(buf, n),
+ P_V(VR_DATA2), P_V(VR_GA_LO), P_V(VR_DATA0), 0, 0);
+
+ /* rx_bytes += inner_len: v[DATA0:DATA1] = {inner_len, 0} */
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(VR_DATA0),
+ P_V(VR_TMP));
+ /* DATA1 already 0 from above */
+ _E(emit_gfx9_global_atomic_add_x2, I9(buf, n),
+ P_V(VR_DATA2), P_V(VR_GA_LO), P_V(VR_DATA0), 8, 0);
+
+ /* ================================================================
+ * L3 header passthrough (transport mode only).
+ *
+ * Copy 20 B of the outer IPv4 L3 header from
+ * pkt + 14 (skip ETH) to out_addr - 20. In shader-GTT
+ * direct mode (knod_ipsec_sdma=0), out_addr - 20 is
+ * pass_buf_slot + 0 so the host-side finalise can skip
+ * the per-packet L3 SDMA copy entirely - the only
+ * remaining SDMA call on the transport IPv4 fast path.
+ *
+ * Tunnel-mode SAs (SR_SA_MODE != 0) skip this write
+ * because for tunnel the destination at slot+0 wants
+ * the *inner* packet, not an outer IP header.
+ *
+ * The VRAM staging path (knod_ipsec_sdma=1) also runs
+ * this copy, but the destination is the 20-byte
+ * headroom knod_ipsec.c reserves at the front of
+ * the decrypt pool; CPU finalise still SDMAs the real
+ * L3 header from the raw packet into the GTT pass_buf
+ * so the shader's write is harmless wasted work.
+ *
+ * Alignment: pkt + 14 is only 2-byte aligned (ETH hdr
+ * = 14 bytes != 4-byte multiple), so dword / dwordx4
+ * loads would fault. Use 10 x global_load_ushort at
+ * offsets 14,16,...,32, paired with 10 x store_short
+ * at slot + 0,2,...,18. 10 scratch VGPRs (v14..v23),
+ * all free by Phase 10 since AES-GCM / GHASH state is
+ * done. One waitcnt between loads and stores.
+ * ================================================================
+ */
+ _E(emit_gfx9_s_cmp_eq_u32, I9(buf, n),
+ P_S(SR_SA_MODE), P_I(0));
+ br_not_transport = _BR(emit_gfx9_s_cbranch_scc0,
+ I9(buf, n), 0);
+
+ /* src = pkt_addr + 14 */
+ _E(emit_gfx9_v_add_co_u32, I9(buf, n),
+ P_V(VR_GA_LO), P_I(14),
+ P_V(VR_SAVE_PKT_LO));
+ _E(emit_gfx9_v_addc_co_u32, I9(buf, n),
+ P_V(VR_GA_HI), P_I(0),
+ P_V(VR_SAVE_PKT_HI));
+
+ /* 10 x 2-byte loads from src+0..+18 */
+ for (li = 0; li < 10; li++) {
+ _E(emit_gfx9_global_load_ushort,
+ I9(buf, n),
+ P_V(L3_TMP_BASE + li),
+ P_V(VR_GA_LO), li * 2);
+ }
+ _E(emit_gfx9_s_waitcnt_vmcnt, I9(buf, n));
+
+ /* dst = out_addr - 20.
+ *
+ * Two GFX9 landmines here:
+ *
+ * 1. v_add_co_u32 can't take a 32-bit
+ * literal src together with implicit VCC
+ * - same class as the v_cndmask literal
+ * restriction.
+ * 2. P_I(n) is a raw initializer that always
+ * sets type=INTEGER_0 and stores n in .v.
+ * For negative inline constants the
+ * encoder must flip to INTEGER_MINUS_1
+ * with v=~n, which P_I does NOT do.
+ * P_I(-1) therefore encodes as
+ * GFX9_SRC_INTEGER_0 + (-1) = 127, a
+ * bogus register that gave us random
+ * high-32 bits and page-faulted stores.
+ *
+ * Dodge both by materialising -20 into VR_TMP
+ * and -1 into VR_TMP2 via v_mov_b32 literals
+ * (VOP1, no VCC, literals fine), then pure
+ * VGPR+VGPR add_co / addc_co. Borrow flows
+ * through VCC as the carry-in to addc_co.
+ */
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n),
+ P_V(VR_TMP), P_L(0xFFFFFFECu));
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n),
+ P_V(VR_TMP2), P_L(0xFFFFFFFFu));
+ _E(emit_gfx9_v_add_co_u32, I9(buf, n),
+ P_V(VR_GA_LO),
+ P_V(VR_TMP), P_V(VR_SAVE_OUT_LO));
+ _E(emit_gfx9_v_addc_co_u32, I9(buf, n),
+ P_V(VR_GA_HI),
+ P_V(VR_TMP2), P_V(VR_SAVE_OUT_HI));
+
+ /* 10 x 2-byte stores to dst+0..+18 */
+ for (li = 0; li < 10; li++) {
+ _E(emit_gfx9_global_store_short,
+ I9(buf, n),
+ P_V(VR_GA_LO),
+ P_V(L3_TMP_BASE + li),
+ li * 2);
+ }
+
+ patch_branch(buf, br_not_transport, n);
+
+ patch_branch(buf, br_icv_bad, n);
+
+ /* Write bd->act (u64 at bd + 8):
+ * high32 = slot_idx (or sentinel),
+ * low32 = KNOD_IPSEC_INFLIGHT so NIC NAPI recognises
+ * this slot as in-flight until the finish worker stamps
+ * the final PASS/DROP.
+ */
+ _E(emit_gfx9_v_add_co_u32, I9(buf, n), P_V(VR_GA_LO),
+ P_L(8), P_V(VR_SAVE_BD_LO));
+ _E(emit_gfx9_v_addc_co_u32, I9(buf, n), P_V(VR_GA_HI),
+ P_I(0), P_V(VR_SAVE_BD_HI));
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(VR_DATA0),
+ P_L(KNOD_IPSEC_INFLIGHT));
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(VR_DATA1),
+ P_V(VR_SAVE_SLOT));
+ _E(emit_gfx9_global_store_dwordx2, I9(buf, n), P_V(VR_GA_LO),
+ P_V(VR_DATA0), 0);
+ _E(emit_gfx9_s_waitcnt_vmcnt, I9(buf, n));
+
+ patch_branch(buf, br_tid0, n);
+
+ /* ================================================================
+ * Phase 11: Miss/bypass path verdict - thread 0 only
+ *
+ * If we skipped crypto (Phase 2 branch), write the miss/bypass
+ * sentinel that's still in v[VR_SAVE_SLOT].
+ * ================================================================
+ */
+ br_crypto_done = _BR(emit_gfx9_s_branch, I9(buf, n), 0);
+
+ patch_branch(buf, br_crypto_end, n);
+
+ /* Thread 0 writes bd->act with miss/bypass sentinel */
+ _E(emit_gfx9_v_cmp_eq_u32, I9(buf, n), P_I(0), P_V(VR_TID));
+ _E(emit_gfx9_s_and_saveexec_b64, I9(buf, n), SR_EXEC_SAVE, 106);
+
+ br_execz2 = _BR(emit_gfx9_s_cbranch_execz, I9(buf, n), 0);
+
+ _E(emit_gfx9_v_add_co_u32, I9(buf, n), P_V(VR_GA_LO),
+ P_L(8), P_V(VR_SAVE_BD_LO));
+ _E(emit_gfx9_v_addc_co_u32, I9(buf, n), P_V(VR_GA_HI),
+ P_I(0), P_V(VR_SAVE_BD_HI));
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(VR_DATA0),
+ P_L(KNOD_IPSEC_INFLIGHT));
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(VR_DATA1),
+ P_V(VR_SAVE_SLOT));
+ _E(emit_gfx9_global_store_dwordx2, I9(buf, n), P_V(VR_GA_LO),
+ P_V(VR_DATA0), 0);
+ _E(emit_gfx9_s_waitcnt_vmcnt, I9(buf, n));
+
+ patch_branch(buf, br_execz2, n);
+
+ patch_branch(buf, br_crypto_done, n);
+
+ /* ================================================================
+ * Phase 12: GPU-initiated SDMA dispatch (thread 0 only)
+ *
+ * Both merge paths (crypto-done, bypass/miss) arrive here with
+ * EXEC restricted to lane 0 of wave 0; waves 1-3 have EXEC=0.
+ * Scalar instructions fire on every wave but are side-effect-free;
+ * vector memory ops are NOPs when EXEC=0.
+ *
+ * The GPU writes SDMA COPY_LINEAR packets only. FENCE, wptr
+ * update, and doorbell are left to the CPU - this avoids a race
+ * where a GPU-emitted FENCE would prematurely satisfy the CPU
+ * fence poll before CPU-added SDMA copies (e.g. IPv6 transport
+ * L3 header) complete.
+ *
+ * Protocol per work-group (= per packet):
+ * 1. Load sdma_ring_addr (kernarg+16) and sdma_ctl_addr (+32).
+ * 2. If sdma_ctl_addr == 0, gpu_sdma disabled - skip.
+ * 3. Load sdma_ctl fields (wptr_base_dw, ring_mask, etc.).
+ * 4. If verdict is MISS/BYPASS, claim a ring slot via
+ * atomic_add(&claim_counter) and write a 7-dword
+ * SDMA COPY_LINEAR packet.
+ * 5. ALL WGs atomic_add(&done_counter).
+ * 6. Last WG: read final claim_counter, store
+ * final_sdma_count + gpu_sdma_ready for CPU.
+ * ================================================================
+ */
+ /* --- kernarg loads ---------------------------------------- */
+ _E(emit_gfx9_s_load_dwordx2, I9(buf, n),
+ P_S(24), P_S(8), 16); /* s[24:25] = sdma_ring_addr */
+ _E(emit_gfx9_s_load_dwordx2, I9(buf, n),
+ P_S(26), P_S(8), 32); /* s[26:27] = sdma_ctl_addr */
+ _E(emit_gfx9_s_waitcnt_lgkmcnt, I9(buf, n));
+
+ /* Skip Phase 12 entirely if sdma_ctl_addr == 0 */
+ _E(emit_gfx9_s_or_b32, I9(buf, n), 28, 26, 27);
+ _E(emit_gfx9_s_cmp_eq_u32, I9(buf, n), P_S(28), P_I(0));
+ br_no_sdma = _BR(emit_gfx9_s_cbranch_scc1, I9(buf, n), 0);
+
+ /* --- load sdma_ctl into VGPRs ----------------------------- */
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(1), P_S(26));
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(2), P_S(27));
+
+ /* ctl+28: wptr_base_dw(4) ring_mask(4) nr_total_wg(4) copy_hdr(4)
+ * -> v[3:6]
+ */
+ _E(emit_gfx9_global_load_dwordx4, I9(buf, n),
+ P_V(3), P_V(1), 28);
+ _E(emit_gfx9_s_waitcnt_vmcnt, I9(buf, n));
+
+ /* v3=wptr_base_dw v4=ring_mask v5=nr_total_wg v6=copy_hdr */
+
+ _E(emit_gfx9_v_readfirstlane_b32, I9(buf, n),
+ 28, 5); /* s28 = nr_total_wg */
+
+ /* --- verdict check ---------------------------------------- */
+ _E(emit_gfx9_v_readfirstlane_b32, I9(buf, n),
+ 29, VR_SAVE_SLOT); /* s29 = verdict */
+ _E(emit_gfx9_s_cmp_ge_u32, I9(buf, n),
+ P_S(29), P_L(0xFFFFFFFEu)); /* MISS|BYPASS? */
+ br_no_copy = _BR(emit_gfx9_s_cbranch_scc0, I9(buf, n), 0);
+
+ /* === This WG needs SDMA copy === */
+
+ /* atomic_add(&ctl->claim_counter, 1, GLC=1) -> my_idx */
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(7), P_I(1));
+ _E(emit_gfx9_global_atomic_add, I9(buf, n),
+ P_V(7), P_V(1), P_V(7), 0, 1);
+ _E(emit_gfx9_s_waitcnt_vmcnt, I9(buf, n));
+ /* v7 = my_idx (old claim_counter) */
+
+ /* ring dword position: (wptr_base_dw + my_idx*7) & ring_mask */
+ _E(emit_gfx9_v_lshlrev_b32, I9(buf, n),
+ P_V(8), P_I(3), P_V(7)); /* my_idx * 8 */
+ _E(emit_gfx9_v_sub_u32, I9(buf, n),
+ P_V(8), P_V(8), P_V(7)); /* my_idx * 7 */
+ _E(emit_gfx9_v_add_u32, I9(buf, n),
+ P_V(8), P_V(3), P_V(8)); /* + wptr_base_dw */
+ _E(emit_gfx9_v_and_b32_e32, I9(buf, n),
+ P_V(8), P_V(4), P_V(8)); /* & ring_mask */
+ _E(emit_gfx9_v_lshlrev_b32, I9(buf, n),
+ P_V(8), P_I(2), P_V(8)); /* * 4 -> byte offset */
+
+ /* v[9:10] = sdma_ring_addr + byte_offset */
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(14), P_S(25));
+ _E(emit_gfx9_v_add_co_u32, I9(buf, n),
+ P_V(9), P_S(24), P_V(8));
+ _E(emit_gfx9_v_addc_co_u32, I9(buf, n),
+ P_V(10), P_I(0), P_V(14));
+
+ /* DW0: copy_hdr */
+ _E(emit_gfx9_global_store_dword, I9(buf, n),
+ P_V(9), P_V(6), 0);
+
+ /* DW1: nbytes - 1 */
+ _E(emit_gfx9_v_add_u32, I9(buf, n),
+ P_V(14), P_L(0xFFFFFFFFu), P_V(VR_SAVE_PKTLEN));
+ _E(emit_gfx9_global_store_dword, I9(buf, n),
+ P_V(9), P_V(14), 4);
+
+ /* DW2: 0 (sub-op parameter) */
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(14), P_I(0));
+ _E(emit_gfx9_global_store_dword, I9(buf, n),
+ P_V(9), P_V(14), 8);
+
+ /* DW3-4: src = pkt_addr (VRAM) */
+ _E(emit_gfx9_global_store_dword, I9(buf, n),
+ P_V(9), P_V(VR_SAVE_PKT_LO), 12);
+ _E(emit_gfx9_global_store_dword, I9(buf, n),
+ P_V(9), P_V(VR_SAVE_PKT_HI), 16);
+
+ /* DW5-6: dst = out_addr - 20 (GTT slot start) */
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n),
+ P_V(14), P_L(0xFFFFFFECu)); /* -20 */
+ _E(emit_gfx9_v_add_co_u32, I9(buf, n),
+ P_V(15), P_V(14), P_V(VR_SAVE_OUT_LO));
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n),
+ P_V(14), P_L(0xFFFFFFFFu)); /* -1 */
+ _E(emit_gfx9_v_addc_co_u32, I9(buf, n),
+ P_V(16), P_V(14), P_V(VR_SAVE_OUT_HI));
+
+ _E(emit_gfx9_global_store_dword, I9(buf, n),
+ P_V(9), P_V(15), 20);
+ _E(emit_gfx9_global_store_dword, I9(buf, n),
+ P_V(9), P_V(16), 24);
+
+ _E(emit_gfx9_s_waitcnt_vmcnt, I9(buf, n));
+
+ /* --- done counter (all WGs) ------------------------------- */
+ patch_branch(buf, br_no_copy, n);
+
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(7), P_I(1));
+ _E(emit_gfx9_global_atomic_add, I9(buf, n),
+ P_V(7), P_V(1), P_V(7), 4, 1); /* ctl+4 = done_counter */
+ _E(emit_gfx9_s_waitcnt_vmcnt, I9(buf, n));
+
+ /* Last WG check: my_done + 1 == nr_total_wg? */
+ _E(emit_gfx9_v_add_u32, I9(buf, n),
+ P_V(7), P_I(1), P_V(7));
+ _E(emit_gfx9_v_readfirstlane_b32, I9(buf, n), 29, 7);
+ _E(emit_gfx9_s_cmp_eq_u32, I9(buf, n), P_S(29), P_S(28));
+ br_not_last = _BR(emit_gfx9_s_cbranch_scc0, I9(buf, n), 0);
+
+ /* === Last WG - publish counters for CPU === */
+
+ /* Read final claim_counter (atomic add 0, GLC=1) */
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(7), P_I(0));
+ _E(emit_gfx9_global_atomic_add, I9(buf, n),
+ P_V(7), P_V(1), P_V(7), 0, 1);
+ _E(emit_gfx9_s_waitcnt_vmcnt, I9(buf, n));
+
+ /* Store final_sdma_count (ctl+52) and gpu_sdma_ready (ctl+48) */
+ _E(emit_gfx9_global_store_dword, I9(buf, n),
+ P_V(1), P_V(7), 52);
+ _E(emit_gfx9_v_mov_b32_e32, I9(buf, n), P_V(7), P_I(1));
+ _E(emit_gfx9_global_store_dword, I9(buf, n),
+ P_V(1), P_V(7), 48);
+ _E(emit_gfx9_s_waitcnt_vmcnt, I9(buf, n));
+
+ patch_branch(buf, br_not_last, n);
+ patch_branch(buf, br_no_sdma, n);
+
+ _E(emit_gfx9_s_endpgm, I9(buf, n));
+
+ return n * 4;
+}
+
+#endif /* KNOD_HELPERS_IPSEC_FUSED_GFX9_H_ */
diff --git a/drivers/gpu/drm/amd/amdkfd/knod/knod_ipsec.c b/drivers/gpu/drm/amd/amdkfd/knod/knod_ipsec.c
new file mode 100644
index 000000000000..f67e7e4299f2
--- /dev/null
+++ b/drivers/gpu/drm/amd/amdkfd/knod/knod_ipsec.c
@@ -0,0 +1,4273 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/* Copyright (c) 2021 Taehee Yoo <ap420073@gmail.com>
+ * Copyright (c) 2021 Hoyeon Lee <hoyeon.rhee@gmail.com>
+ */
+
+/*
+ * KNOD IPsec (xfrm) full-packet GPU offload.
+ *
+ * Accepts xfrm SAs configured with XFRM_DEV_OFFLOAD_PACKET and runs
+ * ESP parse / SA lookup / AES-GCM decrypt on the GPU. Anti-replay is
+ * handled CPU-side in the NIC dd NAPI (see knod_ipsec_sa_window_check)
+ * after the finish worker has SDMA-copied decrypted inner packets into
+ * the per-queue framework delivery pool (pass_pool). Control plane allocates
+ * per-SA key and GHASH H-power tables in VRAM (via the AES-GCM helpers below)
+ * and mirrors SA state into a GPU-visible SA table consumed by the
+ * fused RX shader.
+ *
+ * RX path: the GPU shader decrypts in place in VRAM and flags per-packet
+ * verdicts (ICV ok / malformed / no SA). A CPU finish worker then SDMA-
+ * bulk-copies successfully decrypted inner packets into per-queue host pages
+ * from the framework delivery pool (pass_pool, page_pool-backed) and pushes
+ * a knod_pass_desc onto the framework per-queue pass_pending ring. The NIC
+ * dd drains that ring from its own NAPI (knod_d2h_drain), which builds the
+ * zero-copy head_frag skb (knod_pass_build_skb) and runs the ipsec
+ * finalisation hook (knod_ipsec_post_copy): RFC 4303 sliding-window
+ * anti-replay, cleartext L3 fix-up and secpath attach. The page recycles
+ * to the pool on skb free.
+ *
+ * The original p2pdma bd ring is only used for NIC netmem lifecycle /
+ * recycle and is completely decoupled from verdict delivery.
+ */
+
+#include <linux/types.h>
+#include <linux/module.h>
+#include <linux/slab.h>
+#include <linux/mutex.h>
+#include <linux/debugfs.h>
+#include <linux/seq_file.h>
+#include <linux/jump_label.h>
+#include <linux/xarray.h>
+#include <linux/netdevice.h>
+#include <linux/etherdevice.h>
+#include <linux/skbuff.h>
+#include <linux/percpu.h>
+#include <linux/kthread.h>
+#include <linux/delay.h>
+#include <linux/ip.h>
+#include <linux/ipv6.h>
+#include <net/ip.h>
+#include <net/dst.h>
+#include <net/neighbour.h>
+#include <net/xfrm.h>
+#include <net/page_pool/helpers.h>
+#include <net/esp.h>
+#include <net/gso.h>
+#include <net/knod.h>
+#include <linux/random.h>
+#include <linux/unaligned.h>
+#include <crypto/aes.h>
+#include <linux/bitops.h>
+#include <linux/string.h>
+#include <drm/ttm/ttm_tt.h>
+
+#include "kfd_priv.h"
+#include "kfd_hsa.h"
+#include "kfd_knod.h"
+#include "knod_ipsec.h"
+#include "ipsec_fused_gfx9.h"
+#include "ipsec_fused_gfx10.h"
+
+/* ====================================================================
+ * AES-GCM core helpers: AES T-tables in VRAM and GHASH H-power table
+ * precomputation. IPsec ESP is the only KNOD GCM consumer, so these
+ * live here rather than in the always-builtin KNOD core.
+ * ====================================================================
+ */
+
+/* AES T-tables: 4 tables * 256 entries * 4 bytes = 4KB, laid out as
+ * T0 | T1 | T2 | T3 in a single VRAM page.
+ */
+#define KNOD_GCM_T_TABLE_SIZE (256 * sizeof(u32))
+#define KNOD_GCM_T_TABLE_COUNT 4
+#define KNOD_GCM_T_TABLES_TOTAL \
+ (KNOD_GCM_T_TABLE_SIZE * KNOD_GCM_T_TABLE_COUNT)
+
+/* GHASH H-power table: H^1 .. H^256, each 16 bytes. */
+#define KNOD_GCM_H_POWER_COUNT 256
+#define KNOD_GCM_H_POWER_SIZE 16
+#define KNOD_GCM_H_TABLE_SIZE \
+ (KNOD_GCM_H_POWER_COUNT * KNOD_GCM_H_POWER_SIZE)
+
+static struct knod_mem *knod_gcm_alloc_tables(struct knod *knod)
+{
+ struct knod_mem *tables;
+ u32 *p;
+ int i;
+
+ tables = knod_alloc_mem(knod, PAGE_SIZE, KFD_IOC_ALLOC_MEM_FLAGS_VRAM);
+ if (IS_ERR(tables))
+ return tables;
+
+ p = (u32 *)tables->kaddr;
+
+ /*
+ * Four rotated versions of aes_enc_tab matching the kernel
+ * enc_quarterround() convention (lib/crypto/aes.c):
+ * T0[x] = aes_enc_tab[x]
+ * Tk[x] = rol32(T0[x], k * 8)
+ * The GPU shader performs one AES round as four table lookups and
+ * four XORs per column.
+ */
+ for (i = 0; i < 256; i++)
+ p[i] = aes_enc_tab[i];
+ for (i = 0; i < 256; i++)
+ p[256 + i] = rol32(aes_enc_tab[i], 8);
+ for (i = 0; i < 256; i++)
+ p[512 + i] = rol32(aes_enc_tab[i], 16);
+ for (i = 0; i < 256; i++)
+ p[768 + i] = rol32(aes_enc_tab[i], 24);
+
+ return tables;
+}
+
+static void knod_gcm_free_tables(struct knod *knod, struct knod_mem *tables)
+{
+ if (tables)
+ knod_free_mem(knod, tables);
+}
+
+/*
+ * Multiply a by b in GF(2^128) with the GCM polynomial
+ * x^128 + x^7 + x^2 + x + 1
+ * Both inputs and the output are stored as two big-endian u64.
+ */
+static void gf128_mul(u64 r[2], const u64 a[2], const u64 b[2])
+{
+ u64 v[2], z[2];
+ int i, j;
+
+ v[0] = a[0];
+ v[1] = a[1];
+ z[0] = 0;
+ z[1] = 0;
+
+ for (i = 0; i < 2; i++) {
+ u64 x = b[i];
+
+ for (j = 63; j >= 0; j--) {
+ if ((x >> j) & 1) {
+ z[0] ^= v[0];
+ z[1] ^= v[1];
+ }
+
+ /* v >>= 1 (128-bit) with GCM reduction */
+ if (v[1] & 1) {
+ v[1] = (v[1] >> 1) | (v[0] << 63);
+ v[0] = (v[0] >> 1) ^ ((u64)0xe1 << 56);
+ } else {
+ v[1] = (v[1] >> 1) | (v[0] << 63);
+ v[0] = v[0] >> 1;
+ }
+ }
+ }
+
+ r[0] = z[0];
+ r[1] = z[1];
+}
+
+static void knod_gcm_precompute_h_table(const u8 *key, int key_len,
+ u8 *h_table_buf)
+{
+ struct aes_enckey enc_key;
+ u8 h_block[AES_BLOCK_SIZE] = {};
+ u64 h[2], h_power[2];
+ int i;
+
+ /* H = AES_K(0^128) */
+ aes_prepareenckey(&enc_key, key, key_len);
+ aes_encrypt(&enc_key, h_block, h_block);
+
+ h[0] = get_unaligned_be64(h_block);
+ h[1] = get_unaligned_be64(h_block + 8);
+
+ /* H^1 = H, then H^(i+1) = H^i * H */
+ h_power[0] = h[0];
+ h_power[1] = h[1];
+
+ for (i = 0; i < KNOD_GCM_H_POWER_COUNT; i++) {
+ u64 next[2];
+
+ put_unaligned_be64(h_power[0],
+ h_table_buf + i * KNOD_GCM_H_POWER_SIZE);
+ put_unaligned_be64(h_power[1],
+ h_table_buf + i * KNOD_GCM_H_POWER_SIZE + 8);
+
+ gf128_mul(next, h_power, h);
+ h_power[0] = next[0];
+ h_power[1] = next[1];
+ }
+
+ memzero_explicit(&enc_key, sizeof(enc_key));
+ memzero_explicit(h_block, sizeof(h_block));
+}
+
+/* Single AQL queue on purpose: CPU parallelism is provided by per-queue
+ * double-buffering through `nr_aql_ring` slots inside this one queue, not
+ * by multiple AQL queues. Multiple AQL queues cause ordering / contention
+ * issues with the fused RX dispatch path - stick to one.
+ */
+/*
+ * Dispatcher idle strategy toggle. Exposed as a debugfs toggle
+ * (knod_ipsec/poll) so it can be flipped mid-run without reloading
+ * the module and without the sysfs-module-param discoverability cost.
+ *
+ * - poll_mode=false (default): when try_rx finds no work,
+ * sleep via usleep_range(20, 100). Lets the CPU idle but adds up to
+ * ~100us latency between a packet landing in the SPSC ring and the
+ * dispatcher picking it up.
+ *
+ * - poll_mode=true: busy-poll with cpu_relax() instead of sleeping. Pins
+ * one CPU core at 100% even when no traffic is flowing, but removes
+ * the handoff latency entirely.
+ *
+ * GPU completion wait (knod_ipsec_dispatch_and_wait) is always polled -
+ * this switch only affects the between-dispatch idle path.
+ */
+static bool knod_ipsec_poll_mode;
+
+/*
+ * RX decrypt output: the shader decrypts ESP payloads into a per-work VRAM
+ * output buffer; the finish worker SDMA-copies the result into a framework
+ * delivery-pool page. For BYPASS/MISS traffic, the shader's Phase 12 writes
+ * SDMA COPY_LINEAR packets directly into the SDMA ring, and the CPU only adds
+ * FENCE + doorbell.
+ */
+
+/*
+ * Parallel RX dispatcher count. Each dispatcher gets its own kaql
+ * AQL queue on the GPU, its own SDMA queue for finalise copies, its
+ * own work_pool slice and pool BOs, and an exclusive contiguous range
+ * of NIC RX queues. Two dispatchers running on kaql[0] and
+ * kaql[1] can execute dispatches in true parallel on disjoint CU
+ * slices, which is the only way to break the single-kaql FIFO
+ * throughput ceiling observed at ~60 Gbps (UDP) / ~41 Gbps (TCP) on
+ * the current test rig.
+ *
+ * The number must be fixed at module_init time because it drives
+ * knod_alloc_ctx() which creates the kaql[]/sdma[] pairs when NOD
+ * attaches. Hot-path code reads priv->nr_dispatchers which is
+ * initialised from priv->knod->queue_cnt.
+ *
+ * Load-time only (0444) - flipping between 1 and N at runtime would
+ * require tearing down / re-creating the knod context, which means
+ * unbinding NOD on the NIC. Reload the module instead.
+ */
+static int nr_dispatch = 1;
+module_param(nr_dispatch, int, 0444);
+MODULE_PARM_DESC(nr_dispatch,
+ "Number of parallel KNOD IPsec dispatchers (1..4, default: 1). Each runs on its own AQL queue for real GPU parallelism.");
+
+static DEFINE_STATIC_KEY_FALSE(ipsec_stats_enabled_key);
+
+#define IPSEC_STAT_INC(s, field) do { \
+ if (static_branch_unlikely(&ipsec_stats_enabled_key)) \
+ this_cpu_inc((s)->field); \
+} while (0)
+#define IPSEC_STAT_ADD(s, field, val) do { \
+ if (static_branch_unlikely(&ipsec_stats_enabled_key)) \
+ this_cpu_add((s)->field, (val)); \
+} while (0)
+
+static struct knod_ipsec_priv *ipsec_priv;
+
+static int knod_ipsec_nod_init(struct knod_dev *knodev);
+static void knod_ipsec_nod_exit(struct knod_dev *knodev);
+static bool knod_ipsec_post_copy(struct knod_dev *knodev, struct sk_buff *skb,
+ const struct knod_pass_desc *desc,
+ int queue_idx);
+static int knod_ipsec_init_shader_gfx9(struct knod *knod);
+static int knod_ipsec_init_shader_gfx10(struct knod *knod);
+static int knod_ipsec_work_pool_alloc(struct knod_ipsec_priv *priv);
+static void knod_ipsec_work_pool_free(struct knod_ipsec_priv *priv);
+static int knod_ipsec_dispatcher(void *arg);
+static void knod_ipsec_dispatcher_drain(struct knod_ipsec_dispatcher *disp);
+static int knod_ipsec_disp_create_all(struct knod_ipsec_priv *priv);
+static void knod_ipsec_disp_destroy_all(struct knod_ipsec_priv *priv);
+static void knod_ipsec_debugfs_init(struct knod_ipsec_priv *priv);
+static void knod_ipsec_debugfs_exit(struct knod_ipsec_priv *priv);
+
+/* ========================================================================
+ * Slot management
+ * ========================================================================
+ */
+
+static int knod_ipsec_find_free_slot(struct knod_ipsec_priv *priv)
+{
+ int i;
+
+ /*
+ * Reuse only fully-freed slots (key_mem cleared by state_free); a
+ * deactivated slot whose free is still pending (an in-flight skb holds
+ * the xfrm_state) would be memset here, leaking its BOs.
+ */
+ for (i = 0; i < KNOD_IPSEC_NR_SA; i++) {
+ if (!priv->slots[i].active && !priv->slots[i].key_mem)
+ return i;
+ }
+ return -ENOSPC;
+}
+
+static struct knod_ipsec_sa_slot *
+knod_ipsec_lookup_slot_by_spi(struct knod_ipsec_priv *priv, u32 spi)
+{
+ return xa_load(&priv->spi_to_slot, spi);
+}
+
+/*
+ * Write (or clear) a slot entry into the GPU-visible SA table via the
+ * kernel mapping. SDMA flush is not required - the VRAM mapping used
+ * here is coherent and the shader re-reads per-dispatch.
+ */
+static void knod_ipsec_write_sa_entry(struct knod_ipsec_priv *priv,
+ int slot_idx,
+ const struct knod_ipsec_sa_slot *slot,
+ struct xfrm_state *x)
+{
+ struct knod_ipsec_sa_entry *e;
+ u32 flags = 0;
+
+ e = (struct knod_ipsec_sa_entry *)priv->sa_table->kaddr;
+ e += slot_idx;
+ memset(e, 0, sizeof(*e));
+
+ if (!slot || !x) {
+ e->active = cpu_to_le32(0);
+ /* Zero per-SA stats on delete */
+ memset((u8 *)priv->sa_table->kaddr +
+ KNOD_IPSEC_STATS_REGION_OFF +
+ slot_idx * KNOD_IPSEC_SA_STATS_SIZE, 0,
+ KNOD_IPSEC_SA_STATS_SIZE);
+ return;
+ }
+
+ if (x->props.flags & XFRM_STATE_ESN)
+ flags |= 1u << 0;
+
+ /* Store SPI in host byte order (little-endian on this platform).
+ * The GPU shader byteswaps the raw BE SPI from the packet header
+ * before comparing, so the table entry must be in the same
+ * host-order form. cpu_to_le32(be32_to_cpu()) converts BE->host->LE
+ * which on LE is a no-op for the numeric value.
+ */
+ e->spi = cpu_to_le32(be32_to_cpu(x->id.spi));
+ e->dir = cpu_to_le32(0);
+ e->family = cpu_to_le32(x->props.family);
+ e->flags = cpu_to_le32(flags);
+ e->key_gpu_addr = cpu_to_le64(slot->key_mem ?
+ slot->key_mem->gaddr : 0);
+ e->htable_gpu_addr = cpu_to_le64(slot->htable_mem ?
+ slot->htable_mem->gaddr : 0);
+ e->t_tables_gpu_addr = cpu_to_le64(priv->t_tables ?
+ priv->t_tables->gaddr : 0);
+ e->replay_bitmap_addr = cpu_to_le64(slot->replay_mem ?
+ slot->replay_mem->gaddr : 0);
+ e->replay_window = cpu_to_le32(x->replay_esn ?
+ x->replay_esn->replay_window : 64);
+
+ if (x->aead) {
+ int key_len = (x->aead->alg_key_len + 7) / 8;
+
+ /* AES-GCM: last 4 bytes of keymat are the salt. */
+ if (key_len >= 4) {
+ memcpy(e->salt,
+ x->aead->alg_key + key_len - 4, 4);
+ key_len -= 4;
+ }
+ e->key_len = cpu_to_le32(key_len);
+ /* nr_rounds: AES-128->10, AES-192->12, AES-256->14 */
+ e->nr_rounds = cpu_to_le32(6 + key_len / 4);
+ }
+
+ e->mode = cpu_to_le32(x->props.mode);
+ e->stats_addr = cpu_to_le64(priv->sa_table->gaddr +
+ KNOD_IPSEC_STATS_REGION_OFF +
+ slot_idx * KNOD_IPSEC_SA_STATS_SIZE);
+ /* Zero per-SA stats on (re)key */
+ memset((u8 *)priv->sa_table->kaddr + KNOD_IPSEC_STATS_REGION_OFF +
+ slot_idx * KNOD_IPSEC_SA_STATS_SIZE, 0,
+ KNOD_IPSEC_SA_STATS_SIZE);
+ e->version = cpu_to_le32(slot->version);
+ e->active = cpu_to_le32(1);
+}
+
+/* ========================================================================
+ * xfrmdev_ops callbacks
+ * ========================================================================
+ */
+
+static int knod_ipsec_xdo_state_add(struct knod_dev *knodev,
+ struct xfrm_state *x,
+ struct netlink_ext_ack *extack)
+{
+ struct knod_ipsec_priv *priv = ipsec_priv;
+ struct knod_ipsec_sa_entry *e_dbg;
+ struct crypto_aes_ctx aes_ctx;
+ struct knod_ipsec_sa_slot *slot;
+ int slot_idx, key_len, err;
+ u32 spi;
+
+ if (!priv || !priv->knod) {
+ NL_SET_ERR_MSG(extack, "knod_ipsec: not initialized");
+ return -ENODEV;
+ }
+
+ /* Only support AEAD AES-GCM in packet offload mode. */
+ if (x->xso.type != XFRM_DEV_OFFLOAD_PACKET) {
+ NL_SET_ERR_MSG(extack, "knod_ipsec: only packet offload supported");
+ return -EINVAL;
+ }
+ if (x->xso.dir != XFRM_DEV_OFFLOAD_IN) {
+ NL_SET_ERR_MSG(extack,
+ "knod_ipsec: only inbound (RX decrypt) offload supported");
+ return -EINVAL;
+ }
+ if (!x->aead ||
+ strcmp(x->aead->alg_name, "rfc4106(gcm(aes))")) {
+ NL_SET_ERR_MSG(extack, "knod_ipsec: only rfc4106(gcm(aes)) supported");
+ return -EINVAL;
+ }
+ if (x->id.proto != IPPROTO_ESP) {
+ NL_SET_ERR_MSG(extack, "knod_ipsec: only ESP supported");
+ return -EINVAL;
+ }
+
+ key_len = (x->aead->alg_key_len + 7) / 8;
+ if (key_len < 4) {
+ NL_SET_ERR_MSG(extack, "knod_ipsec: key too short");
+ return -EINVAL;
+ }
+ key_len -= 4; /* strip salt */
+ if (key_len != 16 && key_len != 24 && key_len != 32) {
+ NL_SET_ERR_MSG(extack, "knod_ipsec: unsupported AES key length");
+ return -EINVAL;
+ }
+
+ spi = be32_to_cpu(x->id.spi);
+
+ mutex_lock(&priv->slot_lock);
+
+ if (knod_ipsec_lookup_slot_by_spi(priv, spi)) {
+ mutex_unlock(&priv->slot_lock);
+ NL_SET_ERR_MSG(extack, "knod_ipsec: SPI already offloaded");
+ return -EEXIST;
+ }
+
+ slot_idx = knod_ipsec_find_free_slot(priv);
+ if (slot_idx < 0) {
+ mutex_unlock(&priv->slot_lock);
+ NL_SET_ERR_MSG(extack, "knod_ipsec: SA table full");
+ return -ENOSPC;
+ }
+
+ slot = &priv->slots[slot_idx];
+ memset(slot, 0, sizeof(*slot));
+ slot->slot_idx = slot_idx;
+ slot->spi = spi;
+
+ /* Expanded round key buffer (VRAM). The GPU shader loads round keys
+ * via s_load_dwordx4 (16 bytes per round), so we expand up front
+ * rather than shipping raw key material. AES-128 is 176B,
+ * AES-256 is 240B; a full PAGE_SIZE allocation leaves room and
+ * avoids the VRAM
+ * 7-page alloc trap.
+ */
+ slot->key_mem = knod_alloc_mem(priv->knod, PAGE_SIZE,
+ KFD_IOC_ALLOC_MEM_FLAGS_VRAM |
+ KFD_IOC_ALLOC_MEM_FLAGS_WRITABLE |
+ KFD_IOC_ALLOC_MEM_FLAGS_COHERENT);
+ if (IS_ERR(slot->key_mem)) {
+ err = PTR_ERR(slot->key_mem);
+ slot->key_mem = NULL;
+ goto err_slot;
+ }
+ err = aes_expandkey(&aes_ctx, x->aead->alg_key, key_len);
+ if (err) {
+ NL_SET_ERR_MSG(extack, "knod_ipsec: AES key expand failed");
+ goto err_key;
+ }
+ /* Store the encryption schedule. Nr+1 round keys x 16 bytes.
+ * The shader reads s[SR_NR_ROUNDS] to know how many rounds.
+ */
+ memcpy(slot->key_mem->kaddr, aes_ctx.key_enc,
+ (aes_ctx.key_length / 4 + 7) * 16);
+ memzero_explicit(&aes_ctx, sizeof(aes_ctx));
+
+ /* H-power table (VRAM) */
+ slot->htable_mem = knod_alloc_mem(priv->knod, KNOD_GCM_H_TABLE_SIZE,
+ KFD_IOC_ALLOC_MEM_FLAGS_VRAM |
+ KFD_IOC_ALLOC_MEM_FLAGS_WRITABLE |
+ KFD_IOC_ALLOC_MEM_FLAGS_COHERENT);
+ if (IS_ERR(slot->htable_mem)) {
+ err = PTR_ERR(slot->htable_mem);
+ slot->htable_mem = NULL;
+ goto err_key;
+ }
+ knod_gcm_precompute_h_table(x->aead->alg_key, key_len,
+ (u8 *)slot->htable_mem->kaddr);
+
+ /* Replay bitmap (VRAM) */
+ slot->replay_mem = knod_alloc_mem(priv->knod, PAGE_SIZE,
+ KFD_IOC_ALLOC_MEM_FLAGS_VRAM |
+ KFD_IOC_ALLOC_MEM_FLAGS_WRITABLE |
+ KFD_IOC_ALLOC_MEM_FLAGS_COHERENT);
+ if (IS_ERR(slot->replay_mem)) {
+ err = PTR_ERR(slot->replay_mem);
+ slot->replay_mem = NULL;
+ goto err_htable;
+ }
+ memset(slot->replay_mem->kaddr, 0, PAGE_SIZE);
+
+ slot->x = x;
+ slot->version = 1;
+ slot->active = true;
+
+ knod_ipsec_write_sa_entry(priv, slot_idx, slot, x);
+
+ err = xa_insert(&priv->spi_to_slot, spi, slot, GFP_KERNEL);
+ if (err)
+ goto err_replay;
+
+ x->xso.offload_handle = (unsigned long)slot;
+
+ mutex_unlock(&priv->slot_lock);
+
+ this_cpu_inc(priv->stats->sa_add);
+
+ e_dbg = (struct knod_ipsec_sa_entry *)priv->sa_table->kaddr + slot_idx;
+
+ pr_info("knod_ipsec: SA added spi=0x%08x slot=%d dir=%d\n",
+ spi, slot_idx, x->xso.dir);
+ pr_info(" sa_entry: spi_le=0x%08x active=%u nr_rounds=%u key_len=%u\n",
+ le32_to_cpu(e_dbg->spi), le32_to_cpu(e_dbg->active),
+ le32_to_cpu(e_dbg->nr_rounds), le32_to_cpu(e_dbg->key_len));
+ pr_info(" sa_entry: key_addr=0x%llx htable_addr=0x%llx salt=%*ph\n",
+ le64_to_cpu(e_dbg->key_gpu_addr),
+ le64_to_cpu(e_dbg->htable_gpu_addr),
+ 4, e_dbg->salt);
+ return 0;
+
+err_replay:
+ knod_free_mem(priv->knod, slot->replay_mem);
+err_htable:
+ knod_free_mem(priv->knod, slot->htable_mem);
+err_key:
+ knod_free_mem(priv->knod, slot->key_mem);
+err_slot:
+ memset(slot, 0, sizeof(*slot));
+ mutex_unlock(&priv->slot_lock);
+ return err;
+}
+
+static void knod_ipsec_xdo_state_delete(struct knod_dev *knodev,
+ struct xfrm_state *x)
+{
+ struct knod_ipsec_priv *priv = ipsec_priv;
+ struct knod_ipsec_sa_slot *slot;
+ u32 spi;
+
+ if (!priv)
+ return;
+
+ spi = be32_to_cpu(x->id.spi);
+
+ mutex_lock(&priv->slot_lock);
+ slot = knod_ipsec_lookup_slot_by_spi(priv, spi);
+ if (!slot) {
+ mutex_unlock(&priv->slot_lock);
+ return;
+ }
+
+ /* Deactivate first so GPU shaders skip this slot on next dispatch. */
+ slot->active = false;
+ slot->version++;
+ knod_ipsec_write_sa_entry(priv, slot->slot_idx, NULL, NULL);
+
+ xa_erase(&priv->spi_to_slot, spi);
+
+ mutex_unlock(&priv->slot_lock);
+
+ this_cpu_inc(priv->stats->sa_del);
+}
+
+static void knod_ipsec_xdo_state_free(struct knod_dev *knodev,
+ struct xfrm_state *x)
+{
+ struct knod_ipsec_priv *priv = ipsec_priv;
+ struct knod_ipsec_sa_slot *slot;
+
+ if (!priv)
+ return;
+
+ slot = (struct knod_ipsec_sa_slot *)x->xso.offload_handle;
+ if (!slot)
+ return;
+
+ mutex_lock(&priv->slot_lock);
+
+ if (slot->replay_mem)
+ knod_free_mem(priv->knod, slot->replay_mem);
+ if (slot->htable_mem)
+ knod_free_mem(priv->knod, slot->htable_mem);
+ if (slot->key_mem)
+ knod_free_mem(priv->knod, slot->key_mem);
+
+ memset(slot, 0, sizeof(*slot));
+ x->xso.offload_handle = 0;
+
+ mutex_unlock(&priv->slot_lock);
+}
+
+static bool knod_ipsec_xdo_offload_ok(struct knod_dev *knodev,
+ struct sk_buff *skb,
+ struct xfrm_state *x)
+{
+ /* We support only ESP / AES-GCM / packet offload; state_add
+ * already filtered unsupported cases. Offload OK for matching SA.
+ */
+ return x->xso.type == XFRM_DEV_OFFLOAD_PACKET &&
+ x->id.proto == IPPROTO_ESP && x->aead;
+}
+
+static void knod_ipsec_xdo_state_advance_esn(struct knod_dev *knodev,
+ struct xfrm_state *x)
+{
+ struct knod_ipsec_priv *priv = ipsec_priv;
+ struct knod_ipsec_sa_slot *slot;
+ struct knod_ipsec_sa_entry *e;
+
+ if (!priv || !x->replay_esn)
+ return;
+ slot = (struct knod_ipsec_sa_slot *)x->xso.offload_handle;
+ if (!slot || !slot->active)
+ return;
+
+ e = (struct knod_ipsec_sa_entry *)priv->sa_table->kaddr;
+ e += slot->slot_idx;
+
+ /* xfrm calls advance_esn whenever the inbound ESN high-32 bits
+ * change. The GPU reads seq_hi/seq_last via VMEM (global_load) so
+ * the COHERENT VRAM mapping ensures visibility without SDMA.
+ */
+ WRITE_ONCE(e->seq_hi, cpu_to_le32(x->replay_esn->seq_hi));
+ WRITE_ONCE(e->seq_last, cpu_to_le64(
+ ((u64)x->replay_esn->seq_hi << 32) |
+ (u64)x->replay_esn->seq));
+ /* Ensure both fields are visible to GPU before returning. */
+ wmb();
+}
+
+static void knod_ipsec_xdo_state_update_stats(struct knod_dev *knodev,
+ struct xfrm_state *x)
+{
+ struct knod_ipsec_priv *priv = ipsec_priv;
+ struct knod_ipsec_sa_slot *slot;
+ struct knod_ipsec_sa_gpu_stats *gs;
+ u32 spi;
+
+ if (!priv || !priv->sa_table)
+ return;
+
+ spi = be32_to_cpu(x->id.spi);
+ slot = xa_load(&priv->spi_to_slot, spi);
+ if (!slot || !slot->active)
+ return;
+
+ /* Read GPU-side per-SA counters (atomically updated by shader). */
+ gs = (struct knod_ipsec_sa_gpu_stats *)
+ ((u8 *)priv->sa_table->kaddr + KNOD_IPSEC_STATS_REGION_OFF +
+ slot->slot_idx * KNOD_IPSEC_SA_STATS_SIZE);
+
+ x->curlft.packets = le64_to_cpu(READ_ONCE(gs->rx_packets));
+ x->curlft.bytes = le64_to_cpu(READ_ONCE(gs->rx_bytes));
+}
+
+/* ========================================================================
+ * RX delivery queues (host SPSC descriptor ring; pages from pass_pool)
+ * ========================================================================
+ */
+
+/*
+ * RFC 4303 anti-replay window check + update (single-writer, lockless).
+ * Returns true if the packet is accepted (not a replay and in window),
+ * false if it must be dropped.
+ *
+ * Caller guarantees that RSS pins the SA to a single RX queue, so the
+ * only writer to `win` on this CPU is the NAPI for this queue. The
+ * function both tests and updates - NIC dd calls it exactly once per
+ * desc, either to accept or to drop.
+ */
+static inline bool knod_ipsec_sa_window_check(struct knod_ipsec_sa_window *win,
+ u64 seq)
+{
+ const unsigned int nwords = KNOD_IPSEC_CPU_REPLAY_WORDS;
+ unsigned int diff;
+ unsigned int w;
+ u64 bit;
+ int i;
+
+ if (seq == 0)
+ return false;
+
+ if (seq > win->top_seq) {
+ diff = (unsigned int)(seq - win->top_seq);
+ if (diff >= KNOD_IPSEC_CPU_REPLAY_BITS) {
+ memset(win->bitmap, 0, sizeof(win->bitmap));
+ } else {
+ unsigned int word_shift = diff >> 6; /* diff / 64 */
+ unsigned int bit_shift = diff & 63; /* diff % 64 */
+
+ /* Shift the whole bitmap left by `diff` bits. bit at
+ * position p in bitmap[w] moves to p + diff. Work
+ * from the high word downwards so we don't overwrite
+ * source words before reading them.
+ */
+ if (bit_shift == 0) {
+ for (i = (int)nwords - 1; i >= 0; i--) {
+ int src = i - (int)word_shift;
+
+ win->bitmap[i] = (src >= 0)
+ ? win->bitmap[src] : 0;
+ }
+ } else {
+ for (i = (int)nwords - 1; i >= 0; i--) {
+ int src_hi = i - (int)word_shift;
+ int src_lo = src_hi - 1;
+ u64 hi = 0, lo = 0;
+
+ if (src_hi >= 0)
+ hi = win->bitmap[src_hi] <<
+ bit_shift;
+ if (src_lo >= 0)
+ lo = win->bitmap[src_lo] >>
+ (64 - bit_shift);
+ win->bitmap[i] = hi | lo;
+ }
+ }
+ }
+ win->top_seq = seq;
+ win->bitmap[0] |= 1ull;
+ return true;
+ }
+
+ diff = (unsigned int)(win->top_seq - seq);
+ if (diff >= KNOD_IPSEC_CPU_REPLAY_BITS)
+ return false; /* too old */
+ w = diff >> 6;
+ bit = 1ull << (diff & 63);
+
+ if (win->bitmap[w] & bit)
+ return false; /* replay */
+ win->bitmap[w] |= bit;
+ return true;
+}
+
+static int knod_ipsec_rxq_init_all(struct knod_ipsec_priv *priv)
+{
+ /* One delivery queue per NIC RX queue, capped at KNOD_SPSC_MAX to
+ * match the rest of the NOD/offmem/bd ring infrastructure. Per-queue
+ * delivery now flows through the framework pass_pending ring, so this
+ * only records the queue count for the finish-worker bounds check.
+ */
+ int nr = priv->knodev && priv->knodev->netdev
+ ? (int)priv->knodev->netdev->num_rx_queues : 1;
+
+ if (nr > KNOD_SPSC_MAX)
+ nr = KNOD_SPSC_MAX;
+ if (nr < 1)
+ nr = 1;
+
+ priv->nr_rxq = nr;
+ return 0;
+}
+
+static void knod_ipsec_rxq_exit_all(struct knod_ipsec_priv *priv)
+{
+ priv->nr_rxq = 0;
+}
+
+/* ========================================================================
+ * NOD init / exit - allocate SA table and shared T-tables
+ * ========================================================================
+ */
+
+static int knod_ipsec_nod_init(struct knod_dev *knodev)
+{
+ struct amdgpu_device *adev;
+ struct knod_ipsec_priv *priv;
+ int err;
+
+ if (ipsec_priv) {
+ pr_warn("knod_ipsec: priv already initialized\n");
+ return -EBUSY;
+ }
+
+ /*
+ * Pin the module while IPsec is the selected feature: the core calls
+ * into these ops, so it must not be unloaded until feature->none.
+ * (No-op when built in - THIS_MODULE is NULL.)
+ */
+ if (!try_module_get(THIS_MODULE))
+ return -ENODEV;
+
+ /* kvzalloc because priv has grown large (slots[NR_SA] each with
+ * win[KNOD_SPSC_MAX] sliding windows - a couple of MB now). kzalloc
+ * may succeed but kvzalloc falls back to vmalloc if kmalloc can't
+ * find contiguous pages, which is safer under memory pressure.
+ */
+ priv = kvzalloc_obj(*priv, GFP_KERNEL);
+ if (!priv) {
+ module_put(THIS_MODULE);
+ return -ENOMEM;
+ }
+
+ priv->stats = alloc_percpu(struct knod_ipsec_stats);
+ if (!priv->stats) {
+ err = -ENOMEM;
+ goto err_priv;
+ }
+
+ priv->knod = knodev->accel->priv;
+ if (!priv->knod) {
+ pr_err("knod_ipsec: no knod context (NOD not attached?)\n");
+ err = -ENODEV;
+ goto err_stats;
+ }
+ priv->knodev = knodev;
+ mutex_init(&priv->slot_lock);
+ xa_init(&priv->spi_to_slot);
+
+ /* Number of parallel dispatchers = number of AQL/SDMA queue pairs
+ * that the accel was allocated with, capped at the ipsec module's
+ * compile-time limit. Falls back to 1 when the knod layer reports
+ * 0 or the clamp leaves nothing usable.
+ */
+ priv->nr_dispatchers = priv->knod->queue_cnt;
+ if (priv->nr_dispatchers > KNOD_IPSEC_MAX_DISPATCHERS)
+ priv->nr_dispatchers = KNOD_IPSEC_MAX_DISPATCHERS;
+ if (priv->nr_dispatchers < 1)
+ priv->nr_dispatchers = 1;
+ priv->pkt_batch = KNOD_IPSEC_PKT_BATCH;
+
+ priv->t_tables = knod_gcm_alloc_tables(priv->knod);
+ if (IS_ERR(priv->t_tables)) {
+ err = PTR_ERR(priv->t_tables);
+ priv->t_tables = NULL;
+ goto err_ctx;
+ }
+
+ adev = priv->knod->process->pdds[0]->dev->adev;
+
+ if (adev->asic_type == CHIP_VEGA10 ||
+ adev->asic_type == CHIP_VEGA20) {
+ priv->isa_version = 9;
+ priv->shader_size = knod_ipsec_init_shader_gfx9(priv->knod);
+ } else {
+ priv->isa_version = 10;
+ priv->shader_size = knod_ipsec_init_shader_gfx10(priv->knod);
+ }
+
+ err = knod_ipsec_work_pool_alloc(priv);
+ if (err) {
+ knod_ipsec_work_pool_free(priv);
+ goto err_ttables;
+ }
+
+ /* Persistent KAT scratch BO. Sized for the largest nr
+ * (KNOD_IPSEC_PKT_BATCH=64 bd slots at 64B stride, plus a matching
+ * per-packet dummy area at 128B stride for IPv6 ESP minimum).
+ * 4 pages = 16KB to fit max batch. Keeping it as a single long-lived
+ * BO avoids the per-KAT alloc/free dance that retriggers the
+ * multi-BO mapping bug.
+ */
+ priv->kat_scratch = knod_alloc_mem(priv->knod, PAGE_SIZE * 4,
+ KFD_IOC_ALLOC_MEM_FLAGS_VRAM |
+ KFD_IOC_ALLOC_MEM_FLAGS_WRITABLE |
+ KFD_IOC_ALLOC_MEM_FLAGS_COHERENT);
+ if (IS_ERR(priv->kat_scratch)) {
+ err = PTR_ERR(priv->kat_scratch);
+ priv->kat_scratch = NULL;
+ goto err_works;
+ }
+
+ /*
+ * Allocate sa_table LAST. The GTT/VRAM multi-BO mapping bug
+ * (memory/gtt_multi_bo_bug.md) makes some BOs allocated mid-init fail
+ * to bind their backing pages at the returned gaddr even though the
+ * CPU kaddr is valid. Empirically, BOs allocated after all other init
+ * BOs (knod ctx, t_tables, shader init, work pool, kat_scratch) bind
+ * reliably. Allocating sa_table last sidesteps this.
+ */
+ /* DIAGNOSTIC bisect step 1b: BO size is still ENLARGED, but rounded up
+ * to the next power-of-two (8 pages = 32 KB) instead of the natural
+ * 7 pages = 28 KB. Exact-7-page (0x7000) sa_table alloc empirically
+ * corrupts the subsequent dispatch (kernarg TCP fault at 0x01af5000);
+ * 6 pages (0x6000) is fine. Suspicion: amdgpu VRAM/GTT path handles
+ * non-power-of-two sizes differently for mappings of this scale.
+ */
+ /* RX delivery desc rings (host kvmalloc; payload pages come from the
+ * framework pass_pool, no per-queue GTT BO). Set up before sa_table,
+ * which must stay the last BO bound on this KFD process VM per the
+ * "sa_table last" constraint in memory/gtt_multi_bo_bug.md.
+ */
+ err = knod_ipsec_rxq_init_all(priv);
+ if (err)
+ goto err_kat_scratch;
+
+ priv->sa_table = knod_alloc_mem(priv->knod,
+ ALIGN(KNOD_IPSEC_SA_BO_SIZE,
+ 8 * PAGE_SIZE),
+ KFD_IOC_ALLOC_MEM_FLAGS_VRAM |
+ KFD_IOC_ALLOC_MEM_FLAGS_WRITABLE |
+ KFD_IOC_ALLOC_MEM_FLAGS_COHERENT);
+ if (IS_ERR(priv->sa_table)) {
+ err = PTR_ERR(priv->sa_table);
+ priv->sa_table = NULL;
+ goto err_rxq;
+ }
+ memset(priv->sa_table->kaddr, 0, KNOD_IPSEC_SA_BO_SIZE);
+ pr_info("knod_ipsec: sa_table gaddr=0x%llx kaddr=%p size=%u\n",
+ priv->sa_table->gaddr, priv->sa_table->kaddr,
+ (u32)KNOD_IPSEC_SA_BO_SIZE);
+
+ err = knod_ipsec_disp_create_all(priv);
+ if (err)
+ goto err_sa_table;
+
+ ipsec_priv = priv;
+ knodev->post_copy = knod_ipsec_post_copy;
+ knod_ipsec_debugfs_init(priv);
+ pr_info("knod_ipsec: initialized on %s (gfx%d, single dispatcher)\n",
+ netdev_name(knodev->netdev), priv->isa_version);
+ return 0;
+
+err_sa_table:
+ knod_free_mem(priv->knod, priv->sa_table);
+ priv->sa_table = NULL;
+err_rxq:
+ knod_ipsec_rxq_exit_all(priv);
+err_kat_scratch:
+ knod_free_mem(priv->knod, priv->kat_scratch);
+err_works:
+ knod_ipsec_work_pool_free(priv);
+err_ttables:
+ knod_gcm_free_tables(priv->knod, priv->t_tables);
+err_ctx:
+ priv->knod = NULL;
+err_stats:
+ free_percpu(priv->stats);
+err_priv:
+ kvfree(priv);
+ module_put(THIS_MODULE);
+ return err;
+}
+
+static void knod_ipsec_nod_exit(struct knod_dev *knodev)
+{
+ struct knod_ipsec_priv *priv = ipsec_priv;
+ int i;
+
+ if (!priv)
+ return;
+
+ knod_ipsec_debugfs_exit(priv);
+
+ /*
+ * Publish the NULL and wait a grace period before freeing: the
+ * softirq RX reader (knod_ipsec_post_copy) samples ipsec_priv and must
+ * not touch it once it is freed. Runs under rtnl (feature_set/detach),
+ * so the synchronize_net() is expedited.
+ */
+ knodev->post_copy = NULL;
+ WRITE_ONCE(ipsec_priv, NULL);
+ synchronize_net();
+
+ knod_ipsec_disp_destroy_all(priv);
+
+ /* Free any leftover slots (should be none if xfrm tore down SAs). */
+ mutex_lock(&priv->slot_lock);
+ for (i = 0; i < KNOD_IPSEC_NR_SA; i++) {
+ struct knod_ipsec_sa_slot *slot = &priv->slots[i];
+
+ if (!slot->active && !slot->key_mem)
+ continue;
+ if (slot->replay_mem)
+ knod_free_mem(priv->knod, slot->replay_mem);
+ if (slot->htable_mem)
+ knod_free_mem(priv->knod, slot->htable_mem);
+ if (slot->key_mem)
+ knod_free_mem(priv->knod, slot->key_mem);
+ memset(slot, 0, sizeof(*slot));
+ }
+ xa_destroy(&priv->spi_to_slot);
+ mutex_unlock(&priv->slot_lock);
+
+ knod_ipsec_work_pool_free(priv);
+
+ knod_ipsec_rxq_exit_all(priv);
+
+ if (priv->kat_scratch) {
+ knod_free_mem(priv->knod, priv->kat_scratch);
+ priv->kat_scratch = NULL;
+ }
+ if (priv->sa_table)
+ knod_free_mem(priv->knod, priv->sa_table);
+ if (priv->t_tables)
+ knod_gcm_free_tables(priv->knod, priv->t_tables);
+ /* knod context is owned by NOD core (knod_attach), don't free */
+ priv->knod = NULL;
+ if (priv->stats)
+ free_percpu(priv->stats);
+
+ kvfree(priv);
+ module_put(THIS_MODULE);
+ pr_info("knod_ipsec: torn down\n");
+}
+
+/* True while an offloaded xfrm SA is still bound to this accel. */
+static bool knod_ipsec_nod_busy(struct knod_dev *knodev)
+{
+ struct knod_ipsec_priv *priv = ipsec_priv;
+
+ if (!priv)
+ return false;
+ return !xa_empty(&priv->spi_to_slot);
+}
+
+static int knod_ipsec_disp_create_all(struct knod_ipsec_priv *priv)
+{
+ int d, i;
+
+ for (d = 0; d < priv->nr_dispatchers; d++) {
+ struct knod_ipsec_dispatcher *disp = &priv->disp[d];
+ struct task_struct *p;
+
+ p = kthread_create(knod_ipsec_dispatcher, disp,
+ "knod_ipsec-d%d", d);
+ if (IS_ERR(p)) {
+ pr_warn("knod_ipsec: failed to create dispatcher %d: %ld\n",
+ d, PTR_ERR(p));
+ for (i = 0; i < d; i++) {
+ kthread_stop(priv->disp[i].kthread);
+ priv->disp[i].kthread = NULL;
+ }
+ return PTR_ERR(p);
+ }
+ kthread_park(p);
+ disp->kthread = p;
+ }
+ return 0;
+}
+
+static void knod_ipsec_disp_destroy_all(struct knod_ipsec_priv *priv)
+{
+ int d;
+
+ for (d = 0; d < priv->nr_dispatchers; d++) {
+ struct knod_ipsec_dispatcher *disp = &priv->disp[d];
+
+ if (disp->kthread) {
+ kthread_stop(disp->kthread);
+ disp->kthread = NULL;
+ }
+ }
+}
+
+static void knod_ipsec_nod_start(struct knod_dev *knodev)
+{
+ struct knod_ipsec_priv *priv = ipsec_priv;
+ int base_rx, rem_rx;
+ int off_rx;
+ struct knod *knod;
+ int d, nr_rxq;
+
+ if (!priv || READ_ONCE(priv->running))
+ return;
+
+ knod = priv->knod;
+
+ /* Partition RX queues across dispatchers. Each disp
+ * owns a contiguous range of queues so the per-SPSC single-
+ * consumer invariant holds without cross-dispatcher locking.
+ * Leftover (when the total isn't a multiple of nr_dispatchers)
+ * lands on the first few disps, one extra each.
+ */
+ nr_rxq = knodev->netdev ? knodev->netdev->real_num_rx_queues : 0;
+ if (nr_rxq > KNOD_SPSC_MAX)
+ nr_rxq = KNOD_SPSC_MAX;
+ if (nr_rxq > knod->channels)
+ nr_rxq = knod->channels;
+ if (nr_rxq < 0)
+ nr_rxq = 0;
+
+ base_rx = nr_rxq / priv->nr_dispatchers;
+ rem_rx = nr_rxq % priv->nr_dispatchers;
+ off_rx = 0;
+
+ for (d = 0; d < priv->nr_dispatchers; d++) {
+ struct knod_ipsec_dispatcher *disp = &priv->disp[d];
+ int n_rx = base_rx + (d < rem_rx ? 1 : 0);
+
+ disp->rxq_first = off_rx;
+ disp->rxq_count = n_rx;
+ off_rx += n_rx;
+ }
+
+ WRITE_ONCE(priv->running, true);
+ for (d = 0; d < priv->nr_dispatchers; d++) {
+ struct knod_ipsec_dispatcher *disp = &priv->disp[d];
+
+ if (!disp->kthread)
+ continue;
+ kthread_unpark(disp->kthread);
+ pr_info("knod_ipsec: disp[%d] started rxq[%d..%d) kaql=%d\n",
+ d, disp->rxq_first,
+ disp->rxq_first + disp->rxq_count,
+ disp->kaql_idx);
+ }
+}
+
+static void knod_ipsec_nod_stop(struct knod_dev *knodev)
+{
+ struct knod_ipsec_priv *priv = ipsec_priv;
+ int d;
+
+ if (!priv || !READ_ONCE(priv->running))
+ return;
+
+ WRITE_ONCE(priv->running, false);
+ for (d = 0; d < priv->nr_dispatchers; d++) {
+ struct knod_ipsec_dispatcher *disp = &priv->disp[d];
+
+ if (!disp->kthread)
+ continue;
+ kthread_park(disp->kthread);
+ }
+}
+
+/* ========================================================================
+ * Phase 3: fused RX shader load + dispatch
+ * ========================================================================
+ *
+ * The fused RX shader parses ESP, resolves the SA via SPI lookup, and
+ * (in follow-up work) runs AES-GCM decrypt/ICV. The resulting inner
+ * packet lives in VRAM; the CPU finish worker SDMA-copies it into a
+ * per-queue delivery-pool page and publishes a knod_pass_desc onto the
+ * framework pass_pending ring. Anti-replay and skb build happen in the NIC
+ * dd NAPI consumer - the shader does neither. See knod_ipsec.h for the
+ * full data-flow description.
+ */
+
+static int knod_ipsec_init_shader_gfx9(struct knod *knod)
+{
+ struct compute_pgm_rsrc1 rsrc1 = {};
+ struct compute_pgm_rsrc2 rsrc2 = {};
+ struct kernel_descriptor *kd = knod->kernels[0]->kaddr;
+ struct code_properties props = {};
+ int shader_size;
+
+ memset(kd, 0, sizeof(*kd));
+ kd->kernel_code_entry_byte_offset = 1024;
+ kd->group_segment_fixed_size = KNOD_GCM_T_TABLES_TOTAL;
+
+ /* VGPRs: (gran+1)*4. Need v0-v42 (43 VGPRs) -> gran=12 -> 52.
+ * SGPRs: (gran+1)*8. Need s0-s59 (SR_RK2) -> gran=7 -> 64.
+ */
+ rsrc1.granulated_workitem_vgpr_count = 12;
+ rsrc1.granulated_wavefront_sgpr_count = 7;
+ rsrc1.float_denorm_mode_32 = 3;
+ rsrc1.float_denorm_mode_16_64 = 3;
+ rsrc1.enable_dx10_clamp = 1;
+ rsrc1.enable_ieee_mode = 1;
+
+ rsrc2.user_sgpr_count = 15;
+ rsrc2.enable_sgpr_workgroup_id_x = 1;
+ rsrc2.enable_sgpr_workgroup_id_y = 1;
+ rsrc2.enable_sgpr_workgroup_id_z = 1;
+ rsrc2.granulated_lds_size = 8;
+
+ props.enable_sgpr_private_segment_buffer = 1;
+ props.enable_sgpr_dispatch_ptr = 1;
+ props.enable_sgpr_queue_ptr = 1;
+ props.enable_sgpr_kernarg_segment_ptr = 1;
+ props.enable_sgpr_dispatch_id = 1;
+ props.enable_sgpr_flat_scratch_init = 1;
+ props.enable_sgpr_private_segment_size = 1;
+
+ memcpy(&kd->compute_pgm_rsrc1, &rsrc1, sizeof(rsrc1));
+ memcpy(&kd->compute_pgm_rsrc2, &rsrc2, sizeof(rsrc2));
+ memcpy(&kd->code_properties, &props, sizeof(props));
+
+ memset(knod->kernels[0]->kaddr + kd->kernel_code_entry_byte_offset,
+ 0, (PAGE_SIZE << 4) - kd->kernel_code_entry_byte_offset);
+ shader_size = kfd_ipsec_gen_fused_shader_gfx9(
+ knod->kernels[0]->kaddr + kd->kernel_code_entry_byte_offset);
+ pr_debug("knod_ipsec: GFX9 RX shader generated, %d bytes\n",
+ shader_size);
+
+ return shader_size;
+}
+
+static int knod_ipsec_init_shader_gfx10(struct knod *knod)
+{
+ struct compute_pgm_rsrc1 rsrc1 = {};
+ struct compute_pgm_rsrc2 rsrc2 = {};
+ struct kernel_descriptor *kd = knod->kernels[0]->kaddr;
+ struct code_properties props = {};
+ int shader_size;
+ u32 rsrc1_raw;
+
+ memset(kd, 0, sizeof(*kd));
+ kd->kernel_code_entry_byte_offset = 1024;
+ kd->group_segment_fixed_size = KNOD_GCM_T_TABLES_TOTAL;
+
+ /* VGPRs: Wave64, granularity=4. (12+1)*4 = 52 VGPRs.
+ * Shader uses v0-v42 (VR_SAVE_ESP_OFF).
+ *
+ * SGPRs: RDNA2 ignores granulated_wavefront_sgpr_count -
+ * SGPRs come from a flat 106-entry pool. Field is reserved
+ * and must be 0 (non-zero corrupts RSRC1 interpretation on
+ * some RDNA2 steppings, causing SQC inst-fetch faults).
+ */
+ rsrc1.granulated_workitem_vgpr_count = 12;
+ rsrc1.granulated_wavefront_sgpr_count = 0;
+ rsrc1.float_denorm_mode_32 = 3;
+ rsrc1.float_denorm_mode_16_64 = 3;
+ rsrc1.enable_dx10_clamp = 1;
+ rsrc1.enable_ieee_mode = 1;
+ rsrc1.wgp_mode = 0;
+ rsrc1.mem_ordered = 1;
+
+ rsrc2.user_sgpr_count = 15;
+ rsrc2.enable_sgpr_workgroup_id_x = 1;
+ rsrc2.enable_sgpr_workgroup_id_y = 1;
+ rsrc2.enable_sgpr_workgroup_id_z = 1;
+ rsrc2.granulated_lds_size = 8;
+
+ props.enable_sgpr_private_segment_buffer = 1;
+ props.enable_sgpr_dispatch_ptr = 1;
+ props.enable_sgpr_queue_ptr = 1;
+ props.enable_sgpr_kernarg_segment_ptr = 1;
+ props.enable_sgpr_dispatch_id = 1;
+ props.enable_sgpr_flat_scratch_init = 1;
+ props.enable_sgpr_private_segment_size = 1;
+
+ memcpy(&kd->compute_pgm_rsrc1, &rsrc1, sizeof(rsrc1));
+ memcpy(&kd->compute_pgm_rsrc2, &rsrc2, sizeof(rsrc2));
+ memcpy(&kd->code_properties, &props, sizeof(props));
+
+ memset(knod->kernels[0]->kaddr + kd->kernel_code_entry_byte_offset,
+ 0, (PAGE_SIZE << 4) - kd->kernel_code_entry_byte_offset);
+ shader_size = kfd_ipsec_gen_fused_shader_gfx10(
+ knod->kernels[0]->kaddr + kd->kernel_code_entry_byte_offset);
+ memcpy(&rsrc1_raw, &kd->compute_pgm_rsrc1, 4);
+ pr_info("knod_ipsec: GFX10 shader %d bytes, RSRC1=0x%08x (vgpr=%u sgpr=%u wgp=%u mem=%u)\n",
+ shader_size, rsrc1_raw,
+ rsrc1_raw & 0x3F,
+ (rsrc1_raw >> 6) & 0xF,
+ (rsrc1_raw >> 29) & 1,
+ (rsrc1_raw >> 30) & 1);
+
+ return shader_size;
+}
+
+/*
+ * Single-dispatcher AQL machinery.
+ *
+ * One AQL queue (kaql[0]), one in-flight dispatch, one kthread that owns
+ * everything. The dispatcher drains per-queue NIC RX SPSC bd rings, builds one
+ * dispatch, kicks the GPU, spins on the completion signal, finalises, and
+ * loops. No lock on the hot path.
+ *
+ * KAT paths park the dispatcher with kthread_park() for exclusive ownership
+ * of the queue and the work slot.
+ */
+
+static void knod_ipsec_fill_dispatch(struct knod *knod,
+ struct knod_ipsec_work *work,
+ struct knod_dispatch_params *p)
+{
+ p->workgroup_size_x = 256;
+ p->grid_size_x = 256;
+ p->grid_size_y = max(work->nr_packets, 1);
+ p->private_segment_size = 0;
+ p->group_segment_size = KNOD_GCM_T_TABLES_TOTAL;
+ p->kernel_object = (u64)knod->kernels[0]->gaddr;
+ p->kernarg_address = work->param.gaddr;
+}
+
+/*
+ * Prepare the dispatcher's single work slot to run the fused RX shader
+ * over `sub[0..nr)`. Must be called from dispatcher context only (or while
+ * the dispatcher is parked - e.g. from KAT). `bds` may be NULL for the
+ * in-kernel KAT path; in that case the KAT owns out_addr and this helper
+ * leaves it untouched.
+ */
+static void knod_ipsec_prepare_rx_dispatch(struct knod_ipsec_priv *priv,
+ struct knod_ipsec_work *work,
+ struct knod_ipsec_fused_sub *sub,
+ struct spsc_bd **bds, int nr,
+ struct napi_struct *napi,
+ int queue_idx)
+{
+ struct knod_ipsec_fused_param *param;
+ struct amd_signal *signal;
+
+ if (nr > (int)READ_ONCE(priv->pkt_batch))
+ nr = (int)READ_ONCE(priv->pkt_batch);
+
+ param = (struct knod_ipsec_fused_param *)work->param.kaddr;
+ memset(param, 0, sizeof(*param));
+ param->sa_table_addr = cpu_to_le64(priv->sa_table->gaddr);
+ param->t_tables_addr = cpu_to_le64(priv->t_tables->gaddr);
+ param->nr_sa = cpu_to_le32(KNOD_IPSEC_NR_SA);
+ memcpy(param->sub, sub, sizeof(sub[0]) * nr);
+
+ /* Patch out_addr to point to the per-work decrypt output buffer
+ * instead of pkt_addr. AES-CTR writes plaintext here; GHASH reads
+ * the original ciphertext from pkt_addr. In-place would corrupt
+ * the ciphertext before GHASH could read it.
+ *
+ * Skip for KAT (bds==NULL): the KAT manages out_addr itself.
+ */
+ if (bds) {
+ int pi;
+
+ for (pi = 0; pi < nr; pi++)
+ param->sub[pi].out_addr = cpu_to_le64(
+ work->rx_out_gaddr +
+ (u64)pi * KNOD_IPSEC_DECRYPT_PKT_SIZE);
+ }
+
+ work->nr_packets = nr;
+ work->rx_napi = napi;
+ work->rx_queue_idx = queue_idx;
+ if (bds) {
+ int bi;
+
+ for (bi = 0; bi < nr; bi++) {
+ work->rx_bds[bi] = bds[bi];
+ /* Single-queue legacy/KAT path: all packets belong
+ * to queue_idx. Multi-queue dispatches populate
+ * rx_pkt_queue[] directly in try_rx and never call
+ * this function.
+ */
+ work->rx_pkt_queue[bi] = (u8)queue_idx;
+ }
+ } else {
+ memset(work->rx_bds, 0, sizeof(work->rx_bds[0]) * nr);
+ memset(work->rx_pkt_queue, (u8)queue_idx,
+ sizeof(work->rx_pkt_queue[0]) * nr);
+ }
+ signal = (struct amd_signal *)priv->knod->kaql[0].queue_signal->kaddr;
+ work->sigval = READ_ONCE(signal->value);
+ if (static_branch_unlikely(&ipsec_stats_enabled_key))
+ work->dispatch_ts = ktime_to_ns(ktime_get());
+}
+
+/*
+ * Legacy exported entry points. With the single-dispatcher architecture,
+ * NICs publish bds into knodev->wpriv[].spsc_bds and the dispatcher polls
+ * them directly - no NIC driver actually calls these anymore, but keep the
+ * exports so external out-of-tree builds don't break while they transition.
+ */
+int knod_ipsec_rx_submit(struct knod_ipsec_fused_sub *sub, int nr,
+ struct napi_struct *napi, int queue_idx)
+{
+ return 0;
+}
+EXPORT_SYMBOL_GPL(knod_ipsec_rx_submit);
+
+int knod_ipsec_rx_submit_bds(struct knod_ipsec_fused_sub *sub,
+ struct spsc_bd **bds, int nr,
+ struct napi_struct *napi, int queue_idx)
+{
+ return 0;
+}
+EXPORT_SYMBOL_GPL(knod_ipsec_rx_submit_bds);
+
+/*
+ * Shader verdict sentinels (low-side) - kept in lockstep with
+ * ipsec_fused_gfx9.h. `bd->act` is packed as (low32=snapshot, high32=
+ * slot_idx|sentinel); we only read the high half here.
+ */
+#define KNOD_IPSEC_SHADER_VERDICT_MISS 0xFFFFFFFFu
+#define KNOD_IPSEC_SHADER_VERDICT_BYPASS 0xFFFFFFFEu
+#define KNOD_IPSEC_SHADER_VERDICT_ICV_FAIL 0xFFFFFFFDu
+
+/*
+ * The dispatcher fences each RX batch at the SDMA ring position that
+ * knod_sdma_submit() returned (a 32-bit dword cursor that wraps), so the
+ * completion test is a signed-32 compare against the signal's low word.
+ */
+static bool knod_ipsec_fence_passed(struct knod_ipsec_work *w)
+{
+ return (s32)((u32)READ_ONCE(*w->sdma_fence_ptr) -
+ w->sdma_fence_target) >= 0;
+}
+
+/* Bounded spin until @w's RX-batch SDMA fence fires (teardown path only). */
+static void knod_ipsec_fence_wait(struct knod_ipsec_work *w)
+{
+ int timeout = 100000;
+
+ while (!knod_ipsec_fence_passed(w)) {
+ if (--timeout <= 0)
+ break;
+ cpu_relax();
+ }
+}
+
+/*
+ * RX finish: GPU is done writing verdicts for this batch. For every
+ * packet that the shader flagged as a hit, SDMA-copy the decrypted
+ * inner payload from VRAM to a per-queue delivery-pool (pass_pool) page,
+ * then push a knod_pass_desc onto the framework pass_pending ring so the
+ * NIC dd NAPI picks it up via knod_d2h_drain() + knod_ipsec_post_copy().
+ *
+ * Packets with a miss/bypass/malformed verdict never reach the pending
+ * ring; they just get accounted here and the bd is recycled by the
+ * NIC dd through its normal bd ring path (unchanged).
+ */
+static void knod_ipsec_finish_rx_deliver(struct knod_ipsec_dispatcher *disp,
+ struct knod_ipsec_work *work)
+{
+ struct knod_ipsec_priv *priv = disp->priv;
+ struct knod_ipsec_fused_param *param;
+ struct knod_ipsec_stats *s = NULL;
+ struct knod *knod = priv->knod;
+ int sdma_idx = disp->kaql_idx;
+ struct knod_sdma *sdma_q = &knod->sdma[sdma_idx];
+ struct knod_sdma_copy_desc copies[2];
+ u32 batch_fence = 0;
+ int i;
+ bool stats_on = static_branch_unlikely(&ipsec_stats_enabled_key);
+ int n_sdma = 0;
+ u32 sdma_copies = 0;
+ u32 sdma_bytes = 0;
+ int pkt_idx_of[KNOD_IPSEC_PKT_BATCH];
+ struct knod_ipsec_rx_pending *pending = work->rx_pending;
+
+ if (stats_on)
+ s = this_cpu_ptr(priv->stats);
+
+ param = (struct knod_ipsec_fused_param *)work->param.kaddr;
+
+ /* Step 1: classify + schedule SDMA for hits. Each packet in the
+ * batch may belong to a different NIC RX queue, so rxq routing
+ * happens per-packet via work->rx_pkt_queue[i] -> priv->rxq[].
+ */
+ for (i = 0; i < work->nr_packets; i++) {
+ struct knod_ipsec_sa_slot *sa_slot;
+ struct spsc_bd *bd = work->rx_bds[i];
+ struct page_pool *pool;
+ netmem_ref netmem;
+ u64 pkt_gaddr, out_gaddr;
+ u64 dst_base;
+ u8 pkt_next_hdr;
+ u32 verdict_hi;
+ u32 inner_len;
+ u32 pend_inner_len;
+ u32 fv;
+ int ncopy;
+ u8 pkt_family;
+ u8 pkt_mode;
+ unsigned int rxq_idx = work->rx_pkt_queue[i];
+
+ if (!bd)
+ continue;
+ if ((int)rxq_idx >= priv->nr_rxq)
+ continue;
+ pool = READ_ONCE(priv->knodev->wpriv[rxq_idx].pass_pool);
+
+ verdict_hi = (u32)(bd->act >> 32);
+
+ if (verdict_hi == KNOD_IPSEC_SHADER_VERDICT_MISS ||
+ verdict_hi == KNOD_IPSEC_SHADER_VERDICT_BYPASS) {
+ u32 raw_len = bd->len;
+ u64 pkt_gaddr;
+ u64 dst_gaddr;
+
+ if (raw_len == 0 || raw_len > PAGE_SIZE) {
+ if (stats_on)
+ s->rx_drop_malformed++;
+ pr_warn_ratelimited("knod_ipsec: malform-A bypass raw_len=%u pkt[%d] act=0x%llx q%u\n",
+ raw_len, i, bd->act, rxq_idx);
+ bd->act = KNOD_IPSEC_DROP;
+ continue;
+ }
+ netmem = pool ? page_pool_dev_alloc_netmems(pool) : 0;
+ if (!netmem) {
+ if (stats_on)
+ s->rx_drop_malformed++;
+ pr_warn_ratelimited("knod_ipsec: malform-bypass-nomem pkt[%d] raw_len=%u q%u\n",
+ i, raw_len, rxq_idx);
+ bd->act = KNOD_IPSEC_DROP;
+ continue;
+ }
+ pkt_gaddr = le64_to_cpu(param->sub[i].pkt_addr);
+ dst_gaddr = page_pool_get_dma_addr_netmem(netmem);
+ copies[0].dst = dst_gaddr;
+ copies[0].src = pkt_gaddr;
+ copies[0].len = raw_len;
+ fv = knod_sdma_submit(knod, sdma_idx, copies, 1);
+ if (!fv) {
+ page_pool_put_full_netmem(pool, netmem, false);
+ if (stats_on)
+ s->rx_drop_sdma_full++;
+ bd->act = KNOD_IPSEC_DROP;
+ continue;
+ }
+ batch_fence = fv;
+ sdma_copies++;
+ sdma_bytes += raw_len;
+
+ pending[n_sdma].inner_len = raw_len;
+ pending[n_sdma].netmem = netmem;
+ pending[n_sdma].sa_slot = KNOD_IPSEC_NR_SA;
+ pending[n_sdma].rxq_idx = (u16)rxq_idx;
+ pending[n_sdma].mode = 0;
+ pending[n_sdma].next_hdr = 0;
+ pending[n_sdma].family = 0;
+ pending[n_sdma].inner_off = 0;
+ pkt_idx_of[n_sdma] = i;
+ n_sdma++;
+
+ bd->act = KNOD_IPSEC_PASS;
+ continue;
+ }
+ if (verdict_hi == KNOD_IPSEC_SHADER_VERDICT_ICV_FAIL) {
+ if (stats_on)
+ s->rx_drop_icv++;
+ pr_warn_ratelimited("knod_ipsec: RX ICV fail pkt[%d] act=0x%llx len=%u off=%u\n",
+ i, bd->act, bd->len, bd->off);
+ bd->act = KNOD_IPSEC_DROP;
+ continue;
+ }
+ if (verdict_hi >= KNOD_IPSEC_NR_SA) {
+ if (stats_on)
+ s->rx_drop_malformed++;
+ pr_warn_ratelimited("knod_ipsec: malform-B verdict_hi=%u pkt[%d] act=0x%llx len=%u q%u\n",
+ verdict_hi, i, bd->act, bd->len, rxq_idx);
+ bd->act = KNOD_IPSEC_DROP;
+ continue;
+ }
+
+ inner_len = bd->len;
+ if (inner_len == 0 || inner_len > PAGE_SIZE) {
+ if (stats_on)
+ s->rx_drop_malformed++;
+ pr_warn_ratelimited("knod_ipsec: malform-C inner_len=%u pkt[%d] act=0x%llx slot=%u q%u\n",
+ inner_len, i, bd->act, verdict_hi, rxq_idx);
+ continue;
+ }
+
+ pkt_mode = bd->off & 0xFF;
+ pkt_next_hdr = (bd->off >> 8) & 0xFF;
+
+ sa_slot = &priv->slots[verdict_hi];
+ pkt_family = sa_slot->x ? sa_slot->x->props.family : AF_INET;
+
+ netmem = pool ? page_pool_dev_alloc_netmems(pool) : 0;
+ if (!netmem) {
+ if (stats_on)
+ s->rx_drop_malformed++;
+ pr_warn_ratelimited("knod_ipsec: malform-decrypt-nomem pkt[%d] slot=%u inner_len=%u q%u\n",
+ i, verdict_hi, inner_len, rxq_idx);
+ bd->act = KNOD_IPSEC_DROP;
+ continue;
+ }
+ pkt_gaddr = le64_to_cpu(param->sub[i].pkt_addr);
+ out_gaddr = le64_to_cpu(param->sub[i].out_addr);
+ dst_base = page_pool_get_dma_addr_netmem(netmem);
+
+ if (pkt_mode == XFRM_MODE_TRANSPORT) {
+ u32 l3_hdr_len = (pkt_family == AF_INET6) ? 40 : 20;
+
+ copies[0].dst = dst_base + l3_hdr_len;
+ copies[0].src = out_gaddr;
+ copies[0].len = inner_len;
+ copies[1].dst = dst_base;
+ copies[1].src = pkt_gaddr + 14;
+ copies[1].len = l3_hdr_len;
+ ncopy = 2;
+ pend_inner_len = l3_hdr_len + inner_len;
+ } else {
+ copies[0].dst = dst_base + KNOD_IPSEC_GTT_OUT_L3_OFF;
+ copies[0].src = out_gaddr;
+ copies[0].len = inner_len;
+ ncopy = 1;
+ pend_inner_len = inner_len;
+ }
+
+ fv = knod_sdma_submit(knod, sdma_idx, copies, ncopy);
+ if (!fv) {
+ page_pool_put_full_netmem(pool, netmem, false);
+ if (stats_on)
+ s->rx_drop_sdma_full++;
+ bd->act = KNOD_IPSEC_DROP;
+ continue;
+ }
+ batch_fence = fv;
+ sdma_copies += ncopy;
+ sdma_bytes += pend_inner_len;
+
+ pending[n_sdma].inner_len = pend_inner_len;
+ pending[n_sdma].netmem = netmem;
+ pending[n_sdma].sa_slot = verdict_hi;
+ pending[n_sdma].rxq_idx = (u16)rxq_idx;
+ pending[n_sdma].mode = pkt_mode;
+ pending[n_sdma].next_hdr = pkt_next_hdr;
+ pending[n_sdma].family = pkt_family;
+ pending[n_sdma].inner_off =
+ (pkt_mode != XFRM_MODE_TRANSPORT) ?
+ KNOD_IPSEC_GTT_OUT_L3_OFF : 0;
+ pkt_idx_of[n_sdma] = i;
+ n_sdma++;
+ }
+
+ if (stats_on) {
+ work->rx_sdma_copies = sdma_copies;
+ work->rx_sdma_bytes = sdma_bytes;
+ }
+
+ /* Step 2: FENCE + doorbell. knod_sdma_submit() emitted the copies
+ * above; kick fences the ring position it returned (batch_fence).
+ */
+ if (n_sdma == 0)
+ return;
+ knod_sdma_kick(knod, sdma_idx);
+ work->sdma_fence_target = batch_fence;
+
+ work->sdma_fence_ptr = (s64 *)&((struct amd_signal *)
+ sdma_q->queue_signal->kaddr)->value;
+ work->sdma_submit_ns = stats_on ? ktime_get_ns() : 0;
+ work->n_sdma_pending = n_sdma;
+ for (i = 0; i < n_sdma; i++)
+ work->sdma_pkt_idx_of[i] = (u16)pkt_idx_of[i];
+}
+
+/*
+ * Deferred SDMA completion: publish to pass_pending and mark bds PASS.
+ *
+ * Called from the dispatcher loop once work->sdma_fence_ptr shows the
+ * SDMA fence has fired. All SDMA copies have landed in the delivery pages
+ * so the drain path can now read from them.
+ */
+static void knod_ipsec_finish_rx_complete(struct knod_ipsec_dispatcher *disp,
+ struct knod_ipsec_work *work)
+{
+ struct knod_ipsec_priv *priv = disp->priv;
+ struct knod_ipsec_fused_param *param =
+ (struct knod_ipsec_fused_param *)work->param.kaddr;
+ struct knod_ipsec_rx_pending *pending = work->rx_pending;
+ bool stats_on = static_branch_unlikely(&ipsec_stats_enabled_key);
+ struct knod_ipsec_stats *s = NULL;
+ int n_sdma = work->n_sdma_pending;
+ int i;
+
+ if (stats_on) {
+ s = this_cpu_ptr(priv->stats);
+ if (work->sdma_submit_ns)
+ work->sdma_wait_ns =
+ ktime_get_ns() - work->sdma_submit_ns;
+ }
+
+ /* Publish each decrypted packet onto its RX queue's common pending
+ * ring; knod_d2h_drain delivers it through knod_ipsec_post_copy. The
+ * SDMA fence already fired (the dispatcher waited), so the descriptor
+ * is tagged already-landed and the drain's fence check is a no-op.
+ */
+ for (i = 0; i < n_sdma; i++) {
+ u16 rxq = pending[i].rxq_idx;
+ struct knod_work_priv *wpriv = &priv->knodev->wpriv[rxq];
+ int src_j = work->sdma_pkt_idx_of[i];
+ struct knod_pass_desc *d;
+
+ if (spsc_produce(&wpriv->pass_pending, (void **)&d)) {
+ struct page_pool *pool = READ_ONCE(wpriv->pass_pool);
+
+ /* ring full: return the undelivered page */
+ if (pool)
+ page_pool_put_full_netmem(pool,
+ pending[i].netmem,
+ false);
+ if (stats_on)
+ s->rx_drop_desc_full++;
+ continue;
+ }
+ d->netmem = pending[i].netmem;
+ d->src = 0; /* NIC act handler recycles the RX bd */
+ d->len = pending[i].inner_len;
+ d->off = pending[i].inner_off;
+ d->fence_val = work->sdma_fence_target;
+ d->sdma_idx = disp->kaql_idx;
+ d->feat.sa_slot = pending[i].sa_slot;
+ if (pending[i].sa_slot < KNOD_IPSEC_NR_SA) {
+ struct knod_ipsec_sa_slot *sa =
+ &priv->slots[pending[i].sa_slot];
+
+ d->feat.seq_lo =
+ le32_to_cpu(param->sub[src_j].result_seq);
+ d->feat.seq_hi = (sa->x && sa->x->replay_esn) ?
+ sa->x->replay_esn->seq_hi : 0;
+ } else {
+ d->feat.seq_lo = 0;
+ d->feat.seq_hi = 0;
+ }
+ d->feat.mode = pending[i].mode;
+ d->feat.next_hdr = pending[i].next_hdr;
+ d->feat.family = pending[i].family;
+ spsc_produce_commit(&wpriv->pass_pending);
+ }
+
+ if (stats_on) {
+ s->rx_packets += n_sdma;
+ s->finish_produced += n_sdma;
+ }
+
+ /* Mark all bds as KNOD_IPSEC_PASS so the NIC act_handler recycles
+ * the netmem on the next NAPI poll.
+ */
+ for (i = 0; i < work->nr_packets; i++) {
+ if (work->rx_bds[i])
+ work->rx_bds[i]->act = KNOD_IPSEC_PASS;
+ }
+ memset(work->rx_bds, 0,
+ sizeof(work->rx_bds[0]) * work->nr_packets);
+
+ work->n_sdma_pending = 0;
+}
+
+/*
+ * NIC dd NAPI consumer: drain the per-queue host desc ring, run the
+ * RFC 4303 sliding window check on each descriptor, and deliver the
+ * inner packet up the stack as a zero-copy head_frag skb wrapping the
+ * delivery-pool page (knod_pass_build_skb); the page recycles to the
+ * pool on skb free.
+ *
+ * Safe to call before knod_ipsec_priv has been created: returns 0.
+ * NIC drivers can therefore wire this call unconditionally from their
+ * NAPI poll without extra gating.
+ *
+ * Ordering w.r.t. the bd recycle ring: the NIC driver should drain its
+ * bd ring *first* (to recycle netmem back into the page_pool) and then
+ * call drain_rx. The two rings are independent - bd ring delivers no
+ * verdicts, desc ring carries only PASS candidates.
+ */
+/*
+ * Feature delivery hook (knod_dev->post_copy) for the ipsec RX path.
+ * knod_d2h_drain has already built the head_frag skb wrapping the delivery
+ * page; here we run the SA lookup, RFC 4303 anti-replay window, cleartext
+ * L3 fix-up and secpath attach. Returns false to drop the packet (the
+ * drain frees the skb, which recycles the page).
+ */
+static bool knod_ipsec_post_copy(struct knod_dev *knodev, struct sk_buff *skb,
+ const struct knod_pass_desc *desc,
+ int queue_idx)
+{
+ struct knod_ipsec_priv *priv = ipsec_priv;
+ struct knod_ipsec_stats *s = NULL;
+ struct knod_ipsec_sa_slot *slot;
+ bool stats_on = static_branch_unlikely(&ipsec_stats_enabled_key);
+ u32 sa_slot = desc->feat.sa_slot;
+ u64 seq;
+
+ if (stats_on)
+ s = this_cpu_ptr(priv->stats);
+
+ /* Raw bypass: packet was not IPsec (ARP, non-ESP IP, ESP with an
+ * unknown SPI). The page holds the full Ethernet frame; deliver it
+ * with no IPsec state.
+ */
+ if (sa_slot == KNOD_IPSEC_NR_SA) {
+ skb->dev = knodev->netdev;
+ skb->protocol = eth_type_trans(skb, knodev->netdev);
+ skb_reset_network_header(skb);
+ return true;
+ }
+ if (sa_slot > KNOD_IPSEC_NR_SA) {
+ if (stats_on)
+ s->rx_drop_malformed++;
+ pr_warn_ratelimited("knod_ipsec: malform-pc-saslot sa_slot=%u len=%u q%d\n",
+ sa_slot, desc->len, queue_idx);
+ return false;
+ }
+
+ slot = &priv->slots[sa_slot];
+ if (!slot->active) {
+ if (stats_on)
+ s->rx_drop_no_sa++;
+ return false;
+ }
+
+ seq = ((u64)desc->feat.seq_hi << 32) | (u64)desc->feat.seq_lo;
+ if (!knod_ipsec_sa_window_check(&slot->win[queue_idx], seq)) {
+ if (stats_on)
+ s->rx_drop_replay++;
+ return false;
+ }
+
+ /* skb->data is the decrypted inner packet (head_frag at inner_off). */
+ if (desc->feat.mode == XFRM_MODE_TRANSPORT) {
+ /* Transport: the page holds the outer L3 header (20B IPv4 or
+ * 40B IPv6) followed by the decrypted payload; patch the
+ * next-header and length fields to describe the cleartext.
+ */
+ if (desc->feat.family == AF_INET6) {
+ struct ipv6hdr *ip6h;
+
+ if (desc->len < 40) {
+ if (stats_on)
+ s->rx_drop_malformed++;
+ pr_warn_ratelimited("knod_ipsec: malform-pc-v6len len=%u q%d\n",
+ desc->len, queue_idx);
+ return false;
+ }
+ ip6h = (struct ipv6hdr *)skb->data;
+ ip6h->nexthdr = desc->feat.next_hdr;
+ ip6h->payload_len = htons(desc->len - 40);
+ skb->protocol = htons(ETH_P_IPV6);
+ } else {
+ struct iphdr *iph;
+
+ if (desc->len < 20) {
+ if (stats_on)
+ s->rx_drop_malformed++;
+ pr_warn_ratelimited("knod_ipsec: malform-pc-v4len len=%u q%d\n",
+ desc->len, queue_idx);
+ return false;
+ }
+ iph = (struct iphdr *)skb->data;
+ iph->protocol = desc->feat.next_hdr;
+ iph->tot_len = htons(desc->len);
+ iph->check = 0;
+ iph->check = ip_fast_csum((u8 *)iph, iph->ihl);
+ skb->protocol = htons(ETH_P_IP);
+ }
+ } else {
+ /* Tunnel: decrypted payload is a bare inner L3 packet; infer
+ * v4/v6 from the IP version nibble.
+ */
+ u8 first = *(const u8 *)skb->data;
+
+ if ((first >> 4) == 4) {
+ skb->protocol = htons(ETH_P_IP);
+ } else if ((first >> 4) == 6) {
+ skb->protocol = htons(ETH_P_IPV6);
+ } else {
+ if (stats_on)
+ s->rx_drop_malformed++;
+ pr_warn_ratelimited("knod_ipsec: malform-pc-tunver first=0x%02x len=%u off=%u q%d\n",
+ first, desc->len, desc->off, queue_idx);
+ return false;
+ }
+ }
+
+ skb->dev = knodev->netdev;
+ skb_reset_mac_header(skb);
+ skb_reset_network_header(skb);
+
+ /* The inner L4 checksum was computed by the sender before encryption
+ * and ESP authentication guarantees the decrypted payload is byte-
+ * identical, so trust it instead of recomputing (saves ~1.8% CPU at
+ * 45+ Gbps UDP).
+ */
+ skb->ip_summed = CHECKSUM_UNNECESSARY;
+
+ /* Attach secpath so xfrm_policy_check() recognises this packet as
+ * decrypted by an offload engine; without it the inbound policy
+ * drops the cleartext.
+ */
+ if (slot->x) {
+ struct sec_path *sp;
+ struct xfrm_offload *xo;
+
+ sp = secpath_set(skb);
+ if (unlikely(!sp)) {
+ if (stats_on)
+ s->rx_drop_malformed++;
+ pr_warn_ratelimited("knod_ipsec: malform-pc-secpath len=%u q%d\n",
+ desc->len, queue_idx);
+ return false;
+ }
+ xfrm_state_hold(slot->x);
+ sp->xvec[sp->len++] = slot->x;
+ sp->olen++;
+
+ xo = xfrm_offload(skb);
+ xo->flags = CRYPTO_DONE;
+ xo->status = CRYPTO_SUCCESS;
+ }
+
+ if (stats_on) {
+ s->rx_bytes += desc->len;
+ s->drain_delivered++;
+ }
+ return true;
+}
+
+/*
+ * After the GPU completion signal has fired for `work`, finalise it:
+ * SDMA-copy decrypted RX inner packets into per-queue delivery-pool pages
+ * and publish them on the host desc ring. Called only from dispatcher
+ * context. No lock needed: the dispatcher is the sole accessor.
+ */
+static void knod_ipsec_finalise_work(struct knod_ipsec_dispatcher *disp,
+ struct knod_ipsec_work *work)
+{
+ knod_ipsec_finish_rx_deliver(disp, work);
+
+ if (work->rx_napi) {
+ napi_schedule(work->rx_napi);
+ work->rx_napi = NULL;
+ }
+ /* rx_bds[] clear is deferred to finish_rx_complete /
+ * finalise_sdma_done for the SDMA path, because the bd
+ * pointers are needed to mark bd->act = PASS after SDMA
+ * completes. For the no-SDMA path (n_sdma_pending == 0),
+ * finalise_inflight clears them inline before returning
+ * EMPTY.
+ */
+ if (work->n_sdma_pending == 0)
+ memset(work->rx_bds, 0,
+ sizeof(work->rx_bds[0]) * work->nr_packets);
+
+ /* Timing stats are now accumulated by the dispatcher
+ * (try_rx) which has visibility into all phases -
+ * build / gpu / sdma / finalise. This function no longer
+ * touches *_gpu_ns.
+ */
+}
+
+/*
+ * Kick the GPU using kaql[disp->kaql_idx]. Assigns a forward-looking
+ * sigval by decrementing disp->dispatch_sigval_next so each in-flight
+ * slot has a distinct target - required for depth-N pipelining where
+ * multiple dispatches are queued on the same AQL queue and GPU
+ * decrements signal->value by 1 per completion.
+ *
+ * Submit-only: does not wait. The caller is responsible for polling
+ * knod_ipsec_dispatch_done() or blocking via knod_ipsec_dispatch_wait().
+ */
+static void knod_ipsec_dispatch_submit(struct knod_ipsec_dispatcher *disp,
+ struct knod_ipsec_work *work)
+{
+ struct knod_dispatch_params p;
+
+ disp->dispatch_sigval_next--;
+ work->sigval = disp->dispatch_sigval_next;
+ knod_ipsec_fill_dispatch(disp->priv->knod, work, &p);
+ knod_setup_header(disp->priv->knod, &p, disp->kaql_idx);
+}
+
+/*
+ * Non-blocking completion check. Returns true if the kernel dispatch
+ * for `work` has finished. Used by the pipelined dispatcher loop to
+ * poll in-flight slots without stalling the build side.
+ */
+static bool knod_ipsec_dispatch_done(struct knod_ipsec_dispatcher *disp,
+ struct knod_ipsec_work *work)
+{
+ struct amd_signal *signal =
+ (struct amd_signal *)disp->priv->knod->kaql[disp->kaql_idx]
+ .queue_signal->kaddr;
+
+ return READ_ONCE(signal->value) <= work->sigval;
+}
+
+/*
+ * Blocking wait. Synchronous path used by the KAT selftest where we
+ * don't want pipelining. Spin-waits with cpu_relax and a best-effort
+ * timeout. In production, the dispatcher uses the non-blocking
+ * dispatch_done() check instead.
+ */
+static void knod_ipsec_dispatch_wait(struct knod_ipsec_dispatcher *disp,
+ struct knod_ipsec_work *work)
+{
+ struct amd_signal *signal =
+ (struct amd_signal *)disp->priv->knod->kaql[disp->kaql_idx]
+ .queue_signal->kaddr;
+ int timeout = 1000000;
+
+ while (READ_ONCE(signal->value) > work->sigval) {
+ if (--timeout <= 0) {
+ pr_warn_ratelimited("knod_ipsec: GPU signal timeout nr=%d\n",
+ work->nr_packets);
+ return;
+ }
+ cpu_relax();
+ }
+}
+
+/*
+ * Convenience wrapper: submit + block until done. Keeps the existing
+ * KAT / selftest call sites that expect a synchronous dispatch.
+ */
+static void knod_ipsec_dispatch_and_wait(struct knod_ipsec_dispatcher *disp,
+ struct knod_ipsec_work *work)
+{
+ knod_ipsec_dispatch_submit(disp, work);
+ knod_ipsec_dispatch_wait(disp, work);
+}
+
+/*
+ * Multi-queue RX drain into one GPU dispatch.
+ *
+ * For each NIC RX queue round-robin'd from *rx_rr, peek up to
+ * `cap_per_q = KNOD_IPSEC_PKT_BATCH / nr_queues` packets and stage them
+ * directly into the fused-shader kernarg sub[] array. This gives strict
+ * per-queue fairness: no single queue can monopolise the batch even if
+ * it has 10k packets backed up. When cap_per_q < 1 we fall back to 1 so
+ * that PKT_BATCH < nr_queues setups still drain something per queue.
+ *
+ * All queues drained in one dispatch must share the same SA table, but
+ * their packets may be for different SAs (each sub[].pkt_addr is an
+ * independent ESP frame with its own SPI); the shader performs the SPI
+ * scan per workgroup. Per-packet rx_pkt_queue[i] is recorded so
+ * finish_rx_deliver() routes each decrypted packet to the matching
+ * priv->rxq[q] delivery pool + desc_ring.
+ *
+ * NAPIs for every queue we touched are scheduled at the end so the
+ * driver-side act_handler sees the INFLIGHT -> PASS/DROP transition and
+ * recycles the netmem pages.
+ */
+static bool knod_ipsec_dispatcher_try_rx(struct knod_ipsec_dispatcher *disp,
+ struct knod_ipsec_work *work)
+{
+ struct knod_ipsec_priv *priv = disp->priv;
+ struct knod_dev *knodev = priv->knodev;
+ struct knod_ipsec_fused_param *param;
+ struct {
+ u16 q;
+ u16 count;
+ } per_q[KNOD_SPSC_MAX];
+ int nr_queues, nr_disp_queues, i, q = -1, start;
+ int active, pass, pi;
+ unsigned int total_n = 0;
+ unsigned int cap_per_q;
+ int per_q_n = 0;
+ bool stats_on = static_branch_unlikely(&ipsec_stats_enabled_key);
+ u64 t_start = 0, t_build_end = 0;
+
+ if (!knodev || !priv->knod || !priv->knod->buf)
+ return false;
+
+ if (stats_on) {
+ t_start = ktime_get_ns();
+ work->sdma_wait_ns = 0;
+ work->rx_sdma_copies = 0;
+ work->rx_sdma_bytes = 0;
+ }
+
+ nr_queues = knodev->netdev ? knodev->netdev->real_num_rx_queues : 0;
+ if (nr_queues > KNOD_SPSC_MAX)
+ nr_queues = KNOD_SPSC_MAX;
+ if (nr_queues > priv->knod->channels)
+ nr_queues = priv->knod->channels;
+ if (nr_queues <= 0)
+ return false;
+
+ /* This dispatcher's window into the global queue set. Everything
+ * below indexes with `disp->rxq_first + (rr_offset % nr_disp_queues)`
+ * so dispatcher N only ever touches queues it owns.
+ */
+ nr_disp_queues = disp->rxq_count;
+ if (nr_disp_queues > nr_queues - disp->rxq_first)
+ nr_disp_queues = nr_queues - disp->rxq_first;
+ if (nr_disp_queues <= 0)
+ return false;
+
+ /* Count active (non-empty) queues within this dispatcher's range
+ * so the fair-share cap reflects actual demand. Without this, a
+ * single-flow iperf3 that only populates 1 out of N RX queues
+ * would be limited to BATCH/N per dispatch.
+ */
+ active = 0;
+ for (i = 0; i < nr_disp_queues; i++) {
+ int qi = disp->rxq_first +
+ ((disp->rx_rr + i) % nr_disp_queues);
+ struct knod_work_priv *wp = &knodev->wpriv[qi];
+ struct spsc_ring *rr = &wp->spsc_bds;
+
+ if (!wp->napi || !rr->slots || rr->mask == 0)
+ continue;
+ if (!priv->knod->buf[qi])
+ continue;
+ if (spsc_count(rr) > 0)
+ active++;
+ }
+ if (active == 0)
+ return false;
+ cap_per_q = DIV_ROUND_UP(READ_ONCE(priv->pkt_batch), active);
+
+ /* Build fused_param directly into kernarg. Zero the entire struct
+ * so stale sub[batch_n..BATCH-1] entries from previous dispatches
+ * cannot be picked up by a GPU kernarg prefetch - the GFX9 CP may
+ * speculatively read beyond grid_size_y into the kernarg buffer,
+ * and a stale sub[].pkt_addr pointing at valid VRAM could cause
+ * the shader to process garbage packets (observed as ICV failures
+ * when the memset was removed).
+ */
+ param = (struct knod_ipsec_fused_param *)work->param.kaddr;
+ memset(param, 0, sizeof(*param));
+ param->sa_table_addr = cpu_to_le64(priv->sa_table->gaddr);
+ param->t_tables_addr = cpu_to_le64(priv->t_tables->gaddr);
+ param->nr_sa = cpu_to_le32(KNOD_IPSEC_NR_SA);
+
+ /* Two-pass fair drain.
+ *
+ * Pass 1 caps each queue at cap_per_q (= BATCH / active_queues) so
+ * one queue cannot monopolise the batch when several queues have
+ * traffic.
+ *
+ * Pass 2 fills any remaining batch budget greedily from queues
+ * that still have packets. This matters for single-flow iperf3
+ * where only one RX queue is active: pass 1 fills that queue up
+ * to its fair share (= BATCH for active=1) and pass 2 is a no-op,
+ * but with a few active queues pass 2 picks up the slack left by
+ * queues that had fewer than their fair share.
+ *
+ * IMPORTANT: we advance the SPSC acquired cursor IMMEDIATELY after
+ * each per-queue peek so that a subsequent pass re-visiting the
+ * same queue never re-reads the same packets. Earlier naive 2-pass
+ * without this acquire double-processed packets and caused ~50%
+ * anti-replay drops downstream.
+ */
+ start = disp->rx_rr;
+ for (pass = 0; pass < 2; pass++) {
+ unsigned int per_q_cap = (pass == 0) ?
+ cap_per_q : READ_ONCE(priv->pkt_batch);
+
+ for (i = 0; i < nr_disp_queues; i++) {
+ struct knod_work_priv *wpriv;
+ struct spsc_ring *r;
+ u64 base_gaddr;
+ unsigned int cnt = 0;
+ unsigned int acq_start;
+ unsigned int remaining, budget;
+ int err, k, pi;
+ bool found;
+
+ q = disp->rxq_first +
+ ((start + i) % nr_disp_queues);
+ wpriv = &knodev->wpriv[q];
+ r = &wpriv->spsc_bds;
+
+ if (!wpriv->napi || !r->slots || r->mask == 0)
+ continue;
+ if (!priv->knod->buf[q])
+ continue;
+
+ remaining = READ_ONCE(priv->pkt_batch) - total_n;
+ if (remaining == 0)
+ break;
+ budget = min(per_q_cap, remaining);
+
+ base_gaddr = priv->knod->buf[q]->gaddr;
+ acq_start = r->acquired;
+
+ err = spsc_peek(r, (void **)&work->rx_bds[total_n],
+ budget, &cnt);
+ if (err || cnt == 0)
+ continue;
+
+ /* Delivery pages are allocated lazily at finish time
+ * from the framework page_pool; no per-packet slot
+ * reservation here.
+ */
+ for (k = 0; k < (int)cnt; k++) {
+ struct spsc_bd *bd = work->rx_bds[total_n + k];
+ u32 ring_idx = (acq_start + k) & r->mask;
+ u64 pkt_addr, bd_gaddr, out_gaddr;
+ unsigned int si = total_n + k;
+
+ pkt_addr = base_gaddr +
+ ((u64)bd->page_idx << PAGE_SHIFT) +
+ bd->off;
+ bd_gaddr = wpriv->spsc_pool_gaddr +
+ (u64)ring_idx * r->elem_stride;
+
+ out_gaddr = work->rx_out_gaddr +
+ (u64)si *
+ KNOD_IPSEC_DECRYPT_PKT_SIZE;
+
+ param->sub[si].pkt_addr =
+ cpu_to_le64(pkt_addr);
+ param->sub[si].out_addr =
+ cpu_to_le64(out_gaddr);
+ param->sub[si].bd_addr =
+ cpu_to_le64(bd_gaddr);
+ param->sub[si].pkt_len =
+ cpu_to_le32(bd->len);
+ param->sub[si].result_seq = 0;
+
+ work->rx_pkt_queue[si] = (u8)q;
+ bd->act = KNOD_IPSEC_INFLIGHT;
+ }
+
+ /* Advance r->acquired now so pass 2 never
+ * re-peeks the same entries.
+ */
+ spsc_acquire(r, NULL, cnt, NULL);
+
+ found = false;
+ for (pi = 0; pi < per_q_n; pi++) {
+ if (per_q[pi].q == (u16)q) {
+ per_q[pi].count += (u16)cnt;
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ per_q[per_q_n].q = (u16)q;
+ per_q[per_q_n].count = (u16)cnt;
+ per_q_n++;
+ }
+ total_n += cnt;
+ }
+
+ if (total_n >= READ_ONCE(priv->pkt_batch))
+ break;
+ }
+
+ if (total_n == 0)
+ return false;
+
+ if (static_branch_unlikely(&ipsec_stats_enabled_key)) {
+ struct knod_ipsec_stats *cs = this_cpu_ptr(priv->stats);
+
+ cs->rx_peek_total += total_n;
+ }
+
+ /* spsc_acquire was already called per-queue inside the drain loop
+ * to prevent the two-pass logic from re-peeking the same entries,
+ * so there is nothing to commit here.
+ */
+
+ work->nr_packets = (int)total_n;
+ /* Multi-queue: no single napi. finalise_work will skip its
+ * rx_napi kick and we schedule per-queue napis below.
+ */
+ work->rx_napi = NULL;
+ work->rx_queue_idx = per_q[0].q; /* used only by single-queue KAT */
+
+ if (stats_on) {
+ t_build_end = ktime_get_ns();
+ work->t_build_end = t_build_end;
+ }
+
+ if (stats_on) {
+ struct knod_ipsec_stats *s = this_cpu_ptr(priv->stats);
+
+ s->rx_dispatches++;
+ s->rx_batch_total += total_n;
+ if ((u64)total_n > s->rx_batch_max)
+ s->rx_batch_max = total_n;
+ }
+
+ /* Stash per-queue drain bookkeeping so the deferred finalise
+ * path (called when the dispatch completes, possibly in a later
+ * dispatcher iteration) can wake the touched NAPIs and attribute
+ * stats back to this slot.
+ */
+ work->per_q_n = per_q_n;
+ for (pi = 0; pi < per_q_n; pi++)
+ work->per_q_touched[pi] = per_q[pi].q;
+ work->t_build_start = t_start;
+ work->t_submit = stats_on ? ktime_get_ns() : 0;
+
+ param->sdma_ring_addr = 0;
+ param->sdma_ctl_addr = 0;
+
+ /* Submit-only: do not wait, do not finalise. The dispatcher loop
+ * polls knod_ipsec_dispatch_done() and runs finalise via
+ * knod_ipsec_finalise_inflight() once the completion signal fires.
+ */
+ knod_ipsec_dispatch_submit(disp, work);
+
+ disp->rx_rr = nr_disp_queues > 0
+ ? ((q - disp->rxq_first + 1) % nr_disp_queues)
+ : 0;
+ return true;
+}
+
+/*
+ * Deferred finalise for a slot whose GPU dispatch has completed (signal
+ * fired). Runs finish_rx_deliver(), wakes per-queue
+ * napis tracked by try_rx, and accumulates per-phase timing stats.
+ * Called from the pipelined dispatcher loop once
+ * knod_ipsec_dispatch_done() returns true for the slot.
+ */
+/*
+ * Called when GPU dispatch completes. For RX with SDMA copies, this
+ * transitions to SDMA_PENDING (the dispatcher loop polls the fence
+ * and calls knod_ipsec_finalise_sdma_done when ready). For RX without
+ * SDMA, completes everything inline and returns true so the
+ * caller can transition directly to EMPTY.
+ *
+ * Returns true if fully done (-> EMPTY), false if -> SDMA_PENDING.
+ */
+static bool knod_ipsec_finalise_inflight(struct knod_ipsec_dispatcher *disp,
+ struct knod_ipsec_work *work)
+{
+ struct knod_ipsec_priv *priv = disp->priv;
+ struct knod_dev *knodev = priv->knodev;
+ bool stats_on = static_branch_unlikely(&ipsec_stats_enabled_key);
+ u64 t_gpu_end = 0, t_end = 0;
+ int i;
+
+ if (stats_on)
+ t_gpu_end = ktime_get_ns();
+
+ work->t_finalise_start = t_gpu_end;
+ knod_ipsec_finalise_work(disp, work);
+
+ /* RX path with SDMA copies pending - defer completion until the
+ * SDMA fence fires. The dispatcher loop will poll sdma_fence_ptr
+ * and call knod_ipsec_finalise_sdma_done().
+ */
+ if (work->n_sdma_pending > 0) {
+ if (stats_on) {
+ struct knod_ipsec_stats *s = this_cpu_ptr(priv->stats);
+ u64 build_ns = work->t_build_end - work->t_build_start;
+ u64 gpu_ns = t_gpu_end > work->t_submit
+ ? t_gpu_end - work->t_submit : 0;
+
+ s->rx_build_ns += build_ns;
+ s->rx_gpu_ns += gpu_ns;
+ s->rx_sdma_copies_total += work->rx_sdma_copies;
+ s->rx_sdma_bytes_total += work->rx_sdma_bytes;
+ if (work->rx_sdma_copies > s->rx_sdma_copies_max)
+ s->rx_sdma_copies_max = work->rx_sdma_copies;
+ }
+ return false; /* -> SDMA_PENDING */
+ }
+
+ /* No SDMA copies - mark bds PASS and schedule NAPIs. */
+ for (i = 0; i < work->nr_packets; i++) {
+ if (work->rx_bds[i])
+ work->rx_bds[i]->act = KNOD_IPSEC_PASS;
+ }
+ if (knodev) {
+ for (i = 0; i < work->per_q_n; i++) {
+ struct knod_work_priv *wpriv =
+ &knodev->wpriv[work->per_q_touched[i]];
+
+ knod_napi_kick(wpriv);
+ }
+ }
+
+ if (stats_on) {
+ struct knod_ipsec_stats *s = this_cpu_ptr(priv->stats);
+
+ t_end = ktime_get_ns();
+
+ /* RX no-SDMA: all stats fit here. The SDMA path
+ * records build/gpu stats in the early-return above
+ * and sdma/finalise stats in finalise_sdma_done().
+ */
+ u64 build_ns = work->t_build_end - work->t_build_start;
+ u64 gpu_ns = t_gpu_end > work->t_submit
+ ? t_gpu_end - work->t_submit : 0;
+ u64 finalise_ns = t_end > t_gpu_end
+ ? t_end - t_gpu_end : 0;
+
+ s->rx_build_ns += build_ns;
+ s->rx_gpu_ns += gpu_ns;
+ s->rx_finalise_ns += finalise_ns;
+ s->rx_total_ns += t_end - work->t_build_start;
+ }
+
+ /* Clear slot state so the dispatcher can reuse this work entry. */
+ work->per_q_n = 0;
+ work->nr_packets = 0;
+ work->sdma_wait_ns = 0;
+ work->rx_sdma_copies = 0;
+ work->rx_sdma_bytes = 0;
+ work->n_sdma_pending = 0;
+
+ return true; /* -> EMPTY */
+}
+
+/*
+ * SDMA fence has fired for a slot in SDMA_PENDING. Publish desc_ring
+ * entries, mark bds PASS, schedule NAPIs, accumulate remaining stats,
+ * and clear the slot for reuse.
+ */
+static void knod_ipsec_finalise_sdma_done(struct knod_ipsec_dispatcher *disp,
+ struct knod_ipsec_work *work)
+{
+ struct knod_ipsec_priv *priv = disp->priv;
+ bool stats_on = static_branch_unlikely(&ipsec_stats_enabled_key);
+
+ knod_ipsec_finish_rx_complete(disp, work);
+
+ /* Schedule NAPIs now that bds are marked PASS. */
+ if (priv->knodev) {
+ int i;
+
+ for (i = 0; i < work->per_q_n; i++) {
+ struct knod_work_priv *wpriv =
+ &priv->knodev->wpriv[work->per_q_touched[i]];
+
+ knod_napi_kick(wpriv);
+ }
+ }
+
+ if (stats_on) {
+ struct knod_ipsec_stats *s = this_cpu_ptr(priv->stats);
+ u64 t_end = ktime_get_ns();
+ u64 finalise_ns = t_end > work->t_finalise_start
+ ? t_end - work->t_finalise_start : 0;
+
+ if (finalise_ns > work->sdma_wait_ns)
+ finalise_ns -= work->sdma_wait_ns;
+ else
+ finalise_ns = 0;
+
+ s->rx_sdma_ns += work->sdma_wait_ns;
+ s->rx_finalise_ns += finalise_ns;
+ s->rx_total_ns += t_end - work->t_build_start;
+ }
+
+ work->per_q_n = 0;
+ work->nr_packets = 0;
+ work->sdma_wait_ns = 0;
+ work->rx_sdma_copies = 0;
+ work->rx_sdma_bytes = 0;
+ work->n_sdma_pending = 0;
+}
+
+/*
+ * Pipelined dispatcher kthread. Each dispatcher owns one kaql[i] /
+ * sdma[i] pair, a private slice of the work_pool, and a contiguous
+ * range of RX queues. Up to KNOD_IPSEC_NR_WORK dispatches
+ * may be in-flight on this AQL queue at once. Each iteration:
+ *
+ * 1) Scan INFLIGHT slots within this disp's slice, finalise any
+ * whose completion signal fired.
+ * 2) Scan EMPTY slots in the same slice, try to build+submit one RX
+ * batch into it.
+ * 3) If nothing happened (no work to finalise, nothing to build),
+ * idle (usleep or cpu_relax spins).
+ *
+ * With nr_dispatchers > 1 the AQL queues run independently on the
+ * GPU - disjoint CU allocations per kaql - so two dispatchers get
+ * real parallel execution on the GPU. Within a single dispatcher,
+ * kaql[i] is FIFO: dispatches complete in submission order, and each
+ * slot's work->sigval is assigned a distinct target by
+ * dispatch_submit() so dispatch_done() can distinguish completions
+ * of different in-flight slots.
+ *
+ * No cross-dispatcher synchronisation on the hot path: each disp
+ * only touches its own rxq range and its own work slice.
+ * The shared SA table, slot array and per-CPU stats are read-mostly
+ * or percpu respectively.
+ */
+static int knod_ipsec_dispatcher(void *arg)
+{
+ struct knod_ipsec_dispatcher *disp = arg;
+ struct knod_ipsec_priv *priv = disp->priv;
+ int i;
+
+ while (!kthread_should_stop()) {
+ bool did_work = false;
+
+ if (kthread_should_park()) {
+ knod_ipsec_dispatcher_drain(disp);
+ kthread_parkme();
+ continue;
+ }
+
+ /* Phase 1a: finalise any INFLIGHT slot whose GPU work done. */
+ for (i = 0; i < disp->work_count; i++) {
+ struct knod_ipsec_work *w =
+ &priv->work_pool[disp->work_first + i];
+
+ if (w->state != KNOD_WORK_INFLIGHT)
+ continue;
+ if (!knod_ipsec_dispatch_done(disp, w))
+ continue;
+
+ if (knod_ipsec_finalise_inflight(disp, w))
+ w->state = KNOD_WORK_EMPTY;
+ else
+ w->state = KNOD_WORK_SDMA_PENDING;
+ did_work = true;
+ }
+
+ /*
+ * Phase 1b: complete any SDMA_PENDING slot whose fence
+ * fired.
+ */
+ for (i = 0; i < disp->work_count; i++) {
+ struct knod_ipsec_work *w =
+ &priv->work_pool[disp->work_first + i];
+
+ if (w->state != KNOD_WORK_SDMA_PENDING)
+ continue;
+ if (!knod_ipsec_fence_passed(w))
+ continue;
+
+ knod_ipsec_finalise_sdma_done(disp, w);
+ w->state = KNOD_WORK_EMPTY;
+ did_work = true;
+ }
+
+ /* Phase 2: try to build + submit into at most one EMPTY slot
+ * per iteration. Limiting to one per iteration keeps phase-1
+ * polling responsive so completions don't queue up while we
+ * greedily fill every empty slot.
+ */
+ for (i = 0; i < disp->work_count; i++) {
+ int idx = (disp->build_cursor + i) % disp->work_count;
+ struct knod_ipsec_work *w =
+ &priv->work_pool[disp->work_first + idx];
+
+ if (w->state != KNOD_WORK_EMPTY)
+ continue;
+
+ if (knod_ipsec_dispatcher_try_rx(disp, w)) {
+ w->state = KNOD_WORK_INFLIGHT;
+ did_work = true;
+ disp->build_cursor =
+ (idx + 1) % disp->work_count;
+ break;
+ }
+ }
+
+ if (priv->knodev) {
+ int q;
+
+ for (q = disp->rxq_first;
+ q < disp->rxq_first + disp->rxq_count;
+ q++) {
+ struct knod_work_priv *wp =
+ &priv->knodev->wpriv[q];
+
+ if (!spsc_empty(&wp->pass_pending))
+ knod_napi_kick(wp);
+ }
+ }
+
+ if (!did_work) {
+ u64 idle_t0 = 0;
+ bool stats_on =
+ static_branch_unlikely(&ipsec_stats_enabled_key);
+
+ if (stats_on)
+ idle_t0 = ktime_get_ns();
+
+ if (READ_ONCE(knod_ipsec_poll_mode)) {
+ /* Busy-poll: spin a small number of times with
+ * cpu_relax() so newly-produced SPSC entries or
+ * completions are picked up in <1us.
+ */
+ int spins = 64;
+
+ while (spins-- > 0)
+ cpu_relax();
+ } else {
+ usleep_range(20, 100);
+ }
+
+ if (stats_on)
+ this_cpu_ptr(priv->stats)->rx_idle_ns +=
+ ktime_get_ns() - idle_t0;
+ }
+ }
+
+ knod_ipsec_dispatcher_drain(disp);
+ return 0;
+}
+
+static void knod_ipsec_dispatcher_drain(struct knod_ipsec_dispatcher *disp)
+{
+ struct knod_ipsec_priv *priv = disp->priv;
+ int i;
+
+ for (i = 0; i < disp->work_count; i++) {
+ struct knod_ipsec_work *w =
+ &priv->work_pool[disp->work_first + i];
+
+ if (w->state == KNOD_WORK_INFLIGHT) {
+ knod_ipsec_dispatch_wait(disp, w);
+ if (!knod_ipsec_finalise_inflight(disp, w)) {
+ /*
+ * SDMA started - spin-wait, we can't
+ * defer.
+ */
+ knod_ipsec_fence_wait(w);
+ knod_ipsec_finalise_sdma_done(disp, w);
+ }
+ w->state = KNOD_WORK_EMPTY;
+ } else if (w->state == KNOD_WORK_SDMA_PENDING) {
+ knod_ipsec_fence_wait(w);
+ knod_ipsec_finalise_sdma_done(disp, w);
+ w->state = KNOD_WORK_EMPTY;
+ }
+ }
+}
+
+/*
+ * Work slot pool allocator.
+ *
+ * `work_pool[0..KNOD_IPSEC_NR_WORK-1]` are pipelined work slots owned by
+ * the dispatcher kthread. Each slot gets its own slice of the param /
+ * decrypt pool BOs - single large BOs avoid the multi-BO GPU-VA
+ * mapping bug. KAT / selftest code paths always run against slot 0.
+ *
+ */
+static int knod_ipsec_work_pool_alloc(struct knod_ipsec_priv *priv)
+{
+ /* Round the kernarg pool up to a power-of-two page count so we never
+ * hit the 7-page (or other non-pow2) VRAM alloc bug the KNOD
+ * allocator triggers on certain sizes. At PKT_BATCH=512 fused_param
+ * is ~16 KB = 4 pages (already pow2). At BATCH=1024 it is ~32 KB = 8
+ * pages. Harmless extra slack otherwise.
+ */
+ const size_t param_raw_bytes =
+ ALIGN(sizeof(struct knod_ipsec_fused_param), PAGE_SIZE);
+ const unsigned long param_raw_pages = param_raw_bytes >> PAGE_SHIFT;
+ const unsigned long param_pages =
+ param_raw_pages <= 1 ? 1 : roundup_pow_of_two(param_raw_pages);
+ const size_t param_stride = param_pages << PAGE_SHIFT;
+ struct amd_signal *signal;
+ int nr_disp = priv->nr_dispatchers;
+ int d, i;
+
+ if (nr_disp < 1)
+ nr_disp = 1;
+ if (nr_disp > KNOD_IPSEC_MAX_DISPATCHERS)
+ nr_disp = KNOD_IPSEC_MAX_DISPATCHERS;
+ priv->nr_dispatchers = nr_disp;
+
+ memset(priv->work_pool, 0, sizeof(priv->work_pool));
+
+ for (d = 0; d < nr_disp; d++) {
+ struct knod_ipsec_dispatcher *disp = &priv->disp[d];
+
+ disp->priv = priv;
+ disp->kthread = NULL;
+ disp->kaql_idx = d;
+ disp->work_first = d * KNOD_IPSEC_NR_WORK;
+ disp->work_count = KNOD_IPSEC_NR_WORK;
+ disp->rx_rr = 0;
+ disp->build_cursor = 0;
+
+ /* GTT (not VRAM) for param_pool so the dispatcher CPU
+ * can read sub[].pkt_addr / sub[].out_addr back in
+ * finish_rx_deliver via WB cache rather than WC PCIe.
+ */
+ disp->param_pool = knod_alloc_mem(priv->knod,
+ ALIGN(param_stride * KNOD_IPSEC_NR_WORK, PAGE_SIZE),
+ KFD_IOC_ALLOC_MEM_FLAGS_GTT |
+ KFD_IOC_ALLOC_MEM_FLAGS_WRITABLE |
+ KFD_IOC_ALLOC_MEM_FLAGS_COHERENT);
+ if (IS_ERR(disp->param_pool)) {
+ int err = PTR_ERR(disp->param_pool);
+
+ disp->param_pool = NULL;
+ return err;
+ }
+
+ /* +KNOD_IPSEC_GTT_OUT_L3_OFF: 20 B headroom so the
+ * shader's "L3 header -> out_addr - L3_OFF" write has a
+ * valid GPU VM target when a SLOT_NONE fallback directs
+ * output here. Each work slot's rx_out_gaddr includes
+ * this offset.
+ */
+ disp->decrypt_pool = knod_alloc_mem(priv->knod,
+ KNOD_IPSEC_DECRYPT_WORK_SIZE * KNOD_IPSEC_NR_WORK +
+ KNOD_IPSEC_GTT_OUT_L3_OFF,
+ KFD_IOC_ALLOC_MEM_FLAGS_VRAM |
+ KFD_IOC_ALLOC_MEM_FLAGS_WRITABLE |
+ KFD_IOC_ALLOC_MEM_FLAGS_COHERENT);
+ if (IS_ERR(disp->decrypt_pool)) {
+ int err = PTR_ERR(disp->decrypt_pool);
+
+ disp->decrypt_pool = NULL;
+ return err;
+ }
+
+ for (i = 0; i < KNOD_IPSEC_NR_WORK; i++) {
+ struct knod_ipsec_work *work =
+ &priv->work_pool[disp->work_first + i];
+
+ memset(work, 0, sizeof(*work));
+ work->state = KNOD_WORK_EMPTY;
+ work->param.kaddr = (u8 *)disp->param_pool->kaddr +
+ (size_t)i * param_stride;
+ work->param.gaddr = disp->param_pool->gaddr +
+ (u64)i * param_stride;
+ work->rx_out_gaddr = disp->decrypt_pool->gaddr +
+ KNOD_IPSEC_GTT_OUT_L3_OFF +
+ (u64)i * KNOD_IPSEC_DECRYPT_WORK_SIZE;
+ }
+
+ /* Initialise this dispatcher's forward-looking sigval
+ * counter from its kaql[d] completion signal. Every
+ * submit decrements this so each in-flight slot has a
+ * distinct target.
+ */
+ signal = (struct amd_signal *)
+ priv->knod->kaql[d].queue_signal->kaddr;
+ disp->dispatch_sigval_next = READ_ONCE(signal->value);
+
+ pr_info("knod_ipsec: disp[%d] param=0x%llx decrypt=0x%llx kaql=%d\n",
+ d, (u64)disp->param_pool->gaddr,
+ (u64)disp->decrypt_pool->gaddr,
+ disp->kaql_idx);
+ }
+
+ return 0;
+}
+
+static void knod_ipsec_work_pool_free(struct knod_ipsec_priv *priv)
+{
+ int d;
+
+ for (d = 0; d < KNOD_IPSEC_MAX_DISPATCHERS; d++) {
+ struct knod_ipsec_dispatcher *disp = &priv->disp[d];
+
+ if (disp->decrypt_pool) {
+ knod_free_mem(priv->knod, disp->decrypt_pool);
+ disp->decrypt_pool = NULL;
+ }
+ if (disp->param_pool) {
+ knod_free_mem(priv->knod, disp->param_pool);
+ disp->param_pool = NULL;
+ }
+ }
+ memset(priv->work_pool, 0, sizeof(priv->work_pool));
+}
+
+
+/* ========================================================================
+ * Phase 5: in-kernel KAT (randomized + dispatch smoke)
+ * ========================================================================
+ *
+ * Two-layer validation:
+ *
+ * 1) CPU layer - randomized H-table check
+ * For each supported AES key length (128/192/256), run N iterations:
+ * generate a fresh random key, compute the reference H = AES_K(0^128)
+ * via the kernel's verified <crypto/aes.h> library, then recompute the
+ * same H via our gcm_core helper (which will be used on the GPU path)
+ * and require a bytewise match. This exercises aes_prepareenckey /
+ * aes_encrypt for every round count and the downstream gf128 squaring
+ * chain that builds the H-power table. No hand-computed constants.
+ *
+ * 2) GPU layer - multi-packet shader dispatch smoke test
+ * Run the fused RX shader at several batch sizes (1, 8, 32, 64) with
+ * each sub[i].bd_addr pointing at a distinct fake spsc_bd slot inside
+ * a single scratch BO. Pre-stamp each bd->act with a UNIQUE sentinel
+ * (sentinel base XORed with packet index) and verify the shader
+ * rewrites ALL slots to XDP_PASS. This covers workgroup_id_y indexing
+ * in the shader + variable grid dimensions in the AQL packet. When
+ * the real RX crypto shader lands, the per-slot verification can be
+ * upgraded to "decrypted inner packet matches expected plaintext".
+ *
+ * Randomness seeds vary per run so repeated `echo 1 > selftest` covers a
+ * widening input space over time. The shader layer is O(ms); the CPU
+ * layer runs in microseconds per iteration.
+ */
+
+#define KNOD_IPSEC_KAT_RAND_ROUNDS 16
+
+static int knod_ipsec_last_kat_result = -1;
+static char knod_ipsec_last_kat_detail[256];
+
+static int knod_ipsec_kat_h_one(const u8 *key, int key_len)
+{
+ u8 h_table[KNOD_GCM_H_TABLE_SIZE];
+ struct aes_enckey enckey;
+ u8 zero[AES_BLOCK_SIZE] = {};
+ u8 h_ref[AES_BLOCK_SIZE];
+ int rc;
+
+ rc = aes_prepareenckey(&enckey, key, key_len);
+ if (rc)
+ return rc;
+ aes_encrypt(&enckey, h_ref, zero);
+ memzero_explicit(&enckey, sizeof(enckey));
+
+ memset(h_table, 0, sizeof(h_table));
+ knod_gcm_precompute_h_table(key, key_len, h_table);
+
+ return memcmp(h_table, h_ref, AES_BLOCK_SIZE) == 0 ? 0 : -EBADMSG;
+}
+
+static int knod_ipsec_run_cpu_kat(void)
+{
+ static const int key_lens[] = { 16, 24, 32 };
+ u8 key[32];
+ int i, r, failed = 0;
+
+ for (i = 0; i < ARRAY_SIZE(key_lens); i++) {
+ int kl = key_lens[i];
+ int round_fail = 0;
+
+ for (r = 0; r < KNOD_IPSEC_KAT_RAND_ROUNDS; r++) {
+ get_random_bytes(key, kl);
+ if (knod_ipsec_kat_h_one(key, kl)) {
+ pr_err("knod_ipsec: H-KAT aes%d round=%d FAIL\n",
+ kl * 8, r);
+ round_fail++;
+ }
+ }
+ if (round_fail) {
+ failed += round_fail;
+ } else {
+ pr_info("knod_ipsec: H-KAT aes%d ok (%d random rounds)\n",
+ kl * 8, KNOD_IPSEC_KAT_RAND_ROUNDS);
+ }
+ }
+ memzero_explicit(key, sizeof(key));
+ return failed;
+}
+
+/*
+ * Shader-dispatch smoke test (post 32d.2 architecture pivot).
+ *
+ * Uses the persistent priv->kat_scratch BO as a private sandbox for both
+ * per-packet spsc_bd slots and synthetic ESP packet buffers:
+ *
+ * scratch[0 .. nr*64) -- spsc_bd slots (act at +i*64+8)
+ * scratch[pkt_region_base + i*64 ..) -- ETH+IPv4+ESP synthetic packet
+ *
+ * Each synthetic packet gets a unique SPI = SPI_KAT_BASE | i written in
+ * network byte order at ESP header offset (14 ETH + 20 IPv4 = 34). The
+ * corresponding slot in priv->sa_table is pre-populated with the same
+ * SPI (in host byte order) at index i. The fused RX shader reads
+ * sub[wg_id_y].pkt_addr, loads the BE SPI from pkt+34, byteswaps it,
+ * linear-scans priv->sa_table for a match, and writes the matching
+ * slot_idx (as u64) into bd->act.
+ *
+ * Verification scope (single pass):
+ * - even i (IPv4): bd->act high32 == 0xFFFFFFFD (ICV fail - garbage
+ * keys mean decryption produces wrong tag, but the full crypto
+ * pipeline runs to completion proving SPI scan + dispatch work)
+ * - odd i (IPv6): bd->act high32 == 0xFFFFFFFD (ICV fail - same as
+ * IPv4 but exercises the IPv6 ESP offset path; SPI at +54)
+ *
+ * The 32d.1/32d.2 in-shader replay bitmap path has been removed (the
+ * sliding window now lives CPU-side in the NIC dd NAPI), so the
+ * second dispatch pass and the replay_bitmap_addr/readback block have
+ * been dropped. End-to-end worker->desc_ring->NIC-dd delivery is covered
+ * by the userspace selftest `knod_ipsec_offload.sh`, not by this KAT.
+ */
+static int knod_ipsec_run_shader_kat_n(struct knod_ipsec_priv *priv, int nr)
+{
+ static const u64 SENTINEL_BASE = 0xDEADBEEFCAFEBABEULL;
+ static const u32 SPI_KAT_BASE = 0xDECAF000u;
+ const size_t SLOT_STRIDE = 64;
+ /* must fit IPv6 ESP minimum (86B overhead) */
+ const size_t PKT_STRIDE = 128;
+ /* Heap-backed sub[] - at BATCH=512 the stack copy would be 16 KB
+ * and overflow the dispatcher kernel stack. KAT is cold path, so
+ * kvmalloc is fine.
+ */
+ struct knod_ipsec_sa_entry *e_dbg;
+ struct knod_ipsec_sa_entry *e;
+ struct knod_ipsec_work *dbg_work;
+ struct knod_ipsec_fused_sub *sub;
+ struct amd_signal *sig;
+ struct knod_mem *scratch;
+ void *sa_backup = NULL;
+ size_t bd_region;
+ size_t pkt_region_base;
+ int written_final = 0;
+ s64 sig_before;
+ u64 dbg16 = 0;
+ int ret = 0;
+ int tries, i;
+
+ if (nr < 1 || nr > KNOD_IPSEC_KAT_MAX_BATCH)
+ return -EINVAL;
+
+ sub = kvcalloc(KNOD_IPSEC_KAT_MAX_BATCH, sizeof(*sub), GFP_KERNEL);
+ if (!sub) {
+ scnprintf(knod_ipsec_last_kat_detail,
+ sizeof(knod_ipsec_last_kat_detail),
+ "shader-dispatch[%d] FAIL: sub alloc", nr);
+ return -ENOMEM;
+ }
+
+ bd_region = SLOT_STRIDE * nr;
+ pkt_region_base = ALIGN(bd_region, 64);
+ if (pkt_region_base + nr * PKT_STRIDE > PAGE_SIZE * 4) {
+ ret = -EINVAL; /* defensive; kat_scratch is 4 pages */
+ goto out_free;
+ }
+
+ /* Reuse the persistent per-priv scratch BO - see priv->kat_scratch */
+ scratch = priv->kat_scratch;
+ if (!scratch) {
+ scnprintf(knod_ipsec_last_kat_detail,
+ sizeof(knod_ipsec_last_kat_detail),
+ "shader-dispatch[%d] FAIL: kat_scratch not allocated",
+ nr);
+ ret = -ENOMEM;
+ goto out_free;
+ }
+ memset(scratch->kaddr, 0, PAGE_SIZE * 4);
+
+ /* Back up the production SA table before trashing it with KAT
+ * fixtures. Without this, any production SA installed by the user
+ * (e.g. via `ip xfrm state add`) gets wiped when the KAT wipes the
+ * SA BO at exit, which causes a subsequent production dispatch
+ * to load key_gpu_addr=0 -> VM fault at address 0.
+ */
+ sa_backup = kvmalloc(KNOD_IPSEC_SA_BO_SIZE, GFP_KERNEL);
+ if (!sa_backup) {
+ scnprintf(knod_ipsec_last_kat_detail,
+ sizeof(knod_ipsec_last_kat_detail),
+ "shader-dispatch[%d] FAIL: sa_backup alloc", nr);
+ ret = -ENOMEM;
+ goto out_free;
+ }
+ memcpy(sa_backup, priv->sa_table->kaddr, KNOD_IPSEC_SA_BO_SIZE);
+
+ /* Pre-populate priv->sa_table[0..nr) with one entry per synthetic
+ * packet. Entry i carries spi = SPI_KAT_BASE | i in host byte order
+ * (the table is CPU-side LE and the shader reads it as a host u32).
+ * Higher-index slots are zeroed first so a previous KAT iteration
+ * at larger nr cannot leak into a smaller one.
+ */
+ BUILD_BUG_ON(sizeof(struct knod_ipsec_sa_entry) != 104);
+ BUILD_BUG_ON(KNOD_IPSEC_NR_SA < KNOD_IPSEC_KAT_MAX_BATCH);
+ e = (struct knod_ipsec_sa_entry *)priv->sa_table->kaddr;
+
+ /* Wipe entries + replay bitmap region (one BO). Size is
+ * ALIGNed to 8 pages in the alloc path to dodge the KNOD VRAM
+ * 7-page alloc bug; the logical payload is still SA_BO_SIZE.
+ */
+ memset(e, 0, KNOD_IPSEC_SA_BO_SIZE);
+ for (i = 0; i < nr; i++) {
+ e[i].spi = cpu_to_le32(SPI_KAT_BASE | (u32)i);
+ e[i].active = cpu_to_le32(1);
+ /* Full crypto shader loads key/htable/t_tables from
+ * the SA entry after scan hit. Point them at valid
+ * GPU addresses to avoid NULL page faults. The data
+ * is garbage so decryption will produce ICV mismatch,
+ * but the dispatch will complete without a VM fault.
+ */
+ e[i].key_gpu_addr = cpu_to_le64(scratch->gaddr);
+ e[i].htable_gpu_addr = cpu_to_le64(scratch->gaddr);
+ e[i].t_tables_gpu_addr = cpu_to_le64(
+ priv->t_tables ? priv->t_tables->gaddr :
+ scratch->gaddr);
+ e[i].key_len = cpu_to_le32(16);
+ e[i].nr_rounds = cpu_to_le32(10);
+ }
+ /* publish the key-table writes to WC VRAM before dispatch */
+ wmb();
+
+ /* sub was zero-filled by kvcalloc at entry. */
+ for (i = 0; i < nr; i++) {
+ u64 *act_i = (u64 *)((u8 *)scratch->kaddr +
+ i * SLOT_STRIDE + 8);
+ u8 *pkt_i = (u8 *)scratch->kaddr +
+ pkt_region_base + i * PKT_STRIDE;
+ __be32 spi_be = cpu_to_be32(SPI_KAT_BASE | (u32)i);
+ bool is_ipv4 = !(i & 1);
+
+ /* Pre-stamp bd->act with a sentinel so an untouched slot is
+ * distinguishable from one that was overwritten with the SPI.
+ */
+ *act_i = SENTINEL_BASE ^ (u64)i;
+
+ /* Alternate IPv4 / IPv6 packets per slot to exercise both
+ * code paths in the same batch. Even i: standard IPv4 header
+ * (version=4, IHL=5 -> byte 0x45), SPI at offset 34.
+ * Odd i: IPv6 header (version=6 -> byte 0x60), SPI at
+ * offset 54. Both hit the SA scan, both get ICV fail
+ * (since ciphertext is garbage).
+ */
+ pkt_i[14] = is_ipv4 ? 0x45 : 0x60;
+ if (is_ipv4)
+ memcpy(pkt_i + 34, &spi_be, sizeof(spi_be));
+ else
+ memcpy(pkt_i + 54, &spi_be, sizeof(spi_be));
+
+ sub[i].pkt_addr = cpu_to_le64(scratch->gaddr +
+ pkt_region_base + i * PKT_STRIDE);
+ sub[i].out_addr = sub[i].pkt_addr;
+ sub[i].bd_addr = cpu_to_le64(scratch->gaddr +
+ i * SLOT_STRIDE);
+ sub[i].pkt_len = cpu_to_le32(PKT_STRIDE);
+ }
+ /* publish the sub-descriptor writes to WC VRAM before dispatch */
+ wmb();
+
+ /* Single dispatch: post-32d.2 architecture has no in-shader replay,
+ * so the KAT only validates the fresh scan-hit path (+ the IPv6
+ * IPv6 ESP offset path). End-to-end worker->desc_ring->NAPI delivery
+ * is covered by the userspace selftest, not here.
+ */
+ /* Force the dispatch onto queue 0 so the signal we observe below is
+ * actually the one CP updates. The public rx_submit() picks a queue
+ * by smp_processor_id(), which would let the KAT measure a queue it
+ * never submitted to and see a stale sig=-1 forever.
+ */
+ sig = (struct amd_signal *)
+ priv->knod->kaql[0].queue_signal->kaddr;
+ sig_before = READ_ONCE(sig->value);
+ dbg_work = &priv->work_pool[0];
+
+ e_dbg = (struct knod_ipsec_sa_entry *)priv->sa_table->kaddr;
+
+ pr_info("knod_ipsec: KAT[%d] scratch.gaddr=0x%llx work.param.gaddr=0x%llx kernel.gaddr=0x%llx sa_table.gaddr=0x%llx cpu-readback: entry[0].spi=0x%08x entry[1].spi=0x%08x\n",
+ nr,
+ (u64)scratch->gaddr,
+ (u64)dbg_work->param.gaddr,
+ (u64)priv->knod->kernels[0]->gaddr,
+ (u64)priv->sa_table->gaddr,
+ le32_to_cpu(e_dbg[0].spi),
+ le32_to_cpu(e_dbg[1].spi));
+
+ if (priv->disp[0].kthread)
+ kthread_park(priv->disp[0].kthread);
+ knod_ipsec_prepare_rx_dispatch(priv, dbg_work, sub, NULL,
+ nr, NULL, 0);
+ knod_ipsec_dispatch_and_wait(&priv->disp[0], dbg_work);
+ if (priv->disp[0].kthread)
+ kthread_unpark(priv->disp[0].kthread);
+ ret = 0;
+ pr_info("knod_ipsec: shader-dispatch[%d] submitted, signal %lld -> (done)\n",
+ nr, sig_before);
+
+ /* bd->act is packed as
+ * low = s22 = scan target SPI
+ * high = s26 = final result
+ * even i (IPv4): ICV fail -> 0xFFFFFFFD
+ * odd i (IPv6): ICV fail -> 0xFFFFFFFD
+ * scan miss : 0xFFFFFFFF
+ *
+ * Both IPv4 and IPv6 paths run the full crypto pipeline. The SPI
+ * is placed at the correct offset (34 for v4, 54 for v6) so the
+ * SA scan finds a match, then decrypt + ICV check fails on garbage.
+ */
+ written_final = 0;
+#define KAT_EXPECT_LOW_MASK (0ULL)
+/* Both IPv4 and IPv6 slots: full crypto pipeline -> ICV mismatch
+ * -> verdict = VERDICT_ICV_FAIL (0xFFFFFFFD).
+ */
+#define KAT_EXPECT_FOR_SLOT \
+ (((u64)KNOD_IPSEC_SHADER_VERDICT_ICV_FAIL << 32) | 0ULL)
+
+ for (tries = 0; tries < 500; tries++) {
+ int w = 0;
+
+ for (i = 0; i < nr; i++) {
+ u64 expect = KAT_EXPECT_FOR_SLOT;
+ u64 mask = 0xFFFFFFFF00000000ULL |
+ KAT_EXPECT_LOW_MASK;
+ u64 v = READ_ONCE(*(u64 *)(
+ (u8 *)scratch->kaddr +
+ i * SLOT_STRIDE + 8));
+ if ((v & mask) == (expect & mask))
+ w++;
+ }
+ if (w == nr) {
+ written_final = w;
+ break;
+ }
+ written_final = w;
+ usleep_range(500, 1000);
+ }
+
+ if (written_final != nr) {
+ int first_ok = -1, first_bad = -1, last_ok = -1;
+ u64 first_bad_val = 0;
+ u64 first_bad_expect = 0;
+ struct amd_signal *sig = (struct amd_signal *)
+ priv->knod->kaql[0].queue_signal->kaddr;
+ s64 sig_after = READ_ONCE(sig->value);
+
+ for (i = 0; i < nr; i++) {
+ u64 expect = KAT_EXPECT_FOR_SLOT;
+ u64 mask = 0xFFFFFFFF00000000ULL |
+ KAT_EXPECT_LOW_MASK;
+ u64 v = READ_ONCE(*(u64 *)(
+ (u8 *)scratch->kaddr +
+ i * SLOT_STRIDE + 8));
+ if ((v & mask) == (expect & mask)) {
+ if (first_ok < 0)
+ first_ok = i;
+ last_ok = i;
+ } else if (first_bad < 0) {
+ first_bad = i;
+ first_bad_val = v;
+ first_bad_expect = expect;
+ }
+ }
+ dbg16 = 0;
+ if (first_bad >= 0) {
+ dbg16 = READ_ONCE(*(u64 *)(
+ (u8 *)scratch->kaddr +
+ first_bad * SLOT_STRIDE + 16));
+ }
+ scnprintf(knod_ipsec_last_kat_detail,
+ sizeof(knod_ipsec_last_kat_detail),
+ "shader-dispatch[%d] FAIL written=%d/%d first_bad=%d got=0x%llx (low=0x%08x high=0x%08x) expect=0x%llx sig_after=%lld first_ok=%d last_ok=%d dbg[s27,s23]=0x%08x 0x%08x",
+ nr, written_final, nr,
+ first_bad, first_bad_val,
+ (u32)first_bad_val,
+ (u32)(first_bad_val >> 32),
+ first_bad_expect, sig_after,
+ first_ok, last_ok,
+ (u32)dbg16,
+ (u32)(dbg16 >> 32));
+ pr_err("knod_ipsec: %s\n", knod_ipsec_last_kat_detail);
+ ret = -EIO;
+ goto out_free;
+ }
+#undef KAT_EXPECT_FOR_SLOT
+#undef KAT_EXPECT_LOW_MASK
+
+ pr_info("knod_ipsec: shader-dispatch[%d] ok (%d slots, %d us poll)\n",
+ nr, nr, tries * 750);
+
+out_free:
+ /* Restore the production SA table we saved at entry. Never wipe -
+ * wiping would destroy any live xfrm SAs installed by userspace.
+ */
+ if (sa_backup) {
+ memcpy(priv->sa_table->kaddr, sa_backup, KNOD_IPSEC_SA_BO_SIZE);
+ /* publish the restored SA table to the GPU */
+ wmb();
+ kvfree(sa_backup);
+ }
+ kvfree(sub);
+ return ret;
+}
+
+/* ========================================================================
+ * Crypto KAT: end-to-end AES-GCM decrypt verification via GPU shader
+ *
+ * Builds a synthetic ESP packet with known AES-128-GCM ciphertext+ICV,
+ * dispatches the fused RX shader, and verifies the decrypted output
+ * matches the expected plaintext. This exercises the full decrypt
+ * pipeline: T-table load, CTR decrypt, parallel GHASH, ICV verify,
+ * ESP trailer strip.
+ *
+ * Layout in kat_scratch (2 pages = 8192B):
+ * [0x0000..0x003F] bd slot (64B, act at +8)
+ * [0x0040..0x00FF] out buffer for decrypted output (192B)
+ * [0x0100..0x01FF] expanded AES round keys (256B)
+ * [0x0200..0x02FF] synthetic ESP packet (256B)
+ * [0x1000..0x1FFF] GHASH H-power table (4096B)
+ * ========================================================================
+ */
+
+/* GF(2^128) multiply for reference GHASH - identical to gcm_core but
+ * local to avoid exporting an internal helper for test-only use.
+ */
+static void kat_gf128_mul(u64 r[2], const u64 a[2], const u64 b[2])
+{
+ u64 v[2], z[2];
+ int i, j;
+
+ v[0] = a[0]; v[1] = a[1];
+ z[0] = 0; z[1] = 0;
+
+ for (i = 0; i < 2; i++) {
+ u64 x = b[i];
+
+ for (j = 63; j >= 0; j--) {
+ if ((x >> j) & 1) {
+ z[0] ^= v[0];
+ z[1] ^= v[1];
+ }
+ if (v[1] & 1) {
+ v[1] = (v[1] >> 1) | (v[0] << 63);
+ v[0] = (v[0] >> 1) ^ ((u64)0xe1 << 56);
+ } else {
+ v[1] = (v[1] >> 1) | (v[0] << 63);
+ v[0] = v[0] >> 1;
+ }
+ }
+ }
+ r[0] = z[0];
+ r[1] = z[1];
+}
+
+/* GHASH: hash `data` (must be multiple of 16 bytes) with key H.
+ * Result is stored in `out` (16 bytes, big-endian).
+ */
+static void kat_ghash(const u8 *h, const u8 *data, int data_len, u8 *out)
+{
+ u64 y[2] = { 0, 0 };
+ u64 hh[2];
+ int i;
+
+ hh[0] = get_unaligned_be64(h);
+ hh[1] = get_unaligned_be64(h + 8);
+
+ for (i = 0; i < data_len; i += 16) {
+ u64 d[2], r[2];
+
+ d[0] = get_unaligned_be64(data + i);
+ d[1] = get_unaligned_be64(data + i + 8);
+ y[0] ^= d[0];
+ y[1] ^= d[1];
+ kat_gf128_mul(r, y, hh);
+ y[0] = r[0];
+ y[1] = r[1];
+ }
+ put_unaligned_be64(y[0], out);
+ put_unaligned_be64(y[1], out + 8);
+}
+
+static int knod_ipsec_run_crypto_kat(struct knod_ipsec_priv *priv)
+{
+ /* Fixed AES-128 key + 4-byte salt (from RFC 4106 conventions). */
+ static const u8 kat_key[16] = {
+ 0xfe, 0xff, 0xe9, 0x92, 0x86, 0x65, 0x73, 0x1c,
+ 0x6d, 0x6a, 0x8f, 0x94, 0x67, 0x30, 0x83, 0x08
+ };
+ static const u8 kat_salt[4] = { 0xca, 0xfe, 0xba, 0xbe };
+ static const u8 kat_iv[8] = {
+ 0xfa, 0xce, 0xdb, 0xad, 0xde, 0xca, 0xf8, 0x88
+ };
+ /* 32-byte plaintext (2 AES blocks) + ESP trailer (pad_len=0,
+ * next_hdr=4).
+ * Total decrypted payload = 34 bytes, but ciphertext is padded to
+ * block boundary: 48 bytes (3 blocks) with 14 pad bytes + trailer.
+ * Actually simpler: use exactly 32 bytes of payload + 2 bytes trailer
+ * = 34 bytes, which is NOT block-aligned. The shader handles this
+ * because nblocks = ceil(34/16) = 3, and the last partial block is
+ * XORed with only the relevant bytes.
+ *
+ * Simplification: use 32 bytes ciphertext (2 full blocks) where
+ * the last 2 bytes are ESP trailer: byte[30]=pad_len=0, byte[31]=4
+ * (IPv4 next header). inner_len = 32 - 0 - 2 = 30.
+ */
+ static const u8 kat_plain[32] = {
+ 0xd9, 0x31, 0x32, 0x25, 0xf8, 0x84, 0x06, 0xe5,
+ 0xa5, 0x59, 0x09, 0xc5, 0xaf, 0xf5, 0x26, 0x9a,
+ 0x86, 0xa7, 0xa9, 0x53, 0x15, 0x34, 0xf7, 0xda,
+ 0x2e, 0x4c, 0x30, 0x3d, 0x8a, 0x31,
+ 0x00, /* pad_len = 0 */
+ 0x04, /* next_hdr = IPPROTO_IPIP (IPv4 tunnel) */
+ };
+ static const u32 kat_spi = 0xA5A5A5A5u;
+ static const u32 kat_seq = 0x00000001u;
+
+#define CRYPTO_KAT_BD_OFF 0x0000
+#define CRYPTO_KAT_OUT_OFF 0x0040
+#define CRYPTO_KAT_KEY_OFF 0x0100
+#define CRYPTO_KAT_PKT_OFF 0x0200
+#define CRYPTO_KAT_HTABLE_OFF 0x1000
+
+ struct knod_ipsec_sa_gpu_stats *gs;
+ struct knod_mem *scratch = priv->kat_scratch;
+ struct knod_ipsec_sa_entry *sa_entry;
+ struct knod_ipsec_fused_sub sub[1];
+ struct crypto_aes_ctx aes_ctx;
+ struct aes_enckey enckey;
+ void *sa_backup = NULL;
+ u8 nonce[12], ctr_blk[16], keystream[16];
+ u8 ciphertext[32], icv[16];
+ u8 ghash_input[80];
+ u8 h_block[16], ghash_out[16], j0_enc[16];
+ u8 *pkt, *out_buf, *key_buf, *htable_buf;
+ u64 *act_ptr;
+ u32 j0_ctr;
+ int ret, tries, i, b;
+
+ if (!scratch) {
+ scnprintf(knod_ipsec_last_kat_detail,
+ sizeof(knod_ipsec_last_kat_detail),
+ "crypto-kat FAIL: kat_scratch not allocated");
+ return -ENOMEM;
+ }
+ if (!priv->t_tables) {
+ scnprintf(knod_ipsec_last_kat_detail,
+ sizeof(knod_ipsec_last_kat_detail),
+ "crypto-kat FAIL: t_tables not allocated");
+ return -ENOMEM;
+ }
+
+ memset(scratch->kaddr, 0, PAGE_SIZE * 4);
+
+ pkt = (u8 *)scratch->kaddr + CRYPTO_KAT_PKT_OFF;
+ out_buf = (u8 *)scratch->kaddr + CRYPTO_KAT_OUT_OFF;
+ key_buf = (u8 *)scratch->kaddr + CRYPTO_KAT_KEY_OFF;
+ htable_buf = (u8 *)scratch->kaddr + CRYPTO_KAT_HTABLE_OFF;
+ act_ptr = (u64 *)((u8 *)scratch->kaddr + CRYPTO_KAT_BD_OFF + 8);
+
+ /* ---- Step 1: Expand AES key into kat_scratch ---- */
+ /* GPU round-key format uses crypto_aes_ctx.key_enc
+ * (matches xdo_state_add)
+ */
+ ret = aes_expandkey(&aes_ctx, kat_key, sizeof(kat_key));
+ if (ret) {
+ scnprintf(knod_ipsec_last_kat_detail,
+ sizeof(knod_ipsec_last_kat_detail),
+ "crypto-kat FAIL: aes_expandkey=%d", ret);
+ return ret;
+ }
+ /* AES-128: 11 round keys x 4 u32 = 44 u32 = 176B */
+ memcpy(key_buf, aes_ctx.key_enc,
+ (aes_ctx.key_length / 4 + 7) * 16);
+ /* Also prepare aes_enckey for CPU-side reference encryption */
+ ret = aes_prepareenckey(&enckey, kat_key, sizeof(kat_key));
+ if (ret) {
+ memzero_explicit(&aes_ctx, sizeof(aes_ctx));
+ return ret;
+ }
+
+ /* ---- Step 2: Precompute H-power table into kat_scratch page 2 ---- */
+ knod_gcm_precompute_h_table(kat_key, sizeof(kat_key), htable_buf);
+
+ /* ---- Step 3: CPU-side AES-GCM encrypt to produce ciphertext + ICV */
+ /* H = AES_K(0^128) */
+ memset(h_block, 0, 16);
+ aes_encrypt(&enckey, h_block, h_block);
+
+ /* Nonce = salt || IV */
+ memcpy(nonce, kat_salt, 4);
+ memcpy(nonce + 4, kat_iv, 8);
+
+ /* CTR encrypt: counter starts at 2 for payload blocks */
+ for (i = 0; i < 2; i++) {
+ u32 ctr_val = cpu_to_be32(i + 2);
+
+ memcpy(ctr_blk, nonce, 12);
+ memcpy(ctr_blk + 12, &ctr_val, 4);
+ aes_encrypt(&enckey, keystream, ctr_blk);
+
+ /* C_i = P_i XOR keystream */
+ for (b = 0; b < 16; b++)
+ ciphertext[i * 16 + b] =
+ kat_plain[i * 16 + b] ^ keystream[b];
+ }
+
+ /* GHASH over AAD(16) || ciphertext(32) || len_block(16) */
+ memset(ghash_input, 0, sizeof(ghash_input));
+ /* AAD = SPI(4B,BE) || seq(4B,BE) || zero-pad to 16B */
+ put_unaligned_be32(kat_spi, ghash_input);
+ put_unaligned_be32(kat_seq, ghash_input + 4);
+ /* ciphertext blocks */
+ memcpy(ghash_input + 16, ciphertext, 32);
+ /* len block: AAD_bitlen(64b) || ctext_bitlen(64b) */
+ /* 8 bytes AAD = 64 bits */
+ put_unaligned_be64(8ULL * 8, ghash_input + 48);
+ /* 32 bytes ctext = 256 bits */
+ put_unaligned_be64(32ULL * 8, ghash_input + 56);
+
+ kat_ghash(h_block, ghash_input, 64, ghash_out);
+
+ /* ICV = GHASH XOR AES_K(J0), where J0 = nonce || 0x00000001 (BE) */
+ j0_ctr = cpu_to_be32(1);
+ memcpy(ctr_blk, nonce, 12);
+ memcpy(ctr_blk + 12, &j0_ctr, 4);
+ aes_encrypt(&enckey, j0_enc, ctr_blk);
+ for (b = 0; b < 16; b++)
+ icv[b] = ghash_out[b] ^ j0_enc[b];
+ memzero_explicit(&enckey, sizeof(enckey));
+ memzero_explicit(&aes_ctx, sizeof(aes_ctx));
+
+ /* ---- Step 4: Build synthetic ESP packet ---- */
+ /* ETH header (14B): dst=00:..., src=00:..., ethertype=0x0800 */
+ pkt[12] = 0x08; pkt[13] = 0x00;
+ /* IPv4 header (20B): version=4, IHL=5, protocol=50(ESP) */
+ pkt[14] = 0x45;
+ /* total_length (network order):
+ * 20(IP) + 8(ESP) + 8(IV) + 32(ctext) + 16(ICV) = 84
+ */
+ pkt[16] = 0x00; pkt[17] = 84;
+ /* protocol = 50 (ESP) */
+ pkt[23] = 50;
+ /* SPI at +34 (network byte order) */
+ put_unaligned_be32(kat_spi, pkt + ESP_SPI_OFF);
+ /* Seq at +38 */
+ put_unaligned_be32(kat_seq, pkt + ESP_SEQ_OFF);
+ /* IV at +42 (8 bytes) */
+ memcpy(pkt + ESP_IV_OFF, kat_iv, 8);
+ /* Ciphertext at +50 (32 bytes) */
+ memcpy(pkt + ESP_CTEXT_OFF, ciphertext, 32);
+ /* ICV at +82 (16 bytes) */
+ memcpy(pkt + ESP_CTEXT_OFF + 32, icv, 16);
+
+ /* total pkt len = 14 + 84 = 98 bytes */
+#define CRYPTO_KAT_PKT_LEN 98
+
+ /* ---- Step 5: Set up SA entry (slot 0) in sa_table ---- */
+ BUILD_BUG_ON(sizeof(struct knod_ipsec_sa_entry) != 104);
+
+ /* Back up production SA table before trashing slot 0 with the KAT SA.
+ * Restored at out_wipe. Without this, any live xfrm SA in slot 0
+ * gets wiped and subsequent production dispatch faults on key=0.
+ */
+ sa_backup = kvmalloc(KNOD_IPSEC_SA_BO_SIZE, GFP_KERNEL);
+ if (!sa_backup) {
+ scnprintf(knod_ipsec_last_kat_detail,
+ sizeof(knod_ipsec_last_kat_detail),
+ "crypto-kat FAIL: sa_backup alloc");
+ return -ENOMEM;
+ }
+ memcpy(sa_backup, priv->sa_table->kaddr, KNOD_IPSEC_SA_BO_SIZE);
+ memset(priv->sa_table->kaddr, 0, KNOD_IPSEC_SA_BO_SIZE);
+
+ sa_entry = (struct knod_ipsec_sa_entry *)priv->sa_table->kaddr;
+ sa_entry->spi = cpu_to_le32(kat_spi);
+ sa_entry->dir = cpu_to_le32(0);
+ sa_entry->family = cpu_to_le32(AF_INET);
+ sa_entry->flags = cpu_to_le32(0);
+ sa_entry->key_gpu_addr = cpu_to_le64(scratch->gaddr +
+ CRYPTO_KAT_KEY_OFF);
+ sa_entry->htable_gpu_addr = cpu_to_le64(scratch->gaddr +
+ CRYPTO_KAT_HTABLE_OFF);
+ sa_entry->t_tables_gpu_addr = cpu_to_le64(priv->t_tables->gaddr);
+ memcpy(sa_entry->salt, kat_salt, 4);
+ sa_entry->key_len = cpu_to_le32(16);
+ sa_entry->nr_rounds = cpu_to_le32(10); /* AES-128 */
+ /* KAT uses tunnel mode */
+ sa_entry->mode = cpu_to_le32(XFRM_MODE_TUNNEL);
+ sa_entry->stats_addr = cpu_to_le64(priv->sa_table->gaddr +
+ KNOD_IPSEC_STATS_REGION_OFF);
+ sa_entry->active = cpu_to_le32(1);
+ sa_entry->version = cpu_to_le32(1);
+ /* publish the SA entry to the GPU before it becomes live */
+ wmb();
+
+ /* Pre-stamp bd->act with sentinel */
+ *act_ptr = 0xDEADDEADDEADDEADULL;
+
+ /* ---- Step 6: Build sub[] and dispatch ---- */
+ memset(sub, 0, sizeof(sub));
+ sub[0].pkt_addr = cpu_to_le64(scratch->gaddr + CRYPTO_KAT_PKT_OFF);
+ sub[0].out_addr = cpu_to_le64(scratch->gaddr + CRYPTO_KAT_OUT_OFF);
+ sub[0].bd_addr = cpu_to_le64(scratch->gaddr + CRYPTO_KAT_BD_OFF);
+ sub[0].pkt_len = cpu_to_le32(CRYPTO_KAT_PKT_LEN);
+ sub[0].result_seq = 0;
+
+ pr_info("knod_ipsec: crypto-kat: dispatching AES-128-GCM decrypt (spi=0x%08x, %d bytes ctext)\n",
+ kat_spi, 32);
+
+ if (priv->disp[0].kthread)
+ kthread_park(priv->disp[0].kthread);
+ knod_ipsec_prepare_rx_dispatch(priv, &priv->work_pool[0], sub, NULL,
+ 1, NULL, 0);
+ knod_ipsec_dispatch_and_wait(&priv->disp[0], &priv->work_pool[0]);
+ if (priv->disp[0].kthread)
+ kthread_unpark(priv->disp[0].kthread);
+ ret = 0;
+
+ /* ---- Step 7: Poll for completion ---- */
+ for (tries = 0; tries < 500; tries++) {
+ u64 v = READ_ONCE(*act_ptr);
+ u32 verdict_hi = (u32)(v >> 32);
+
+ if (verdict_hi != 0xDEADDEAD) {
+ if (verdict_hi == 0) {
+ /*
+ * Slot 0 = ICV passed!
+ * Verify decrypted output.
+ */
+ if (memcmp(out_buf, kat_plain, 32) != 0) {
+ pr_err("knod_ipsec: crypto-kat FAIL: plaintext mismatch\n");
+ print_hex_dump(KERN_ERR, " expected: ",
+ DUMP_PREFIX_NONE,
+ 16, 1, kat_plain, 32,
+ false);
+ print_hex_dump(KERN_ERR, " got: ",
+ DUMP_PREFIX_NONE,
+ 16, 1, out_buf, 32,
+ false);
+ scnprintf(knod_ipsec_last_kat_detail,
+ sizeof(knod_ipsec_last_kat_detail),
+ "crypto-kat FAIL: plaintext mismatch (verdict ok)");
+ ret = -EBADMSG;
+ goto out_wipe;
+ }
+ gs = (struct knod_ipsec_sa_gpu_stats *)
+ ((u8 *)priv->sa_table->kaddr +
+ KNOD_IPSEC_STATS_REGION_OFF);
+ pr_info("knod_ipsec: crypto-kat PASS: AES-128-GCM decrypt verified (%d us poll, gpu_stats: pkts=%llu bytes=%llu)\n",
+ tries * 750,
+ le64_to_cpu(gs->rx_packets),
+ le64_to_cpu(gs->rx_bytes));
+ ret = 0;
+ goto out_wipe;
+ } else if (verdict_hi == VERDICT_ICV_FAIL) {
+ pr_err("knod_ipsec: crypto-kat FAIL: ICV mismatch (verdict=0x%08x)\n",
+ verdict_hi);
+ print_hex_dump(KERN_ERR, " ref-icv: ",
+ DUMP_PREFIX_NONE,
+ 16, 1, icv, 16, false);
+ print_hex_dump(KERN_ERR, " out-buf: ",
+ DUMP_PREFIX_NONE,
+ 16, 1, out_buf, 32, false);
+ print_hex_dump(KERN_ERR, " ref-plain:",
+ DUMP_PREFIX_NONE,
+ 16, 1, kat_plain, 32, false);
+ scnprintf(knod_ipsec_last_kat_detail,
+ sizeof(knod_ipsec_last_kat_detail),
+ "crypto-kat FAIL: shader ICV mismatch");
+ ret = -EBADMSG;
+ goto out_wipe;
+ } else {
+ pr_err("knod_ipsec: crypto-kat FAIL: unexpected verdict=0x%08x (low=0x%08x)\n",
+ verdict_hi, (u32)v);
+ scnprintf(knod_ipsec_last_kat_detail,
+ sizeof(knod_ipsec_last_kat_detail),
+ "crypto-kat FAIL: verdict=0x%08x",
+ verdict_hi);
+ ret = -EIO;
+ goto out_wipe;
+ }
+ }
+ usleep_range(500, 1000);
+ }
+
+ pr_err("knod_ipsec: crypto-kat FAIL: timeout (bd->act=0x%016llx)\n",
+ READ_ONCE(*act_ptr));
+ scnprintf(knod_ipsec_last_kat_detail,
+ sizeof(knod_ipsec_last_kat_detail),
+ "crypto-kat FAIL: timeout");
+ ret = -ETIMEDOUT;
+
+out_wipe:
+ if (sa_backup) {
+ memcpy(priv->sa_table->kaddr, sa_backup, KNOD_IPSEC_SA_BO_SIZE);
+ /* publish the restored SA table to the GPU */
+ wmb();
+ kvfree(sa_backup);
+ }
+ return ret;
+
+#undef CRYPTO_KAT_BD_OFF
+#undef CRYPTO_KAT_OUT_OFF
+#undef CRYPTO_KAT_KEY_OFF
+#undef CRYPTO_KAT_PKT_OFF
+#undef CRYPTO_KAT_HTABLE_OFF
+#undef CRYPTO_KAT_PKT_LEN
+}
+
+static int knod_ipsec_run_shader_kat(struct knod_ipsec_priv *priv)
+{
+ static const int batch_sizes[] = { 1, 8, 32, KNOD_IPSEC_KAT_MAX_BATCH };
+ int i, failed = 0;
+ int last_ok = 0;
+
+ if (!priv->work_pool[0].param.kaddr) {
+ scnprintf(knod_ipsec_last_kat_detail,
+ sizeof(knod_ipsec_last_kat_detail),
+ "shader-dispatch SKIPPED (NOD not attached)");
+ pr_info("knod_ipsec: %s\n", knod_ipsec_last_kat_detail);
+ return 0;
+ }
+
+ for (i = 0; i < ARRAY_SIZE(batch_sizes); i++) {
+ int rc = knod_ipsec_run_shader_kat_n(priv, batch_sizes[i]);
+
+ if (rc)
+ failed++;
+ else
+ last_ok = batch_sizes[i];
+ }
+
+ if (!failed) {
+ scnprintf(knod_ipsec_last_kat_detail,
+ sizeof(knod_ipsec_last_kat_detail),
+ "shader-dispatch PASS (batch sizes 1/8/32/%d, last_ok=%d)",
+ KNOD_IPSEC_PKT_BATCH, last_ok);
+ pr_info("knod_ipsec: %s\n", knod_ipsec_last_kat_detail);
+ }
+ return failed;
+}
+
+static int knod_ipsec_run_kat(void)
+{
+ struct knod_ipsec_priv *priv = ipsec_priv;
+ int failed = 0;
+
+ failed += knod_ipsec_run_cpu_kat();
+
+ if (priv) {
+ failed += knod_ipsec_run_shader_kat(priv);
+ if (priv->work_pool[0].param.kaddr)
+ failed += knod_ipsec_run_crypto_kat(priv);
+ else
+ pr_info("knod_ipsec: crypto-kat SKIPPED (NOD not attached)\n");
+ } else {
+ scnprintf(knod_ipsec_last_kat_detail,
+ sizeof(knod_ipsec_last_kat_detail),
+ "shader-dispatch SKIPPED (priv not initialized)");
+ }
+
+ knod_ipsec_last_kat_result = failed;
+ return failed;
+}
+
+static void knod_ipsec_stats_sum(struct knod_ipsec_priv *priv,
+ struct knod_ipsec_stats *dst)
+{
+ int cpu;
+
+ memset(dst, 0, sizeof(*dst));
+ if (!priv)
+ return;
+
+ for_each_possible_cpu(cpu) {
+ struct knod_ipsec_stats *s = per_cpu_ptr(priv->stats, cpu);
+
+ dst->rx_packets += s->rx_packets;
+ dst->rx_bytes += s->rx_bytes;
+ dst->rx_dispatches += s->rx_dispatches;
+ dst->rx_batch_total += s->rx_batch_total;
+ if (s->rx_batch_max > dst->rx_batch_max)
+ dst->rx_batch_max = s->rx_batch_max;
+ dst->rx_sdma_copies_total += s->rx_sdma_copies_total;
+ dst->rx_sdma_bytes_total += s->rx_sdma_bytes_total;
+ if (s->rx_sdma_copies_max > dst->rx_sdma_copies_max)
+ dst->rx_sdma_copies_max = s->rx_sdma_copies_max;
+ dst->rx_drop_icv += s->rx_drop_icv;
+ dst->rx_drop_replay += s->rx_drop_replay;
+ dst->rx_drop_no_sa += s->rx_drop_no_sa;
+ dst->rx_drop_malformed += s->rx_drop_malformed;
+ dst->rx_drop_desc_full += s->rx_drop_desc_full;
+ dst->rx_drop_sdma_full += s->rx_drop_sdma_full;
+ dst->rx_build_ns += s->rx_build_ns;
+ dst->rx_gpu_ns += s->rx_gpu_ns;
+ dst->rx_sdma_ns += s->rx_sdma_ns;
+ dst->rx_finalise_ns += s->rx_finalise_ns;
+ dst->rx_total_ns += s->rx_total_ns;
+ dst->rx_idle_ns += s->rx_idle_ns;
+ dst->sa_add += s->sa_add;
+ dst->sa_del += s->sa_del;
+ dst->sa_rekey += s->sa_rekey;
+ dst->drain_calls += s->drain_calls;
+ dst->drain_found += s->drain_found;
+ dst->drain_delivered += s->drain_delivered;
+ dst->drain_alloc_ns += s->drain_alloc_ns;
+ dst->drain_copy_ns += s->drain_copy_ns;
+ dst->drain_proto_ns += s->drain_proto_ns;
+ dst->drain_gro_ns += s->drain_gro_ns;
+ dst->drain_total_ns += s->drain_total_ns;
+ dst->drain_zc_ok += s->drain_zc_ok;
+ dst->drain_zc_fallback += s->drain_zc_fallback;
+ dst->finish_produced += s->finish_produced;
+ dst->rx_peek_total += s->rx_peek_total;
+ dst->rx_submit_fail += s->rx_submit_fail;
+ }
+}
+
+static int knod_ipsec_stats_show(struct seq_file *s, void *v)
+{
+ struct knod_ipsec_priv *priv = s->private;
+ struct knod_ipsec_stats tot;
+ int i;
+
+ if (!priv)
+ return -ENODEV;
+
+ knod_ipsec_stats_sum(priv, &tot);
+
+ seq_printf(s, "stats_enabled : %d\n",
+ static_branch_unlikely(&ipsec_stats_enabled_key) ? 1 : 0);
+ seq_puts(s, "== RX ==\n");
+ seq_printf(s, "rx_packets : %llu\n", tot.rx_packets);
+ seq_printf(s, "rx_bytes : %llu\n", tot.rx_bytes);
+ seq_printf(s, "rx_dispatches : %llu\n", tot.rx_dispatches);
+ seq_printf(s, "rx_batch_total : %llu\n", tot.rx_batch_total);
+ seq_printf(s, "rx_batch_max : %llu\n", tot.rx_batch_max);
+ if (tot.rx_dispatches)
+ seq_printf(s, "rx_batch_avg : %llu\n",
+ tot.rx_batch_total / tot.rx_dispatches);
+ seq_printf(s, "rx_sdma_copies : %llu\n", tot.rx_sdma_copies_total);
+ seq_printf(s, "rx_sdma_cp_max : %llu\n", tot.rx_sdma_copies_max);
+ seq_printf(s, "rx_sdma_bytes : %llu\n", tot.rx_sdma_bytes_total);
+ if (tot.rx_dispatches) {
+ seq_printf(s, "rx_sdma_cp_avg : %llu\n",
+ tot.rx_sdma_copies_total / tot.rx_dispatches);
+ seq_printf(s, "rx_sdma_by_avg : %llu\n",
+ tot.rx_sdma_bytes_total / tot.rx_dispatches);
+ }
+ if (tot.rx_sdma_copies_total)
+ seq_printf(s, "rx_sdma_by_per : %llu\n",
+ tot.rx_sdma_bytes_total / tot.rx_sdma_copies_total);
+ seq_printf(s, "rx_drop_icv : %llu\n", tot.rx_drop_icv);
+ seq_printf(s, "rx_drop_replay : %llu\n", tot.rx_drop_replay);
+ seq_printf(s, "rx_drop_no_sa : %llu\n", tot.rx_drop_no_sa);
+ seq_printf(s, "rx_drop_malform: %llu\n", tot.rx_drop_malformed);
+ seq_printf(s, "rx_drop_descful: %llu\n", tot.rx_drop_desc_full);
+ seq_printf(s, "rx_drop_sdmaful: %llu\n", tot.rx_drop_sdma_full);
+ seq_puts(s, "-- RX timing (ns, summed across dispatches) --\n");
+ seq_printf(s, "rx_build_ns : %llu\n", tot.rx_build_ns);
+ seq_printf(s, "rx_gpu_ns : %llu\n", tot.rx_gpu_ns);
+ seq_printf(s, "rx_sdma_ns : %llu\n", tot.rx_sdma_ns);
+ seq_printf(s, "rx_finalise_ns : %llu\n", tot.rx_finalise_ns);
+ seq_printf(s, "rx_total_ns : %llu\n", tot.rx_total_ns);
+ seq_printf(s, "rx_idle_ns : %llu\n", tot.rx_idle_ns);
+ if (tot.rx_dispatches) {
+ seq_printf(s, "rx_build_avg_ns : %llu\n",
+ tot.rx_build_ns / tot.rx_dispatches);
+ seq_printf(s, "rx_gpu_avg_ns : %llu\n",
+ tot.rx_gpu_ns / tot.rx_dispatches);
+ seq_printf(s, "rx_sdma_avg_ns : %llu\n",
+ tot.rx_sdma_ns / tot.rx_dispatches);
+ seq_printf(s, "rx_finalise_avg_ns: %llu\n",
+ tot.rx_finalise_ns / tot.rx_dispatches);
+ seq_printf(s, "rx_total_avg_ns : %llu\n",
+ tot.rx_total_ns / tot.rx_dispatches);
+ }
+ seq_puts(s, "== Control ==\n");
+ seq_printf(s, "sa_add : %llu\n", tot.sa_add);
+ seq_printf(s, "sa_del : %llu\n", tot.sa_del);
+ seq_printf(s, "sa_rekey : %llu\n", tot.sa_rekey);
+ seq_puts(s, "== Debug ==\n");
+ seq_printf(s, "drain_calls : %llu\n", tot.drain_calls);
+ seq_printf(s, "drain_found : %llu\n", tot.drain_found);
+ seq_printf(s, "drain_delivered: %llu\n", tot.drain_delivered);
+ seq_puts(s, "-- drain_rx timing (ns, summed) --\n");
+ seq_printf(s, "drain_alloc_ns : %llu\n", tot.drain_alloc_ns);
+ seq_printf(s, "drain_copy_ns : %llu\n", tot.drain_copy_ns);
+ seq_printf(s, "drain_proto_ns : %llu\n", tot.drain_proto_ns);
+ seq_printf(s, "drain_gro_ns : %llu\n", tot.drain_gro_ns);
+ seq_printf(s, "drain_total_ns : %llu\n", tot.drain_total_ns);
+ if (tot.drain_delivered) {
+ seq_printf(s, "drain_alloc_per: %llu\n",
+ tot.drain_alloc_ns / tot.drain_delivered);
+ seq_printf(s, "drain_copy_per : %llu\n",
+ tot.drain_copy_ns / tot.drain_delivered);
+ seq_printf(s, "drain_proto_per: %llu\n",
+ tot.drain_proto_ns / tot.drain_delivered);
+ seq_printf(s, "drain_gro_per : %llu\n",
+ tot.drain_gro_ns / tot.drain_delivered);
+ seq_printf(s, "drain_total_per: %llu\n",
+ tot.drain_total_ns / tot.drain_delivered);
+ }
+ if (tot.drain_calls)
+ seq_printf(s, "drain_pkts_call: %llu\n",
+ tot.drain_delivered / tot.drain_calls);
+ seq_printf(s, "finish_produced: %llu\n", tot.finish_produced);
+ seq_printf(s, "rx_peek_total : %llu\n", tot.rx_peek_total);
+ seq_printf(s, "rx_submit_fail : %llu\n", tot.rx_submit_fail);
+ seq_printf(s, "drain_zc_ok : %llu\n", tot.drain_zc_ok);
+ seq_printf(s, "drain_zc_fallback: %llu\n", tot.drain_zc_fallback);
+
+ /* Per-queue SPSC ring state - shows where bds are stuck */
+ if (priv && priv->knodev) {
+ int nr_q = priv->knodev->netdev ?
+ priv->knodev->netdev->real_num_rx_queues : 0;
+
+ if (nr_q > KNOD_SPSC_MAX)
+ nr_q = KNOD_SPSC_MAX;
+ seq_puts(s, "== SPSC per-queue ==\n");
+ for (i = 0; i < nr_q; i++) {
+ struct spsc_ring *r = &priv->knodev->wpriv[i].spsc_bds;
+
+ if (!r->slots || r->mask == 0)
+ continue;
+ /* Only show queues with non-zero activity */
+ if (r->head == 0 && r->acquired == 0 && r->tail == 0)
+ continue;
+ seq_printf(s, " q%02d: head=%u acq=%u tail=%u (unpeek=%u inflight=%u)\n",
+ i, r->head, r->acquired, r->tail,
+ r->head - r->acquired,
+ r->acquired - r->tail);
+ }
+ }
+
+ /* Dispatcher */
+ if (priv) {
+ int d, n_running = 0;
+
+ for (d = 0; d < priv->nr_dispatchers; d++)
+ if (priv->disp[d].kthread)
+ n_running++;
+ seq_printf(s, "== Dispatchers ==\n running=%d/%d\n",
+ n_running, priv->nr_dispatchers);
+ for (d = 0; d < priv->nr_dispatchers; d++) {
+ struct knod_ipsec_dispatcher *disp = &priv->disp[d];
+
+ seq_printf(s,
+ " disp[%d]: kaql=%d work=[%d..%d) rxq=[%d..%d)\n",
+ d, disp->kaql_idx,
+ disp->work_first,
+ disp->work_first + disp->work_count,
+ disp->rxq_first,
+ disp->rxq_first + disp->rxq_count);
+ }
+ }
+
+ return 0;
+}
+
+static int knod_ipsec_stats_open(struct inode *inode, struct file *file)
+{
+ return single_open(file, knod_ipsec_stats_show, inode->i_private);
+}
+
+static const struct file_operations knod_ipsec_stats_fops = {
+ .owner = THIS_MODULE,
+ .open = knod_ipsec_stats_open,
+ .read = seq_read,
+ .llseek = seq_lseek,
+ .release = single_release,
+};
+
+static ssize_t knod_ipsec_stats_enable_write(struct file *file,
+ const char __user *ubuf,
+ size_t len, loff_t *ppos)
+{
+ char buf[4] = {};
+ int val;
+
+ if (len == 0 || len > sizeof(buf) - 1)
+ return -EINVAL;
+ if (copy_from_user(buf, ubuf, len))
+ return -EFAULT;
+ if (kstrtoint(strim(buf), 0, &val))
+ return -EINVAL;
+
+ if (val)
+ static_branch_enable(&ipsec_stats_enabled_key);
+ else
+ static_branch_disable(&ipsec_stats_enabled_key);
+ return len;
+}
+
+static int knod_ipsec_stats_enable_show(struct seq_file *s, void *v)
+{
+ seq_printf(s, "%d\n",
+ static_branch_unlikely(&ipsec_stats_enabled_key) ? 1 : 0);
+ return 0;
+}
+
+static int knod_ipsec_stats_enable_open(struct inode *inode, struct file *file)
+{
+ return single_open(file, knod_ipsec_stats_enable_show, NULL);
+}
+
+static const struct file_operations knod_ipsec_stats_enable_fops = {
+ .owner = THIS_MODULE,
+ .open = knod_ipsec_stats_enable_open,
+ .read = seq_read,
+ .write = knod_ipsec_stats_enable_write,
+ .llseek = seq_lseek,
+ .release = single_release,
+};
+
+static ssize_t knod_ipsec_poll_write(struct file *file,
+ const char __user *ubuf,
+ size_t len, loff_t *ppos)
+{
+ char buf[4] = {};
+ int val;
+
+ if (len == 0 || len > sizeof(buf) - 1)
+ return -EINVAL;
+ if (copy_from_user(buf, ubuf, len))
+ return -EFAULT;
+ if (kstrtoint(strim(buf), 0, &val))
+ return -EINVAL;
+
+ WRITE_ONCE(knod_ipsec_poll_mode, !!val);
+ return len;
+}
+
+static int knod_ipsec_poll_show(struct seq_file *s, void *v)
+{
+ seq_printf(s, "%d\n", READ_ONCE(knod_ipsec_poll_mode) ? 1 : 0);
+ return 0;
+}
+
+static int knod_ipsec_poll_open(struct inode *inode, struct file *file)
+{
+ return single_open(file, knod_ipsec_poll_show, NULL);
+}
+
+static const struct file_operations knod_ipsec_poll_fops = {
+ .owner = THIS_MODULE,
+ .open = knod_ipsec_poll_open,
+ .read = seq_read,
+ .write = knod_ipsec_poll_write,
+ .llseek = seq_lseek,
+ .release = single_release,
+};
+
+static ssize_t knod_ipsec_pkt_batch_write(struct file *file,
+ const char __user *ubuf,
+ size_t len, loff_t *ppos)
+{
+ struct knod_ipsec_priv *priv = file_inode(file)->i_private;
+ char buf[8] = {};
+ u32 val;
+
+ if (!priv)
+ return -ENODEV;
+ if (len == 0 || len > sizeof(buf) - 1)
+ return -EINVAL;
+ if (copy_from_user(buf, ubuf, len))
+ return -EFAULT;
+ if (kstrtou32(strim(buf), 0, &val))
+ return -EINVAL;
+ if (val < 1 || val > KNOD_IPSEC_PKT_BATCH)
+ return -ERANGE;
+
+ WRITE_ONCE(priv->pkt_batch, val);
+ return len;
+}
+
+static int knod_ipsec_pkt_batch_show(struct seq_file *s, void *v)
+{
+ struct knod_ipsec_priv *priv = s->private;
+
+ if (!priv)
+ return -ENODEV;
+ seq_printf(s, "%u\n", READ_ONCE(priv->pkt_batch));
+ return 0;
+}
+
+static int knod_ipsec_pkt_batch_open(struct inode *inode, struct file *file)
+{
+ return single_open(file, knod_ipsec_pkt_batch_show, inode->i_private);
+}
+
+static const struct file_operations knod_ipsec_pkt_batch_fops = {
+ .owner = THIS_MODULE,
+ .open = knod_ipsec_pkt_batch_open,
+ .read = seq_read,
+ .write = knod_ipsec_pkt_batch_write,
+ .llseek = seq_lseek,
+ .release = single_release,
+};
+
+static ssize_t knod_ipsec_stats_reset_write(struct file *file,
+ const char __user *ubuf,
+ size_t len, loff_t *ppos)
+{
+ struct knod_ipsec_priv *priv = file_inode(file)->i_private;
+ int cpu;
+
+ if (!priv)
+ return -ENODEV;
+
+ for_each_possible_cpu(cpu) {
+ struct knod_ipsec_stats *s = per_cpu_ptr(priv->stats, cpu);
+ u64 sa_add = s->sa_add, sa_del = s->sa_del;
+ u64 sa_rekey = s->sa_rekey;
+
+ memset(s, 0, sizeof(*s));
+ s->sa_add = sa_add;
+ s->sa_del = sa_del;
+ s->sa_rekey = sa_rekey;
+ }
+ return len;
+}
+
+static const struct file_operations knod_ipsec_stats_reset_fops = {
+ .owner = THIS_MODULE,
+ .open = simple_open,
+ .write = knod_ipsec_stats_reset_write,
+};
+
+static int knod_ipsec_sa_table_show(struct seq_file *s, void *v)
+{
+ struct knod_ipsec_priv *priv = s->private;
+ int i, used = 0;
+
+ if (!priv)
+ return -ENODEV;
+
+ mutex_lock(&priv->slot_lock);
+ for (i = 0; i < KNOD_IPSEC_NR_SA; i++) {
+ struct knod_ipsec_sa_slot *slot = &priv->slots[i];
+
+ if (!slot->active)
+ continue;
+ seq_printf(s, "slot[%3d] spi=0x%08x version=%u\n",
+ i, slot->spi, slot->version);
+ used++;
+ }
+ mutex_unlock(&priv->slot_lock);
+ seq_printf(s, "used: %d / %d\n", used, KNOD_IPSEC_NR_SA);
+ return 0;
+}
+
+static int knod_ipsec_sa_table_open(struct inode *inode, struct file *file)
+{
+ return single_open(file, knod_ipsec_sa_table_show, inode->i_private);
+}
+
+static const struct file_operations knod_ipsec_sa_table_fops = {
+ .owner = THIS_MODULE,
+ .open = knod_ipsec_sa_table_open,
+ .read = seq_read,
+ .llseek = seq_lseek,
+ .release = single_release,
+};
+
+/*
+ * Disassembly goes through the shared amdgcn_disasm_raw() (knod_amdgpu_insn.h):
+ * it classifies each raw instruction into a struct amdgcn_insn and prints it
+ * via the complete-opnames disassembler, so every feature shares one decoder
+ * instead of the old per-feature hand-rolled hex re-parsers.
+ */
+static int knod_ipsec_insn_show(struct seq_file *s, void *v)
+{
+ struct knod_ipsec_priv *priv = s->private;
+ struct kernel_descriptor *kd;
+ u32 *code;
+ int ndw, off;
+
+ if (!priv || !priv->knod || !priv->knod->kernels[0])
+ return -ENODEV;
+
+ kd = priv->knod->kernels[0]->kaddr;
+ code = priv->knod->kernels[0]->kaddr +
+ kd->kernel_code_entry_byte_offset;
+ ndw = max_t(int, 1, (int)priv->shader_size / 4);
+
+ seq_printf(s, "=== IPsec fused RX shader (GFX%d, %zu bytes, %d dwords) ===\n",
+ priv->isa_version, priv->shader_size, ndw);
+ seq_printf(s, "kernel_code_gaddr: 0x%llx\n\n",
+ priv->knod->kernels[0]->gaddr +
+ kd->kernel_code_entry_byte_offset);
+
+ off = 0;
+ while (off < ndw) {
+ int adv;
+
+ seq_printf(s, "%04x: ", off * 4);
+ adv = amdgcn_disasm_raw(priv->isa_version, &code[off],
+ ndw - off, s);
+ if (adv <= 0)
+ break;
+ off += adv;
+ }
+
+ if (ndw == 1 && code[0] == 0xBF810000)
+ seq_puts(s, "\n(empty shader: single s_endpgm)\n");
+
+ return 0;
+}
+
+static int knod_ipsec_insn_open(struct inode *inode, struct file *file)
+{
+ return single_open(file, knod_ipsec_insn_show, inode->i_private);
+}
+
+static const struct file_operations knod_ipsec_insn_fops = {
+ .owner = THIS_MODULE,
+ .open = knod_ipsec_insn_open,
+ .read = seq_read,
+ .llseek = seq_lseek,
+ .release = single_release,
+};
+
+static ssize_t knod_ipsec_selftest_write(struct file *file,
+ const char __user *ubuf,
+ size_t len, loff_t *ppos)
+{
+ char buf[4] = {};
+ int val;
+
+ if (len == 0 || len > sizeof(buf) - 1)
+ return -EINVAL;
+ if (copy_from_user(buf, ubuf, len))
+ return -EFAULT;
+ if (kstrtoint(strim(buf), 0, &val))
+ return -EINVAL;
+ if (val)
+ knod_ipsec_run_kat();
+ return len;
+}
+
+static int knod_ipsec_selftest_show(struct seq_file *s, void *v)
+{
+ seq_puts(s, "write 1 to trigger in-kernel KAT\n");
+ seq_puts(s, " cpu layer : H = AES_K(0^128) for aes128/192/256, ");
+ seq_printf(s, "%d random keys each\n", KNOD_IPSEC_KAT_RAND_ROUNDS);
+ seq_printf(s, " gpu layer : fused-shader dispatch at batch sizes 1/8/32/%d\n",
+ KNOD_IPSEC_PKT_BATCH);
+ seq_puts(s, " crypto layer: AES-128-GCM full decrypt+ICV verify via GPU shader\n");
+ seq_puts(s, " tx-crypto : AES-128-GCM full encrypt+ICV generate via GPU shader\n");
+ if (knod_ipsec_last_kat_result < 0) {
+ seq_puts(s, "last run: never\n");
+ } else if (knod_ipsec_last_kat_result == 0) {
+ seq_puts(s, "last run: PASS\n");
+ } else {
+ seq_printf(s, "last run: FAIL (%d checks failed)\n",
+ knod_ipsec_last_kat_result);
+ }
+ if (knod_ipsec_last_kat_detail[0])
+ seq_printf(s, "detail: %s\n", knod_ipsec_last_kat_detail);
+ return 0;
+}
+
+static int knod_ipsec_selftest_open(struct inode *inode, struct file *file)
+{
+ return single_open(file, knod_ipsec_selftest_show, NULL);
+}
+
+static const struct file_operations knod_ipsec_selftest_fops = {
+ .owner = THIS_MODULE,
+ .open = knod_ipsec_selftest_open,
+ .read = seq_read,
+ .write = knod_ipsec_selftest_write,
+ .llseek = seq_lseek,
+ .release = single_release,
+};
+
+/*
+ * debugfs lives under the shared knod ctx dir so IPsec files coexist
+ * with MACsec (aesgcm_selftest) and WG (wg subdir) under one tree:
+ * /sys/kernel/debug/dri/<N>/knod/ipsec/{stats,selftest,insn,...}
+ *
+ * The knod ctx is owned by knod_init/knod_exit, so debugfs lifecycle is
+ * tied to those callbacks rather than module init/exit.
+ */
+static void knod_ipsec_debugfs_init(struct knod_ipsec_priv *priv)
+{
+ struct dentry *parent;
+
+ if (!priv || !priv->knod || !priv->knod->debug_dir)
+ return;
+
+ parent = debugfs_create_dir("ipsec", priv->knod->debug_dir);
+ if (IS_ERR_OR_NULL(parent))
+ return;
+ priv->debug_dir = parent;
+
+ /* Populate this module's opcode-name tables for the shared
+ * disassembler (amdgcn_disasm_raw, used by the "insn" file). Each
+ * feature module carries its own copy of the opnames_gfx9/10 tables
+ * (knod_amdgpu_insn.h), so the tables must be initialised here rather
+ * than relying on the BPF module having done it.
+ */
+
+ debugfs_create_file("stats", 0444, parent,
+ priv, &knod_ipsec_stats_fops);
+ debugfs_create_file("stats_enable", 0644, parent,
+ priv, &knod_ipsec_stats_enable_fops);
+ debugfs_create_file("stats_reset", 0200, parent,
+ priv, &knod_ipsec_stats_reset_fops);
+ debugfs_create_file("sa_table", 0444, parent,
+ priv, &knod_ipsec_sa_table_fops);
+ debugfs_create_file("insn", 0444, parent,
+ priv, &knod_ipsec_insn_fops);
+ debugfs_create_file("selftest", 0644, parent,
+ priv, &knod_ipsec_selftest_fops);
+ debugfs_create_file("poll", 0644, parent,
+ priv, &knod_ipsec_poll_fops);
+ debugfs_create_file("pkt_batch", 0644, parent,
+ priv, &knod_ipsec_pkt_batch_fops);
+}
+
+static void knod_ipsec_debugfs_exit(struct knod_ipsec_priv *priv)
+{
+ if (!priv)
+ return;
+ debugfs_remove_recursive(priv->debug_dir);
+ priv->debug_dir = NULL;
+}
+
+/* ========================================================================
+ * Policy offload - accept PACKET-mode policies so xfrm_state_find()
+ * will match our PACKET-mode SAs. No GPU-side action needed; we just
+ * return 0 to let the kernel record the offload type on the policy.
+ * ========================================================================
+ */
+static int knod_ipsec_xdo_policy_add(struct knod_dev *knodev,
+ struct xfrm_policy *xp,
+ struct netlink_ext_ack *extack)
+{
+ return 0; /* accept unconditionally */
+}
+
+static void knod_ipsec_xdo_policy_delete(struct knod_dev *knodev,
+ struct xfrm_policy *xp)
+{
+ /* nothing to clean up on the GPU side */
+}
+
+static void knod_ipsec_xdo_policy_free(struct knod_dev *knodev,
+ struct xfrm_policy *xp)
+{
+ /* nothing to free */
+}
+
+/* ========================================================================
+ * knod_accel_ipsec_ops registration
+ * ========================================================================
+ */
+
+static struct knod_accel_ipsec_ops knod_ipsec_ops = {
+ /* feature select: alloc/free the IPsec GPU resources */
+ .activate = knod_ipsec_nod_init,
+ .deactivate = knod_ipsec_nod_exit,
+ .busy = knod_ipsec_nod_busy,
+ /* interface up/down (or feature switch): dispatchers + GPU drain */
+ .start = knod_ipsec_nod_start,
+ .stop = knod_ipsec_nod_stop,
+ .xdo_dev_state_add = knod_ipsec_xdo_state_add,
+ .xdo_dev_state_delete = knod_ipsec_xdo_state_delete,
+ .xdo_dev_state_free = knod_ipsec_xdo_state_free,
+ .xdo_dev_offload_ok = knod_ipsec_xdo_offload_ok,
+ .xdo_dev_state_advance_esn = knod_ipsec_xdo_state_advance_esn,
+ .xdo_dev_state_update_stats = knod_ipsec_xdo_state_update_stats,
+ .xdo_dev_policy_add = knod_ipsec_xdo_policy_add,
+ .xdo_dev_policy_delete = knod_ipsec_xdo_policy_delete,
+ .xdo_dev_policy_free = knod_ipsec_xdo_policy_free,
+};
+
+static int __init knod_ipsec_init(void)
+{
+ pr_debug("knod_ipsec: module load\n");
+
+ /* Publish our dispatcher-count requirement to the shared knod
+ * core before any NOD attach happens so knod_attach() creates a
+ * context with enough kaql/sdma pairs. knod_request_queue_cnt
+ * is a high-water mark so raising it here is idempotent and
+ * won't stomp on a larger value from another accel.
+ */
+ knod_request_queue_cnt(clamp(nr_dispatch, 1,
+ KNOD_IPSEC_MAX_DISPATCHERS));
+
+ /* knod_accel_ipsec_register() already attaches and inits the ipsec
+ * ops on every registered accel via knod_ipsec_attach(), so there is no
+ * separate per-accel init loop here: a second init() would only be
+ * rejected with -EBUSY by the ipsec_priv guard and log a misleading
+ * "init failed (-16)".
+ */
+ knod_dev_lock();
+ knod_accel_ipsec_register(&knod_ipsec_ops);
+ knod_dev_unlock();
+ return 0;
+}
+
+static void __exit knod_ipsec_exit(void)
+{
+ struct knod_accel *accel;
+
+ rtnl_lock();
+ knod_dev_lock();
+ for_each_accel(accel) {
+ if (!strncmp(accel->name, "amdgpu-", 7) && accel->knodev) {
+ if (accel->accel_ops->ipsec_ops)
+ accel->accel_ops->ipsec_ops->exit(
+ accel->knodev);
+ }
+ }
+ knod_accel_ipsec_unregister();
+ knod_dev_unlock();
+ rtnl_unlock();
+
+ pr_debug("knod_ipsec: module unload\n");
+}
+
+module_init(knod_ipsec_init);
+module_exit(knod_ipsec_exit);
+
+MODULE_DESCRIPTION("AMDGPU IPsec (xfrm) full-packet offload via KNOD");
+MODULE_AUTHOR("Taehee Yoo <ap420073@gmail.com>");
+MODULE_LICENSE("GPL");
diff --git a/drivers/gpu/drm/amd/amdkfd/knod/knod_ipsec.h b/drivers/gpu/drm/amd/amdkfd/knod/knod_ipsec.h
new file mode 100644
index 000000000000..3754a86d6c63
--- /dev/null
+++ b/drivers/gpu/drm/amd/amdkfd/knod/knod_ipsec.h
@@ -0,0 +1,596 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (c) 2021 Taehee Yoo <ap420073@gmail.com>
+ * Copyright (c) 2021 Hoyeon Lee <hoyeon.rhee@gmail.com>
+ */
+
+#ifndef KNOD_IPSEC_H_
+#define KNOD_IPSEC_H_
+
+#include <linux/types.h>
+#include <linux/xarray.h>
+#include <linux/mutex.h>
+#include <linux/jump_label.h>
+#include <linux/list.h>
+#include <linux/spinlock.h>
+#include <linux/ktime.h>
+#include <linux/mmzone.h>
+#include <net/spsc_ring.h>
+#include <net/knod.h> /* KNOD_SPSC_MAX */
+
+struct knod;
+struct knod_mem;
+struct dentry;
+struct xfrm_state;
+struct sk_buff;
+struct task_struct;
+struct net_device;
+struct napi_struct;
+struct knod_ipsec_priv;
+
+#define KNOD_IPSEC_NR_SA 256
+/* The shader-dispatch KAT installs one fake SA per fused_sub entry, so
+ * its maximum batch size is bounded by NR_SA. Keep separate from
+ * KNOD_IPSEC_PKT_BATCH so production can run larger batches without
+ * growing the SA table BO.
+ */
+#define KNOD_IPSEC_KAT_MAX_BATCH 64
+/* 128-bit window, 4 u32 words */
+#define KNOD_IPSEC_REPLAY_WORDS 4
+#define KNOD_IPSEC_REPLAY_BITS (KNOD_IPSEC_REPLAY_WORDS * 32)
+#define KNOD_IPSEC_REPLAY_BYTES (KNOD_IPSEC_REPLAY_WORDS * 4)
+
+/*
+ * CPU-side per-queue sliding anti-replay window (knod_ipsec_sa_window).
+ * Independent from the GPU-side REPLAY_BITS above - the shader's
+ * replay region stays 128 bits for GPU-visible state, but the CPU path
+ * needs a much larger window because with multi-queue RSS the packets
+ * of a single SA may land on several RX queues due to NIC hash
+ * collisions. Each per-queue window then sees only a sparse subset of
+ * the SA's monotonic seq stream, and the seq gap between consecutive
+ * packets arriving at one queue can exceed 128 easily. 2048 bits lets
+ * the window absorb worst-case gaps (e.g. 8 queues x 256 batch).
+ *
+ * Sized as u64 words for easier bit-shift logic in the check function.
+ */
+#define KNOD_IPSEC_CPU_REPLAY_WORDS 32 /* 32 * 64 = 2048 bits */
+#define KNOD_IPSEC_CPU_REPLAY_BITS \
+ (KNOD_IPSEC_CPU_REPLAY_WORDS * 64)
+
+/*
+ * Per-dispatch packet batch. The RX fused shader launches grid_y=nr_packets
+ * workgroups and each workgroup indexes its own sub[wg_y] entry, so batch
+ * scales linearly with GPU occupancy up to the LDS-bound concurrent
+ * workgroup limit (~1024 on 64-CU gfx9 with 4 KB LDS per WG). Bumping this
+ * grows:
+ * - struct knod_ipsec_fused_param (sub[] inline in kernarg)
+ * - work_decrypt_pool VRAM (BATCH * DECRYPT_PKT_SIZE)
+ * - struct knod_ipsec_work (rx_bds[], rx_pkt_queue[], rx_pending[])
+ */
+#define KNOD_IPSEC_PKT_BATCH 512
+
+/*
+ * Kernarg layout consumed by the fused RX shader.
+ *
+ * The shader dereferences sa_table_addr/t_tables_addr from the top-level
+ * struct and then iterates `sub[]` for per-packet work. Decrypted inner
+ * packets are written to `out_addr` (VRAM); a CPU finish worker later
+ * SDMA-copies them into a per-queue framework delivery-pool page and
+ * publishes a `knod_pass_desc` onto the framework pass_pending ring. The
+ * bd ring is not used for verdict delivery.
+ */
+struct knod_ipsec_fused_sub {
+ __le64 pkt_addr; /* raw packet VRAM addr (ETH start) */
+ __le64 out_addr; /* decrypted inner packet dest */
+ __le64 bd_addr; /* SPSC bd for direct verdict write */
+ __le32 pkt_len;
+ __le32 result_seq; /* shader writes bswapped ESP seq here */
+};
+
+struct knod_ipsec_fused_param {
+ __le64 sa_table_addr; /* 0 */
+ __le64 t_tables_addr; /* 8 */
+ __le64 sdma_ring_addr; /* 16: SDMA ring buffer gaddr */
+ __le32 nr_sa; /* 24 */
+ __le32 family_filter; /* 28 */
+ __le64 sdma_ctl_addr; /* 32: GPU VA of sdma_ctl region */
+ struct knod_ipsec_fused_sub sub[KNOD_IPSEC_PKT_BATCH]; /* 40 */
+};
+
+/*
+ * GPU-visible SA table entry. Layout is shared with the GPU shader and
+ * must remain stable / packed.
+ */
+struct knod_ipsec_sa_entry {
+ /* network byte order in wire, LE in table */
+ __le32 spi;
+ __le32 dir; /* 0=IN, 1=OUT */
+ __le32 family; /* AF_INET=2, AF_INET6=10 */
+ __le32 flags; /* bit0: ESN, bit1: CRYPT_ONLY */
+ __le64 key_gpu_addr; /* AES key buffer (VRAM) */
+ __le64 htable_gpu_addr; /* GHASH H-power table (VRAM) */
+ __le64 t_tables_gpu_addr; /* shared T-tables (VRAM) */
+ u8 salt[4];
+ __le32 key_len; /* 16/24/32 */
+ __le32 nr_rounds; /* AES rounds: 10/12/14 */
+ /* XFRM_MODE_TRANSPORT=0, XFRM_MODE_TUNNEL=1 */
+ __le32 mode;
+ __le64 replay_bitmap_addr; /* GPU-visible replay bitmap */
+ __le32 replay_window; /* 64/128/256 */
+ __le32 seq_hi; /* ESN high-32 */
+ __le64 seq_last; /* last accepted sequence number */
+ __le32 active;
+ __le32 version; /* rekey protection */
+ __le64 stats_addr; /* per-SA counters (optional) */
+ __le64 _pad1;
+};
+
+#define KNOD_IPSEC_SA_ENTRY_SIZE sizeof(struct knod_ipsec_sa_entry)
+#define KNOD_IPSEC_SA_TABLE_SIZE \
+ (KNOD_IPSEC_NR_SA * KNOD_IPSEC_SA_ENTRY_SIZE)
+
+/*
+ * Replay bitmap + per-SA stats regions are appended to the SA entry table
+ * inside the same backing BO. Keeping everything in one BO avoids the
+ * multi-BO GPU VA mapping bug (see gtt_multi_bo_bug.md).
+ */
+#define KNOD_IPSEC_REPLAY_REGION_SIZE \
+ (KNOD_IPSEC_NR_SA * KNOD_IPSEC_REPLAY_BYTES)
+#define KNOD_IPSEC_REPLAY_REGION_OFF KNOD_IPSEC_SA_TABLE_SIZE
+
+/*
+ * Per-SA GPU-visible stats. The shader atomically increments these via
+ * global_atomic_add_x2 on every successful decrypt. CPU reads them back
+ * in xdo_state_update_stats to feed x->curlft.
+ */
+struct knod_ipsec_sa_gpu_stats {
+ __le64 rx_packets;
+ __le64 rx_bytes;
+};
+
+#define KNOD_IPSEC_SA_STATS_SIZE sizeof(struct knod_ipsec_sa_gpu_stats)
+#define KNOD_IPSEC_STATS_REGION_SIZE \
+ (KNOD_IPSEC_NR_SA * KNOD_IPSEC_SA_STATS_SIZE)
+#define KNOD_IPSEC_STATS_REGION_OFF \
+ (KNOD_IPSEC_REPLAY_REGION_OFF + KNOD_IPSEC_REPLAY_REGION_SIZE)
+
+#define KNOD_IPSEC_SA_BO_SIZE \
+ (KNOD_IPSEC_SA_TABLE_SIZE + KNOD_IPSEC_REPLAY_REGION_SIZE + \
+ KNOD_IPSEC_STATS_REGION_SIZE)
+
+/*
+ * RFC 4303 anti-replay sliding window - CPU-side, per-SA, per-RXQ.
+ *
+ * RSS hashes ESP flows on (saddr, daddr, proto, SPI) so that every packet
+ * of a given SA lands on the same NIC RX queue. That means a single writer
+ * (the NIC dd NAPI for that queue) owns the window and no locking is
+ * required on the fast path. Replicated per queue because different SAs
+ * may still be hashed to different queues, and we do not want false
+ * sharing between queues on a shared cacheline.
+ */
+struct knod_ipsec_sa_window {
+ u64 top_seq; /* highest accepted seq */
+ /* N*64-bit sliding window */
+ u64 bitmap[KNOD_IPSEC_CPU_REPLAY_WORDS];
+};
+
+/* CPU-side slot metadata */
+struct knod_ipsec_sa_slot {
+ struct xfrm_state *x; /* back pointer (CPU only) */
+ struct knod_mem *key_mem;
+ struct knod_mem *htable_mem;
+ struct knod_mem *replay_mem;
+ u32 spi; /* host order */
+ u32 slot_idx;
+ u32 version;
+ bool active;
+ /* Per-RXQ sliding window state. Owned by the NIC dd NAPI for that
+ * queue - do not touch from control plane while SA is active.
+ */
+ struct knod_ipsec_sa_window win[KNOD_SPSC_MAX];
+};
+
+/* Percpu stats for observability */
+struct knod_ipsec_stats {
+ /* RX */
+ u64 rx_packets;
+ u64 rx_bytes;
+ u64 rx_dispatches;
+ u64 rx_batch_total;
+ u64 rx_batch_max;
+ /* SDMA copy observability: per dispatch we emit 1 copy per raw-bypass
+ * or tunnel packet, 2 copies per transport packet (outer L3 + inner
+ * payload). rx_sdma_copies_total accumulates every call; max tracks
+ * the peak single-dispatch count; bytes tracks total DMA volume.
+ */
+ u64 rx_sdma_copies_total;
+ u64 rx_sdma_copies_max;
+ u64 rx_sdma_bytes_total;
+ u64 rx_drop_icv;
+ u64 rx_drop_replay;
+ u64 rx_drop_no_sa;
+ u64 rx_drop_malformed;
+ u64 rx_drop_desc_full;
+ u64 rx_drop_sdma_full;
+ /* Per-phase RX dispatch timings, accumulated per percpu counter.
+ * Hot path guarded by ipsec_stats_enabled_key static branch so
+ * they cost zero cycles when disabled.
+ *
+ * rx_build_ns : try_rx pre-scan + per-queue drain loop
+ * (CPU staging work into kernarg sub[]).
+ * rx_gpu_ns : spin on GPU completion signal.
+ * rx_sdma_ns : spin on SDMA fence after finish scheduled
+ * per-packet SDMA copies.
+ * rx_finalise_ns : finish_rx_deliver CPU work excluding SDMA
+ * fence wait (verdict loop + desc publish +
+ * per-queue NAPI schedule).
+ * rx_total_ns : end-to-end try_rx call time (build + dispatch
+ * wait + finalise + napi kicks).
+ * rx_idle_ns : time the dispatcher spent in usleep_range
+ * waiting for work when both try_tx and
+ * try_rx returned false.
+ */
+ u64 rx_build_ns;
+ u64 rx_gpu_ns;
+ u64 rx_sdma_ns;
+ u64 rx_finalise_ns;
+ u64 rx_total_ns;
+ u64 rx_idle_ns;
+ /* Control plane */
+ u64 sa_add;
+ u64 sa_del;
+ u64 sa_rekey;
+ /* Debug: drain_rx pipeline visibility */
+ u64 drain_calls;
+ u64 drain_found;
+ u64 drain_delivered;
+ u64 finish_produced;
+ u64 rx_peek_total;
+ u64 rx_submit_fail;
+ /* drain_rx per-phase timing (ns, summed). Each drain_rx call
+ * processes `drain_found` descriptors; these buckets split the
+ * CPU work per-phase so we can see which step dominates:
+ * drain_alloc_ns : knod_pass_build_skb cost
+ * drain_copy_ns : legacy copy cost (0 - delivery is zero-copy)
+ * drain_proto_ns : L3 header patch + secpath setup
+ * drain_gro_ns : netif_receive_skb_list (stack entry)
+ * drain_total_ns : end-to-end drain_rx call time
+ * Guarded by ipsec_stats_enabled_key so zero cost when off.
+ */
+ u64 drain_alloc_ns;
+ u64 drain_copy_ns;
+ u64 drain_proto_ns;
+ u64 drain_gro_ns;
+ u64 drain_total_ns;
+ u64 drain_zc_ok;
+ u64 drain_zc_fallback;
+};
+
+/*
+ * Work-slot pool owned by the dispatcher kthread. Depth-2 pipelining:
+ * while one slot is executing on the GPU, the dispatcher can build and
+ * submit the next batch into the other slot and finalise the one that
+ * just completed. kaql[0] is still a single AQL queue; multiple
+ * in-flight dispatches are queued in-order and complete in-order.
+ *
+ * A slot's lifecycle:
+ * EMPTY -> try_tx/try_rx fills kernarg and submits -> INFLIGHT
+ * INFLIGHT -> GPU running; dispatcher polls completion signal
+ * INFLIGHT -> signal fires -> start SDMA copies + fence (no spin)
+ * -> SDMA_PENDING
+ * SDMA_PENDING -> dispatcher polls SDMA fence (non-blocking)
+ * SDMA_PENDING -> fence done -> desc publish, napi kicks, bd PASS,
+ * stats -> EMPTY
+ *
+ * The SDMA_PENDING state decouples the SDMA fence wait from the
+ * dispatcher loop so the CPU never busy-spins on the fence. While
+ * one slot sits in SDMA_PENDING, the dispatcher can build and
+ * submit the next batch into another EMPTY slot - true 3-way
+ * parallelism of GPU execution, SDMA transfer, and CPU build.
+ */
+#define KNOD_IPSEC_NR_WORK 4
+
+/*
+ * Front offset reserved in each delivery page (= IPv4 transport L3 size).
+ * The shader decrypts into a per-work VRAM output buffer; finish_rx_deliver
+ * then SDMA-lays the packet into the delivery page by mode:
+ *
+ * transport: outer L3 header (20B IPv4 / 40B IPv6) to page+0, decrypted
+ * L4 payload to page+l3_len; inner_off = 0.
+ * tunnel: decrypted inner L3 packet to page+GTT_OUT_L3_OFF, leaving
+ * 20B of headroom at the front; inner_off = 20.
+ *
+ * (The name is a holdover from a removed mode where the shader wrote
+ * straight into a GTT slot at this offset.)
+ */
+#define KNOD_IPSEC_GTT_OUT_L3_OFF 20
+
+/*
+ * GPU-initiated SDMA control block, placed at a fixed offset after
+ * knod_ipsec_fused_param in the same kernarg BO. The shader's Phase 12
+ * uses atomic counters here to coordinate multi-workgroup SDMA ring
+ * writes without CPU involvement.
+ */
+#define KNOD_IPSEC_SDMA_CTL_OFF \
+ ALIGN(sizeof(struct knod_ipsec_fused_param), 64)
+
+struct knod_ipsec_sdma_ctl {
+ /* 0: atomic - SDMA-needing WGs increment */
+ __le32 claim_counter;
+ __le32 done_counter; /* 4: atomic - ALL WGs increment */
+ /* 8: current wptr byte offset (CPU snapshot) */
+ __le64 wptr_val;
+ __le64 fence_addr; /* 16: SDMA fence write target GPU VA */
+ /* 24: value SDMA writes on completion */
+ __le32 fence_val;
+ /* 28: wptr_val / 4 (dword index into ring) */
+ __le32 wptr_base_dw;
+ __le32 ring_mask; /* 32: (ring_size_bytes/4) - 1 */
+ __le32 nr_total_wg; /* 36: grid_size_y = nr_packets */
+ __le32 copy_hdr; /* 40: SDMA COPY_LINEAR header dword */
+ __le32 fence_hdr; /* 44: SDMA FENCE header dword */
+ __le32 gpu_sdma_ready; /* 48: last WG sets 1 -> CPU polls */
+ /* 52: total SDMA COPY packets emitted */
+ __le32 final_sdma_count;
+ /* 56: GPU VA of HW wptr (queue->gaddr+8) */
+ __le64 wptr_gpu_addr;
+};
+
+enum knod_ipsec_work_state {
+ KNOD_WORK_EMPTY = 0,
+ KNOD_WORK_INFLIGHT,
+ KNOD_WORK_SDMA_PENDING,
+};
+
+/*
+ * Per-work RX decrypt output buffer size. AES-CTR decrypt writes plaintext
+ * here instead of overwriting the ciphertext in-place (which would corrupt
+ * the data before GHASH reads it). One slot per packet in the batch.
+ *
+ * Must hold the largest expected plaintext (ctext_len, before ESP trailer
+ * strip). Sized for MTU 9000 ESP jumbo frames (up to ~9200B total),
+ * rounded up to 16 KB. At PKT_BATCH=64 the total pool is 1 MB of VRAM.
+ * Smaller values silently corrupt adjacent slots and eventually fault
+ * past the end of the pool BO.
+ */
+#define KNOD_IPSEC_DECRYPT_PKT_SIZE 16384
+#define KNOD_IPSEC_DECRYPT_WORK_SIZE \
+ ((size_t)KNOD_IPSEC_PKT_BATCH * KNOD_IPSEC_DECRYPT_PKT_SIZE)
+
+/*
+ * Per-work kernarg and decrypt buffers are SLICES of a single large
+ * BO owned by `struct knod_ipsec_works`, not individual BOs. Allocating
+ * many small BOs and mapping them all to the KFD process GPU VA hits a
+ * long-standing AMDKFD issue where only the first BO is reliably mapped
+ * (see memory/gtt_multi_bo_bug.md) - subsequent BOs fault on GPU access.
+ * One large pool BO, sliced at fixed offsets, sidesteps this entirely.
+ */
+struct knod_ipsec_slice {
+ void *kaddr;
+ u64 gaddr;
+};
+
+/*
+ * Per-packet finish state built by knod_ipsec_finish_rx_deliver() while it
+ * schedules SDMA copies, then read back to publish desc_ring entries.
+ *
+ * Kept as an array inside struct knod_ipsec_work so the dispatcher does
+ * not have to stack-allocate BATCH * sizeof(...) on every finalise - at
+ * large PKT_BATCH values (512+) stack allocation would overflow the
+ * 16 KB kernel stack.
+ */
+struct knod_ipsec_rx_pending {
+ netmem_ref netmem; /* delivery page from framework pass_pool */
+ u32 inner_len; /* bytes copied into the delivery page */
+ u32 sa_slot; /* SA table index, or KNOD_IPSEC_NR_SA (raw) */
+ u16 rxq_idx; /* which priv->rxq[] this packet belongs to */
+ u8 mode; /* XFRM_MODE_TRANSPORT / TUNNEL */
+ u8 next_hdr; /* ESP trailer next_hdr */
+ u8 family; /* AF_INET / AF_INET6 */
+ u8 inner_off; /* byte offset within the delivery page */
+ u8 _pad[2];
+};
+
+struct knod_ipsec_work {
+ /* Views into the shared pools in priv. No per-work BO. */
+ struct knod_ipsec_slice param; /* kernarg slice (VRAM pool) */
+ /* RX decrypt output (VRAM pool) */
+ u64 rx_out_gaddr;
+ /* Pipelined slot state + deferred finalise tracking. The dispatcher
+ * sets state to INFLIGHT on submit, polls completion, then flips to
+ * EMPTY after finalise. Timestamps / per-queue napi info captured
+ * at build time get consumed when finalise runs later.
+ */
+ enum knod_ipsec_work_state state;
+ u64 t_build_start;
+ u64 t_build_end;
+ u64 t_submit;
+ u64 t_finalise_start;
+ /* Per-queue tracker for deferred napi_schedule. try_rx records
+ * which NIC RX queues had packets drained into this slot so the
+ * finalise path (running later, possibly one iteration later) can
+ * wake those NAPIs after finalise completes.
+ */
+ u16 per_q_touched[KNOD_SPSC_MAX];
+ int per_q_n;
+ /* Per-dispatch state (owned by dispatcher) */
+ s64 sigval;
+ u64 dispatch_ts;
+ /* SDMA deferred-fence state. When the work transitions from
+ * INFLIGHT -> SDMA_PENDING, finish_rx_deliver queues copies +
+ * fence but does NOT spin. The dispatcher checks sdma_fence_ptr
+ * on the next iteration and transitions to EMPTY once the fence
+ * fires. sdma_fence_target is the expected fence value; the
+ * pointer is the host-visible signal->value location.
+ */
+ u32 sdma_fence_target;
+ s64 *sdma_fence_ptr;
+ u64 sdma_submit_ns;
+ /* Carry the SDMA fence wait time out of finish_rx_deliver so
+ * the caller (try_rx) can subtract it from the finalise phase
+ * and report it as rx_sdma_ns. Zero when stats are disabled.
+ */
+ u64 sdma_wait_ns;
+ /* Per-dispatch SDMA copy accounting (populated by finish_rx_deliver).
+ * rx_sdma_copies = number of knod_sdma_copy() calls issued this
+ * batch (1 per raw/tunnel packet, 2 per transport packet). bytes =
+ * total bytes DMA'd. Zero when stats are disabled.
+ */
+ u32 rx_sdma_copies;
+ u32 rx_sdma_bytes;
+ /* Deferred SDMA completion state. finish_rx_deliver stores
+ * n_sdma_pending + pkt_idx_of[] so the dispatcher's
+ * SDMA_PENDING -> EMPTY transition can publish descs and mark
+ * bds without re-scanning the verdict loop.
+ */
+ int n_sdma_pending;
+ u16 sdma_pkt_idx_of[KNOD_IPSEC_PKT_BATCH];
+ int nr_packets;
+ /* RX: napi to kick after GPU writes verdicts (single-queue fallback
+ * for KAT; production multi-queue dispatch schedules per-queue napis
+ * at dispatcher level and leaves rx_napi == NULL).
+ */
+ struct napi_struct *rx_napi;
+ int rx_queue_idx;
+ struct spsc_bd *rx_bds[KNOD_IPSEC_PKT_BATCH];
+ /* Per-packet queue index for multi-queue batched RX dispatches.
+ * finish_rx_deliver uses this to route each decrypted packet to
+ * the correct priv->rxq[] delivery pool + desc_ring.
+ */
+ u8 rx_pkt_queue[KNOD_IPSEC_PKT_BATCH];
+ /* Heap-backed finish-state scratchpad used by finish_rx_deliver.
+ * Sized to KNOD_IPSEC_PKT_BATCH so we never overflow the dispatcher
+ * kernel stack when BATCH grows.
+ */
+ struct knod_ipsec_rx_pending rx_pending[KNOD_IPSEC_PKT_BATCH];
+};
+
+/*
+ * Hard upper bound on parallel dispatchers. Each dispatcher owns one
+ * kaql[i] / sdma[i] pair, one private slice of work_pool and the
+ * backing pool BOs, and one contiguous range of RX queues. The actual
+ * count is knod_ipsec_priv::nr_dispatchers, set at
+ * start time from knod->queue_cnt.
+ *
+ * Bumped beyond 1 for real parallelism: one GPU AQL queue is a
+ * hardware FIFO, so throughput is single-kaql drain rate bound. Two
+ * kaqls let the GPU scheduler run two dispatches on disjoint CUs in
+ * parallel, subject to CU / memory bandwidth contention.
+ */
+#define KNOD_IPSEC_MAX_DISPATCHERS 4
+
+/*
+ * Per-dispatcher runtime state. Each dispatcher kthread owns exactly
+ * one of these and never shares hot-path state with any other
+ * dispatcher - cursors, in-flight work slots, pool BOs, fence counters
+ * and the kaql/sdma index are all private.
+ *
+ * Cross-dispatcher sharing lives in knod_ipsec_priv: the SA table +
+ * slot array + spi_to_slot xarray (read-mostly), the per-CPU stats,
+ * and the delivery pool / desc_ring for RX delivery (but each rxq slot is
+ * only drained by the owning dispatcher, so no locking needed inside
+ * a queue).
+ */
+struct knod_ipsec_dispatcher {
+ struct knod_ipsec_priv *priv;
+ struct task_struct *kthread;
+
+ /* kaql[kaql_idx] + sdma[kaql_idx] owned exclusively by this disp. */
+ int kaql_idx;
+
+ /* Slice into priv->work_pool[]. Dispatcher N uses slots
+ * [work_first, work_first+work_count). work_count is usually
+ * KNOD_IPSEC_NR_WORK but can be smaller if the final dispatcher
+ * got a partial slice.
+ */
+ int work_first;
+ int work_count;
+
+ /* Private backing BOs for this dispatcher's work slice. Each
+ * slot within [work_first, work_first+work_count) gets its own
+ * sub-range of these BOs, so no cross-dispatcher aliasing.
+ */
+ struct knod_mem *param_pool;
+ struct knod_mem *decrypt_pool;
+
+ /* RX queue range this dispatcher drains. [rxq_first, rxq_first+
+ * rxq_count) indexes into priv->rxq[] and knodev->wpriv[].spsc_bds.
+ */
+ int rxq_first;
+ int rxq_count;
+
+ /* Hot-path cursors, previously locals in the dispatcher loop. */
+ int rx_rr;
+ int build_cursor;
+
+ /* Forward-looking AQL completion-signal counter. See old priv->
+ * dispatch_sigval_next comment; now per-dispatcher because each
+ * disp has its own kaql signal.
+ */
+ s64 dispatch_sigval_next;
+};
+
+struct knod_ipsec_priv {
+ struct knod *knod;
+ struct knod_dev *knodev;
+
+ /* Shared GCM tables (VRAM, one per priv, owned by knod ctx) */
+ struct knod_mem *t_tables;
+
+ /* GPU-visible SA table */
+ struct knod_mem *sa_table;
+
+ /* CPU-side slot metadata */
+ struct knod_ipsec_sa_slot slots[KNOD_IPSEC_NR_SA];
+ struct xarray spi_to_slot;
+ struct mutex slot_lock;
+
+ /*
+ * Work pool is sized NR_WORK * MAX_DISPATCHERS so each
+ * dispatcher gets its own NR_WORK-sized slice via work_first.
+ * With nr_dispatchers=1 only the first slice is populated, so
+ * runtime cost is identical to the old single-slot layout when
+ * a single dispatcher is in use.
+ */
+ struct knod_ipsec_work work_pool[KNOD_IPSEC_NR_WORK *
+ KNOD_IPSEC_MAX_DISPATCHERS];
+
+ /* Number of NIC RX queues bound to the NOD (<= KNOD_SPSC_MAX); the
+ * finish worker bounds-checks the shader's queue index against it.
+ */
+ int nr_rxq;
+
+ /* Dispatcher state. nr_dispatchers <= KNOD_IPSEC_MAX_DISPATCHERS,
+ * set at start time from the knod queue_cnt the accel was
+ * created with. Each disp[i] owns kaql[i] / sdma[i].
+ */
+ struct knod_ipsec_dispatcher disp[KNOD_IPSEC_MAX_DISPATCHERS];
+ int nr_dispatchers;
+ bool running;
+ u32 pkt_batch;
+
+ /* Shaders */
+ size_t shader_size;
+ int isa_version;
+
+ /* Observability */
+ struct dentry *debug_dir;
+ struct knod_ipsec_stats __percpu *stats;
+
+ /* Persistent KAT scratch BO. Allocated once at priv init, reused on
+ * every KAT invocation. Re-allocating per-KAT re-triggers the GTT/VRAM
+ * multi-BO mapping symptom (only the first BO is reliably mapped on
+ * the KFD process VM), which is the same class of bug documented for
+ * the BPF delivery path. One long-lived BO sidesteps it entirely.
+ */
+ struct knod_mem *kat_scratch;
+};
+
+/* Public entry points for NIC consumers. */
+struct spsc_bd;
+int knod_ipsec_rx_submit(struct knod_ipsec_fused_sub *sub, int nr,
+ struct napi_struct *napi, int queue_idx);
+int knod_ipsec_rx_submit_bds(struct knod_ipsec_fused_sub *sub,
+ struct spsc_bd **bds, int nr,
+ struct napi_struct *napi, int queue_idx);
+
+#endif /* KNOD_IPSEC_H_ */
diff --git a/tools/testing/selftests/drivers/net/knod/config b/tools/testing/selftests/drivers/net/knod/config
index a60ece9da112..837187205f89 100644
--- a/tools/testing/selftests/drivers/net/knod/config
+++ b/tools/testing/selftests/drivers/net/knod/config
@@ -4,4 +4,5 @@ CONFIG_XDP_SOCKETS=y
CONFIG_KNOD=m
CONFIG_HSA_AMD=y
CONFIG_HSA_AMD_KNOD_BPF=m
+CONFIG_HSA_AMD_KNOD_IPSEC=m
CONFIG_DEBUG_FS=y
--
2.43.0
next prev parent reply other threads:[~2026-07-19 18:02 UTC|newest]
Thread overview: 32+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-07-19 17:58 [RFC PATCH net-next 00/13] net: knod: in-kernel network offload device Taehee Yoo
2026-07-19 17:58 ` [RFC PATCH net-next 01/13] net: knod: add uapi and core headers Taehee Yoo
2026-07-21 7:17 ` sashiko-bot
2026-07-19 17:58 ` [RFC PATCH net-next 02/13] net: devmem: extend memory provider for knod Taehee Yoo
2026-07-20 19:43 ` Mina Almasry
2026-07-21 16:15 ` Taehee Yoo
2026-07-21 7:17 ` sashiko-bot
2026-07-19 17:58 ` [RFC PATCH net-next 03/13] net: core: add XDP_MODE_HW offload hook " Taehee Yoo
2026-07-21 7:17 ` sashiko-bot
2026-07-19 17:58 ` [RFC PATCH net-next 04/13] net: knod: add offload device core and control plane Taehee Yoo
2026-07-21 7:17 ` sashiko-bot
2026-07-19 17:58 ` [RFC PATCH net-next 05/13] bpf: offload: allow PERCPU_ARRAY maps for offloaded programs Taehee Yoo
2026-07-21 7:17 ` sashiko-bot
2026-07-19 17:58 ` [RFC PATCH net-next 06/13] drm/amdkfd: prepare kfd core for the knod provider Taehee Yoo
2026-07-21 7:17 ` sashiko-bot
2026-07-19 17:58 ` [RFC PATCH net-next 07/13] drm/amdkfd: add knod provider core Taehee Yoo
2026-07-21 7:17 ` sashiko-bot
2026-07-19 17:58 ` [RFC PATCH net-next 08/13] drm/amdkfd: add GPU instruction emitter and disassembler Taehee Yoo
2026-07-20 20:05 ` Natalie Vock
2026-07-20 20:53 ` Andrew Lunn
2026-07-21 16:36 ` Hoyeon Lee
2026-07-19 17:58 ` [RFC PATCH net-next 09/13] drm/amdkfd: add BPF-to-GPU JIT offload Taehee Yoo
2026-07-19 17:58 ` [RFC PATCH net-next 10/13] net/mlx5e: add knod XDP offload support Taehee Yoo
2026-07-21 7:17 ` sashiko-bot
2026-07-19 17:58 ` [RFC PATCH net-next 11/13] bnxt_en: " Taehee Yoo
2026-07-21 7:18 ` sashiko-bot
2026-07-19 17:58 ` [RFC PATCH net-next 12/13] selftests: drivers/net: add knod tests Taehee Yoo
2026-07-21 7:18 ` sashiko-bot
2026-07-19 17:58 ` Taehee Yoo [this message]
2026-07-21 7:18 ` [RFC PATCH net-next 13/13] drm/amdkfd: add IPsec full-packet offload sashiko-bot
2026-07-20 19:18 ` [RFC PATCH net-next 00/13] net: knod: in-kernel network offload device Mina Almasry
2026-07-21 15:17 ` Taehee Yoo
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260719175857.4071636-14-ap420073@gmail.com \
--to=ap420073@gmail.com \
--cc=Felix.Kuehling@amd.com \
--cc=airlied@gmail.com \
--cc=alexander.deucher@amd.com \
--cc=amd-gfx@lists.freedesktop.org \
--cc=andrew+netdev@lunn.ch \
--cc=andrii@kernel.org \
--cc=ast@kernel.org \
--cc=bpf@vger.kernel.org \
--cc=christian.koenig@amd.com \
--cc=daniel@iogearbox.net \
--cc=davem@davemloft.net \
--cc=donald.hunter@gmail.com \
--cc=dri-devel@lists.freedesktop.org \
--cc=eddyz87@gmail.com \
--cc=edumazet@google.com \
--cc=emil@etsalapatis.com \
--cc=hawk@kernel.org \
--cc=horms@kernel.org \
--cc=hoyeon.rhee@gmail.com \
--cc=ilias.apalodimas@linaro.org \
--cc=john.fastabend@gmail.com \
--cc=jolsa@kernel.org \
--cc=justinstitt@google.com \
--cc=kees@kernel.org \
--cc=kuba@kernel.org \
--cc=leon@kernel.org \
--cc=linaro-mm-sig@lists.linaro.org \
--cc=linux-hardening@vger.kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-kselftest@vger.kernel.org \
--cc=linux-media@vger.kernel.org \
--cc=linux-rdma@vger.kernel.org \
--cc=llvm@lists.linux.dev \
--cc=martin.lau@linux.dev \
--cc=mbloch@nvidia.com \
--cc=memxor@gmail.com \
--cc=michael.chan@broadcom.com \
--cc=morbo@google.com \
--cc=nathan@kernel.org \
--cc=ndesaulniers@google.com \
--cc=netdev@vger.kernel.org \
--cc=pabeni@redhat.com \
--cc=pavan.chebbi@broadcom.com \
--cc=saeedm@nvidia.com \
--cc=sdf@fomichev.me \
--cc=shuah@kernel.org \
--cc=simona@ffwll.ch \
--cc=song@kernel.org \
--cc=sumit.semwal@linaro.org \
--cc=tariqt@nvidia.com \
--cc=yonghong.song@linux.dev \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
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.