Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions arch/lkl/Kconfig
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions arch/lkl/include/asm/thread_info.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,26 @@ 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) \
{ \
.task = &tsk, \
.preempt_count = INIT_PREEMPT_COUNT, \
.flags = 0, \
.irqs_enabled = 1, /* ARCH_IRQ_ENABLED */ \
}

/* how to get the thread information struct from C */
Expand Down
2 changes: 2 additions & 0 deletions arch/lkl/kernel/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
45 changes: 36 additions & 9 deletions arch/lkl/kernel/irq.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -89,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 (!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);
Expand Down Expand Up @@ -168,15 +193,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)
Expand Down
96 changes: 96 additions & 0 deletions arch/lkl/kernel/irq_test.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// SPDX-License-Identifier: GPL-2.0

#include <kunit/test.h>

#include <linux/atomic.h>
#include <linux/completion.h>
#include <linux/interrupt.h>
#include <linux/jiffies.h>
#include <linux/module.h>

#include <asm/host_ops.h>
#include <asm/irq.h>

/*
* 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");
7 changes: 7 additions & 0 deletions arch/lkl/kernel/threads.c
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include <linux/sched/signal.h>
#include <asm/host_ops.h>
#include <asm/cpu.h>
#include <asm/irqflags.h>
#include <asm/sched.h>
#include <asm/switch_to.h>

Expand All @@ -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;
}
Expand Down
2 changes: 2 additions & 0 deletions tools/lkl/Makefile.autoconf
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 27 additions & 0 deletions tools/lkl/tests/boot.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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),
};
Expand Down