[PATCH 2/2] thunderbolt: Test ring interrupt warning after host reset

From: Andrei Rusu de Castro

Date: Wed Sep 02 2026 - 08:41:31 EST


Cover the warning decision independently of MMIO by constructing a ring
and NHI generation pair. Verify that duplicate enables always warn,
duplicate disables without an intervening reset warn, a disable after a
reset does not warn, and an update that changed the register never
warns.

Expose the predicate only in KUnit builds through VISIBLE_IF_KUNIT; it
remains private in production builds and is not exported outside the
Thunderbolt module.

The cases were verified under UML KUnit.

Signed-off-by: Andrei Rusu de Castro <arc@empyreal.works>
---
drivers/thunderbolt/nhi.c | 4 +-
drivers/thunderbolt/nhi.h | 4 ++
drivers/thunderbolt/test.c | 82 ++++++++++++++++++++++++++++++++++++++
3 files changed, 89 insertions(+), 1 deletion(-)

diff --git a/drivers/thunderbolt/nhi.c b/drivers/thunderbolt/nhi.c
index f56590100aef..d768a84adaab 100644
--- a/drivers/thunderbolt/nhi.c
+++ b/drivers/thunderbolt/nhi.c
@@ -21,6 +21,8 @@
#include <linux/string_choices.h>
#include <linux/string_helpers.h>

+#include <kunit/visibility.h>
+
#include "nhi.h"
#include "nhi_regs.h"
#include "tb.h"
@@ -86,7 +88,7 @@ static void nhi_clear_interrupt(struct tb_nhi *nhi, int ring)
*
* Return: %true if the caller should warn about the no-op update.
*/
-static bool
+VISIBLE_IF_KUNIT bool
nhi_ring_interrupt_should_warn(const struct tb_ring *ring, bool active,
bool unchanged)
{
diff --git a/drivers/thunderbolt/nhi.h b/drivers/thunderbolt/nhi.h
index f72d6b274501..393bd831375f 100644
--- a/drivers/thunderbolt/nhi.h
+++ b/drivers/thunderbolt/nhi.h
@@ -37,6 +37,10 @@ irqreturn_t ring_msix(int irq, void *data);
int nhi_probe(struct tb_nhi *nhi);
void nhi_shutdown(struct tb_nhi *nhi);
void nhi_reset_interface(struct tb_nhi *nhi);
+#if IS_ENABLED(CONFIG_KUNIT)
+bool nhi_ring_interrupt_should_warn(const struct tb_ring *ring, bool active,
+ bool unchanged);
+#endif

extern const struct dev_pm_ops nhi_pm_ops;

diff --git a/drivers/thunderbolt/test.c b/drivers/thunderbolt/test.c
index 05652ee82fbf..3ccdd967396b 100644
--- a/drivers/thunderbolt/test.c
+++ b/drivers/thunderbolt/test.c
@@ -9,6 +9,7 @@
#include <kunit/test.h>
#include <linux/idr.h>

+#include "nhi.h"
#include "tb.h"
#include "tunnel.h"

@@ -3095,6 +3096,83 @@ static void tb_test_property_merge(struct kunit *test)
tb_property_free_dir(dir1);
}

+static struct tb_ring *alloc_interrupt_test_ring(struct kunit *test,
+ int nhi_generation,
+ int ring_generation)
+{
+ struct tb_nhi *nhi;
+ struct tb_ring *ring;
+
+ nhi = kunit_kzalloc(test, sizeof(*nhi), GFP_KERNEL);
+ if (!nhi)
+ return NULL;
+
+ ring = kunit_kzalloc(test, sizeof(*ring), GFP_KERNEL);
+ if (!ring)
+ return NULL;
+
+ ring->nhi = nhi;
+ atomic_set(&nhi->reset_generation, nhi_generation);
+ ring->reset_generation = ring_generation;
+
+ return ring;
+}
+
+static void tb_test_ring_interrupt_warn_duplicate_enable(struct kunit *test)
+{
+ struct tb_ring *ring;
+
+ /* Enabling an already enabled interrupt is always a driver bug */
+ ring = alloc_interrupt_test_ring(test, 7, 7);
+ KUNIT_ASSERT_NOT_NULL(test, ring);
+ KUNIT_EXPECT_TRUE(test, nhi_ring_interrupt_should_warn(ring, true, true));
+
+ /* Including when the host interface was reset in between */
+ ring = alloc_interrupt_test_ring(test, 8, 7);
+ KUNIT_ASSERT_NOT_NULL(test, ring);
+ KUNIT_EXPECT_TRUE(test, nhi_ring_interrupt_should_warn(ring, true, true));
+}
+
+static void tb_test_ring_interrupt_warn_duplicate_disable(struct kunit *test)
+{
+ struct tb_ring *ring;
+
+ /*
+ * No reset happened while this ring was running, so a redundant
+ * disable means the driver lost track of the hardware state.
+ */
+ ring = alloc_interrupt_test_ring(test, 7, 7);
+ KUNIT_ASSERT_NOT_NULL(test, ring);
+ KUNIT_EXPECT_TRUE(test, nhi_ring_interrupt_should_warn(ring, false, true));
+}
+
+static void tb_test_ring_interrupt_no_warn_after_reset(struct kunit *test)
+{
+ struct tb_ring *ring;
+
+ /*
+ * The ring was started before the host interface was reset, which
+ * cleared the ring interrupt bit underneath it.
+ */
+ ring = alloc_interrupt_test_ring(test, 8, 7);
+ KUNIT_ASSERT_NOT_NULL(test, ring);
+ KUNIT_EXPECT_FALSE(test,
+ nhi_ring_interrupt_should_warn(ring, false, true));
+}
+
+static void tb_test_ring_interrupt_no_warn_when_changed(struct kunit *test)
+{
+ struct tb_ring *ring;
+
+ /* An update that actually changed the register never warns */
+ ring = alloc_interrupt_test_ring(test, 8, 7);
+ KUNIT_ASSERT_NOT_NULL(test, ring);
+ KUNIT_EXPECT_FALSE(test,
+ nhi_ring_interrupt_should_warn(ring, false, false));
+ KUNIT_EXPECT_FALSE(test,
+ nhi_ring_interrupt_should_warn(ring, true, false));
+}
+
static struct kunit_case tb_test_cases[] = {
KUNIT_CASE(tb_test_property_parse_u32_wrap),
KUNIT_CASE(tb_test_property_parse_recursion),
@@ -3141,6 +3219,10 @@ static struct kunit_case tb_test_cases[] = {
KUNIT_CASE(tb_test_property_parse_zero_length),
KUNIT_CASE(tb_test_property_parse_rootdir_overflow),
KUNIT_CASE(tb_test_property_merge),
+ KUNIT_CASE(tb_test_ring_interrupt_warn_duplicate_enable),
+ KUNIT_CASE(tb_test_ring_interrupt_warn_duplicate_disable),
+ KUNIT_CASE(tb_test_ring_interrupt_no_warn_after_reset),
+ KUNIT_CASE(tb_test_ring_interrupt_no_warn_when_changed),
{ }
};