From 8c08fa7d41eba45e535222716eef40ab872c84f1 Mon Sep 17 00:00:00 2001 From: Joseph Date: Thu, 28 May 2026 09:51:15 +0300 Subject: [PATCH 1/3] lkl: make irqs_enabled per-thread via current_thread_info() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit irqs_enabled in arch/lkl/kernel/irq.c is a single `static bool` global, with no save/restore in __switch_to. That is fine for the existing test suite, which doesn't exercise the failure paths it exposes, but it has two correctness consequences for code outside the in-tree tests: - Any kernel path that does spin_lock_irqsave and schedules before the matching restore (an unsupported pattern in Linux; lockdep would catch it on a normal kernel — LKL doesn't) leaks the DISABLED value to whichever thread runs next, and the next thread's restore overwrites it. The save/restore semantics of arch_local_irq_save / arch_local_irq_restore are not per-thread in LKL today. - More importantly, a host pthread (one created via lkl_ops->thread_create, e.g. a libusb event thread or a GLib/Qt timer callback that backs a virtio/USB-style host shim) invoking lkl_trigger_irq from outside any kernel context reads whatever value the last kernel task to run left in the global. The next commit relies on per-thread irqs_enabled to distinguish such host callers cleanly; this commit prepares the field for that distinction. This patch is a pure refactor: move irqs_enabled into struct thread_info; access via current_thread_info() from arch_local_save_flags, arch_local_irq_restore, and the lkl_trigger_irq pending-check. No explicit save/restore is added to __switch_to — the existing _current_thread_info = task_thread_info(next); line is the entire mechanism. Each thread's irqs_enabled travels with its thread_info, the same way a real CPU's register file follows the thread. Behaviour for the existing test suite is identical (the suite doesn't exercise the cross-thread leak). The follow-up commit ("lkl: deliver IRQs from host-thread callers regardless of irqs_enabled") uses the per-thread field to fix the host-caller case observed in real-world backends. - arch/lkl/include/asm/thread_info.h: add `unsigned long irqs_enabled` field; INIT_THREAD_INFO sets it to 1 (ARCH_IRQ_ENABLED) so the init task starts enabled. - arch/lkl/kernel/irq.c: drop the `static bool irqs_enabled` global. arch_local_save_flags, arch_local_irq_restore, and the lkl_trigger_irq check all go through current_thread_info(). - arch/lkl/kernel/threads.c: init_ti sets ti->irqs_enabled = ARCH_IRQ_ENABLED for freshly-allocated kernel threads. Signed-off-by: Joseph --- arch/lkl/include/asm/thread_info.h | 13 +++++++++++++ arch/lkl/kernel/irq.c | 18 +++++++++++++----- arch/lkl/kernel/threads.c | 7 +++++++ 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/arch/lkl/include/asm/thread_info.h b/arch/lkl/include/asm/thread_info.h index ae6df8f77f5e2e..5749271f1441f6 100644 --- a/arch/lkl/include/asm/thread_info.h +++ b/arch/lkl/include/asm/thread_info.h @@ -18,6 +18,18 @@ struct thread_info { lkl_thread_t tid; struct task_struct *prev_sched; unsigned long stackend; + /* + * IRQ-enable state, accessed via current_thread_info() from + * arch_local_save_flags / arch_local_irq_restore in + * arch/lkl/kernel/irq.c. Living here (instead of as a single + * global) means __switch_to moves the state with the thread + * for free: the line + * + * _current_thread_info = task_thread_info(next); + * + * in arch/lkl/kernel/threads.c is the whole save/restore. + */ + unsigned long irqs_enabled; }; #define INIT_THREAD_INFO(tsk) \ @@ -25,6 +37,7 @@ struct thread_info { .task = &tsk, \ .preempt_count = INIT_PREEMPT_COUNT, \ .flags = 0, \ + .irqs_enabled = 1, /* ARCH_IRQ_ENABLED */ \ } /* how to get the thread information struct from C */ diff --git a/arch/lkl/kernel/irq.c b/arch/lkl/kernel/irq.c index 361f3373e84b95..05f6f0df017866 100644 --- a/arch/lkl/kernel/irq.c +++ b/arch/lkl/kernel/irq.c @@ -52,7 +52,13 @@ static struct irq_info { const char *user; } irqs[NR_IRQS]; -static bool irqs_enabled; +/* + * irqs_enabled lives in struct thread_info; see + * arch/lkl/include/asm/thread_info.h. Accessed via current_thread_info() + * in arch_local_save_flags / arch_local_irq_restore below. Switching + * _current_thread_info in __switch_to (arch/lkl/kernel/threads.c) is + * the entire save/restore: no explicit per-thread save/load is needed. + */ static struct pt_regs dummy; @@ -91,7 +97,7 @@ int lkl_trigger_irq(int irq) * IRQ -> softirq -> lkl_trigger_irq) make sure we are actually allowed * to run irqs at this point */ - if (!irqs_enabled) { + if (!current_thread_info()->irqs_enabled) { set_irq_pending(irq); lkl_cpu_put(); return 0; @@ -168,15 +174,17 @@ void lkl_put_irq(int i, const char *user) unsigned long arch_local_save_flags(void) { - return irqs_enabled; + return current_thread_info()->irqs_enabled; } void arch_local_irq_restore(unsigned long flags) { - if (flags == ARCH_IRQ_ENABLED && irqs_enabled == ARCH_IRQ_DISABLED && + struct thread_info *ti = current_thread_info(); + + if (flags == ARCH_IRQ_ENABLED && ti->irqs_enabled == ARCH_IRQ_DISABLED && !in_interrupt()) run_irqs(); - irqs_enabled = flags; + ti->irqs_enabled = flags; } void init_IRQ(void) diff --git a/arch/lkl/kernel/threads.c b/arch/lkl/kernel/threads.c index 84564d52884843..a5cb7053f8d004 100644 --- a/arch/lkl/kernel/threads.c +++ b/arch/lkl/kernel/threads.c @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -16,6 +17,12 @@ static int init_ti(struct thread_info *ti) ti->dead = false; ti->prev_sched = NULL; ti->tid = 0; + /* + * New threads start with IRQs enabled. State moves with the + * thread via _current_thread_info in __switch_to; see + * arch/lkl/kernel/irq.c for the rationale. + */ + ti->irqs_enabled = ARCH_IRQ_ENABLED; return 0; } From fabce9c9db54cb6aecd76f130bb105cfb4180474 Mon Sep 17 00:00:00 2001 From: Joseph Date: Thu, 28 May 2026 09:51:45 +0300 Subject: [PATCH 2/3] lkl: deliver IRQs from host-thread callers regardless of irqs_enabled MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit lkl_trigger_irq is documented as callable "from arbitrary host threads" (see the comment block above the function). True host pthreads — libusb completion threads, glibc SIGEV_THREAD timer callbacks, anything created by a backend library that ends up posting an IRQ into the LKL kernel — acquire the LKL CPU via lkl_cpu_get but never go through __switch_to. _current_thread_info therefore still points at whichever kernel task last ran (often the idle task), and that task's irqs_enabled field may be ARCH_IRQ_DISABLED at the moment the host caller reads it. Honoring that stale flag for host-thread callers is a silent IRQ-pending hang: the IRQ gets marked pending, but nothing on the kernel side notices until the matching irqrestore in the original kernel context — which often never comes, because the kernel has already moved on. Drivers that post IRQs from host-thread backends (e.g. an out-of-tree USB host-controller shim that forwards URBs to libusb and signals completion from libusb's event thread; any virtio/host-shim backend with a thread-based notification scheme has the same exposure) hang the kernel within tens of operations. Detect host-thread callers by comparing thread_self() to the thread_info owner's tid via lkl_ops->thread_equal. When they differ, the caller is not the kernel thread that owns this thread_info; the stale irqs_enabled field has no claim on us, and we deliver the IRQ. Kernel-thread callers — including the recursive "lkl_trigger_irq -> IRQ -> softirq -> lkl_trigger_irq" path called out in the original comment — continue to honor their own per-thread irqs_enabled (set by the previous commit). Signed-off-by: Joseph --- arch/lkl/kernel/irq.c | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/arch/lkl/kernel/irq.c b/arch/lkl/kernel/irq.c index 05f6f0df017866..cb2f21a3e94c95 100644 --- a/arch/lkl/kernel/irq.c +++ b/arch/lkl/kernel/irq.c @@ -95,12 +95,31 @@ int lkl_trigger_irq(int irq) /* * Since this can be called from Linux context (e.g. lkl_trigger_irq -> * IRQ -> softirq -> lkl_trigger_irq) make sure we are actually allowed - * to run irqs at this point + * to run irqs at this point. + * + * The per-thread irqs_enabled check only applies when the caller + * actually OWNS current_thread_info — i.e. it is the kernel + * thread (or host_task) whose context lkl_cpu_get most recently + * switched to. Calls from a true host pthread (a libusb + * completion thread, a glibc SIGEV_THREAD timer callback, etc.) + * acquire the LKL CPU but never set _current_thread_info; it + * still points at whichever kernel task last ran, often the idle + * task. Honoring that stale flag for host callers silently pends + * the IRQ (real drivers driven through host-pthread backends + * trip this reliably). Detect host callers by comparing + * thread_self() to the thread_info owner's tid and deliver + * unconditionally in that case. */ - if (!current_thread_info()->irqs_enabled) { - set_irq_pending(irq); - lkl_cpu_put(); - return 0; + { + struct thread_info *ti = current_thread_info(); + bool caller_is_kernel = lkl_ops->thread_equal(ti->tid, + lkl_ops->thread_self()); + + if (caller_is_kernel && !ti->irqs_enabled) { + set_irq_pending(irq); + lkl_cpu_put(); + return 0; + } } run_irq(irq); From 79779c99a15cffc0e624221ea26341380d91a621 Mon Sep 17 00:00:00 2001 From: Joseph Date: Thu, 28 May 2026 09:53:36 +0300 Subject: [PATCH 3/3] lkl: add KUnit test for host-pthread caller of lkl_trigger_irq lkl_trigger_irq is documented as callable "from arbitrary host threads," and host-thread-driven backends (the in-tree virtio-net-tap and similar) rely on that contract. The previous commit ("lkl: deliver IRQs from host-thread callers regardless of irqs_enabled") makes the contract hold even when the kernel task currently in current_thread_info() has irqs_enabled disabled. Add a small KUnit suite (CONFIG_LKL_IRQ_KUNIT_TEST) that locks that contract in: - Allocate an IRQ via lkl_get_free_irq + request_irq with a handler that increments a counter and completes a completion. - Spawn a true host pthread via lkl_ops->thread_create. - The host pthread calls lkl_trigger_irq on the registered IRQ from outside any kernel context. - wait_for_completion_timeout from the test kthread releases the LKL CPU so the host pthread can acquire it via lkl_cpu_try_run_irq inside lkl_trigger_irq, and gives the handler 500 ms to fire. - Assert the handler ran exactly once. Wiring: - arch/lkl/kernel/irq_test.c: the new KUnit suite (.name = "lkl_irq"). - arch/lkl/kernel/Makefile: build it when CONFIG_LKL_IRQ_KUNIT_TEST=y. - arch/lkl/Kconfig: new boolean depends on KUNIT. - tools/lkl/Makefile.autoconf: kunit_test_enable also sets LKL_IRQ_KUNIT_TEST, so the existing kunit=yes CI lane picks it up alongside LKL_PCI_KUNIT_TEST. - tools/lkl/tests/boot.c: lkl_test_kunit_irq parses the boot log for "ok N lkl_irq", mirroring lkl_test_kunit_pci. Signed-off-by: Joseph --- arch/lkl/Kconfig | 13 +++++ arch/lkl/kernel/Makefile | 2 + arch/lkl/kernel/irq_test.c | 96 +++++++++++++++++++++++++++++++++++++ tools/lkl/Makefile.autoconf | 2 + tools/lkl/tests/boot.c | 27 +++++++++++ 5 files changed, 140 insertions(+) create mode 100644 arch/lkl/kernel/irq_test.c diff --git a/arch/lkl/Kconfig b/arch/lkl/Kconfig index 5a04bfdb99424b..c07f276fb91cf3 100644 --- a/arch/lkl/Kconfig +++ b/arch/lkl/Kconfig @@ -139,6 +139,19 @@ config LKL_PCI_KUNIT_TEST memory returned to callers is unmapped and released using the correct CPU address. +config LKL_IRQ_KUNIT_TEST + bool "KUnit tests for LKL IRQ host-thread caller path" + depends on KUNIT + default n + help + Enable KUnit tests for arch/lkl/kernel/irq.c. + + Exercises the contract documented on lkl_trigger_irq that it + is callable from arbitrary host threads. A KUnit test spawns + a real host pthread via lkl_ops->thread_create and asserts + that the registered handler runs synchronously when the host + thread calls lkl_trigger_irq from outside any kernel context. + config RAID6_PQ_BENCHMARK bool default n diff --git a/arch/lkl/kernel/Makefile b/arch/lkl/kernel/Makefile index 80a557a92df78f..7aeb3859ff687d 100644 --- a/arch/lkl/kernel/Makefile +++ b/arch/lkl/kernel/Makefile @@ -6,3 +6,5 @@ KASAN_SANITIZE_stacktrace.o := n obj-y = setup.o threads.o irq.o time.o syscalls.o misc.o console.o \ syscalls_32.o cpu.o init.o stacktrace.o + +obj-$(CONFIG_LKL_IRQ_KUNIT_TEST) += irq_test.o diff --git a/arch/lkl/kernel/irq_test.c b/arch/lkl/kernel/irq_test.c new file mode 100644 index 00000000000000..f1183562bb5002 --- /dev/null +++ b/arch/lkl/kernel/irq_test.c @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: GPL-2.0 + +#include + +#include +#include +#include +#include +#include + +#include +#include + +/* + * KUnit coverage for the host-pthread caller path of lkl_trigger_irq. + * + * lkl_trigger_irq is documented as callable "from arbitrary host + * threads" — backends like a libusb event thread post URB completions + * by injecting an IRQ into the LKL kernel from outside any kernel + * context. A host pthread doesn't own current_thread_info() (it never + * went through __switch_to), so any per-thread state read from there + * belongs to whichever kernel task most recently switched in. + * + * This test spawns a real host pthread via lkl_ops->thread_create, + * has it call lkl_trigger_irq on a kernel-registered IRQ from outside + * any kernel context, and asserts the handler runs synchronously — + * the contract that host-thread-driven backends rely on. + */ + +static struct completion handler_fired; +static atomic_t handler_runs; +static int test_irq; + +static irqreturn_t test_irq_handler(int irq, void *data) +{ + atomic_inc(&handler_runs); + complete(&handler_fired); + return IRQ_HANDLED; +} + +static void host_thread_trigger(void *arg) +{ + lkl_trigger_irq(*(int *)arg); +} + +static void host_thread_irq_delivery_test(struct kunit *test) +{ + lkl_thread_t tid; + long ret; + + atomic_set(&handler_runs, 0); + init_completion(&handler_fired); + + test_irq = lkl_get_free_irq("lkl_irq_kunit"); + KUNIT_ASSERT_GE(test, test_irq, 0); + + ret = request_irq(test_irq, test_irq_handler, 0, + "lkl_irq_kunit", &test_irq); + KUNIT_ASSERT_EQ(test, ret, 0); + + /* + * Spawn a true host pthread — outside any kernel context. It + * calls lkl_trigger_irq on the IRQ we just registered, exactly + * the path a libusb event thread (or any host-side backend + * notification thread) would take to inject a URB completion. + * + * wait_for_completion_timeout releases the LKL CPU so the host + * pthread can acquire it via lkl_cpu_try_run_irq inside + * lkl_trigger_irq. + */ + tid = lkl_ops->thread_create(host_thread_trigger, &test_irq); + KUNIT_ASSERT_NE(test, (unsigned long)tid, 0UL); + + ret = wait_for_completion_timeout(&handler_fired, + msecs_to_jiffies(500)); + KUNIT_EXPECT_GT(test, ret, 0); + KUNIT_EXPECT_EQ(test, atomic_read(&handler_runs), 1); + + lkl_ops->thread_join(tid); + free_irq(test_irq, &test_irq); + lkl_put_irq(test_irq, "lkl_irq_kunit"); +} + +static struct kunit_case lkl_irq_kunit_test_cases[] = { + KUNIT_CASE(host_thread_irq_delivery_test), + {} +}; + +static struct kunit_suite lkl_irq_kunit_test_suite = { + .name = "lkl_irq", + .test_cases = lkl_irq_kunit_test_cases, +}; + +kunit_test_suite(lkl_irq_kunit_test_suite); + +MODULE_LICENSE("GPL"); diff --git a/tools/lkl/Makefile.autoconf b/tools/lkl/Makefile.autoconf index 5c4e0194b31a9b..857dc57f7f41a9 100644 --- a/tools/lkl/Makefile.autoconf +++ b/tools/lkl/Makefile.autoconf @@ -252,6 +252,8 @@ define kunit_test_enable $(call set_autoconf_var,LKL_PCI_KUNIT_TEST,y) $(call set_kernel_config,KUNIT,y) $(call set_kernel_config,LKL_PCI_KUNIT_TEST,y) + $(call set_autoconf_var,LKL_IRQ_KUNIT_TEST,y) + $(call set_kernel_config,LKL_IRQ_KUNIT_TEST,y) $(if $(filter $(LD_FMT),$(KASAN_HOSTS)),$(call set_kernel_config,KASAN,y)) $(if $(filter $(LD_FMT),$(KASAN_HOSTS)),$(call kasan_test_enable)) endef diff --git a/tools/lkl/tests/boot.c b/tools/lkl/tests/boot.c index 3d727f30e52f06..564b3850256631 100644 --- a/tools/lkl/tests/boot.c +++ b/tools/lkl/tests/boot.c @@ -706,6 +706,30 @@ static int lkl_test_kunit_pci(void) } #endif // LKL_HOST_CONFIG_LKL_PCI_KUNIT_TEST +#ifdef LKL_HOST_CONFIG_LKL_IRQ_KUNIT_TEST +static int lkl_test_kunit_irq(void) +{ + char *log = strdup(boot_log); + char *line = NULL; + int n; + + line = strtok(log, "\n"); + while (line) { + if (sscanf(line, "[ %*f] ok %d lkl_irq", &n) == 1) { + lkl_test_logf("%s", line); + free(log); + return TEST_SUCCESS; + } + + line = strtok(NULL, "\n"); + } + + free(log); + + return TEST_FAILURE; +} +#endif // LKL_HOST_CONFIG_LKL_IRQ_KUNIT_TEST + #define CMD_LINE "mem=32M loglevel=8 " static int lkl_test_start_kernel(void) @@ -778,6 +802,9 @@ struct lkl_test tests[] = { #endif #ifdef LKL_HOST_CONFIG_LKL_PCI_KUNIT_TEST LKL_TEST(kunit_pci), +#endif +#ifdef LKL_HOST_CONFIG_LKL_IRQ_KUNIT_TEST + LKL_TEST(kunit_irq), #endif LKL_TEST(stop_kernel), };