project
Architecture design
A detailed view of the project's architecture, including execution flow, activation arbitration, lifecycle, state ownership and design constraints.
Wrong Boot follows a small core, pluggable trigger architecture.
Core
The core owns execution and lifecycle management. Triggers are independent event sources that detect conditions and request activation through a single core-owned interface.
Responsibilities of the core
The core module (wrong8007.c) is responsible for:
- Validating and storing core module parameters
- Initializing and tearing down registered triggers
- Maintaining the global execution state
- Arbitrating competing trigger activations
- Scheduling deferred execution through
exec_work - Invoking the configured userspace command through the User Mode Helper API
- Rolling back trigger initialization if module loading fails
- Flushing pending execution work during module unload
Responsibilities of triggers
Each trigger is responsible for:
- Registering with the relevant kernel subsystem
- Validating trigger-specific configuration
- Maintaining any state required to detect its condition
- Evaluating incoming events
- Calling
wrong8007_activate()when its condition is satisfied - Unregistering its hooks and freeing its state during teardown
A trigger must not own execution policy.
In particular, triggers must not:
- Call
schedule_work(&exec_work) - Call
call_usermodehelper()directly - Execute the configured payload themselves
- Modify the global execution latch
- Depend on another trigger being enabled
The core is the sole owner of execution arbitration and deferred execution.
Trigger plugins
Each trigger owns its own kernel hook and its own module parameters and is
responsible for validating those parameters at init() time. None of
the three in-tree triggers poll; each attaches to an existing kernel
notifier/hook chain:
- keyboard: A
notifier_blockregistered viaregister_keyboard_notifier(). Each keydown is mapped through a fixed US keymap table and compared against the next expected character ofPHRASE, tracked with a small spinlock-guarded counter. A wrong key resets the match, unless that key happens to be the phrase's first character. - usb: A
notifier_blockregistered viausb_register_notify(). ParsesUSB_DEVICESinto an array of{{vid, pid, event}}rules at load time and matchesUSB_DEVICE_ADD/USB_DEVICE_REMOVEnotifications against them, honoring whitelist or blacklist mode. - network: An
nf_hook_opsregistered atNF_INET_PRE_ROUTING, priorityNF_IP_PRI_FIRST. Matches on source MAC, source IP, a port+payload magic packet, or a per-host heartbeat timeout tracked with atimer_list.
Complete behavior and parameters: Activation triggers. Plugin contract and calling-context rules: Development.
Activation and one-shot execution
Triggers do not schedule the execution work directly.
When a trigger detects its configured condition, it calls:
void wrong8007_activate(void);
core/wrong8007.c
The core owns the activation decision and uses an atomic execution latch:
static atomic_t exec_armed = ATOMIC_INIT(1);
void wrong8007_activate(void)
{
if (atomic_cmpxchg(&exec_armed, 1, 0) == 1) {
schedule_work(&exec_work);
}
}
core/wrong8007.c
This provides the module's one-shot guarantee.
First trigger wins
The module starts armed:
exec_armed = 1
All triggers converge on the same activation path. The first caller atomically consumes the latch and schedules the work.
Every concurrent or subsequent call, from the same trigger or a different one is a no-op because exec_armed has already been consumed.
There is no dedup logic in the triggers themselves; the USB notifier, for instance, schedules work once per USB event without checking whether it has already fired, because the latch makes that check unnecessary.
atomic_cmpxchg is safe to call from the contexts every trigger
actually runs in process context for keyboard and
usb, softirq for network without extra locking
around the decision itself. Whichever trigger calls it first schedules the work
item; every other call, from that point until the module is reloaded, observes
exec_armed == 0 and does nothing.
The latch is therefore owned by the core rather than by individual trigger implementations. Multiple triggers may request activation concurrently, but only one can consume the execution latch.
Deferred execution
Trigger callbacks perform detection only. On a successful match they invoke wrong8007_activate() and return immediately; they never execute user-space code directly.
Execution is deferred to a kernel workqueue, where do_exec_work() runs on a worker thread. This keeps notifier callbacks and interrupt-context code short, avoids sleeping in atomic contexts and provides a single execution path regardless of which trigger fired.
Once activation has been accepted, execution is deferred to the core's work item:
The core invokes the configured payload through:
/bin/sh -c "$EXEC"
with a deliberately minimal environment:
static char *env[] = {
"HOME=/",
"PATH=/sbin:/bin:/usr/sbin:/usr/bin",
NULL
};
No shell startup files or user session state are assumed. If the payload depends on additional environment variables, it should establish them explicitly.
The User Mode Helper is invoked with UMH_WAIT_PROC, so the work item does not complete until the spawned command exits. As a result, flush_work() during module unload also waits for the configured command to finish before returning.
Only one activation is permitted per module load. Once the internal activation latch is set, subsequent trigger events are ignored until the module is unloaded and loaded again.
Fail-closed parameter validation
Every trigger validates its own parameters in init() and returns a
negative errno on anything malformed. A bad usb_devices rule, an
unparseable MAC or IP, or a missing exec parameter aborts module load
entirely:
if (!exec || !*exec) {
wb_err("exec parameter required\n");
return -EINVAL;
}
At module_init, the core validates its own exec
parameter, initializes the workqueue item, then walks the trigger array calling
each init() in order. Any failure unwinds triggers already
initialized, in reverse order, before returning the error; there is no
partially-loaded state.
No persistent state
The module writes nothing to disk. Trigger state (match progress, parsed rules,
heartbeat timestamps) lives in kernel memory for the life of the module and is
freed on exit(). Nothing is logged after a trigger fires, beyond the
one wb_info() line noting which trigger matched.
Deliberate omissions
- No default payload. The module has no built-in wipe, lock, or shutdown
logic;
EXECis required at load time and is entirely operator-supplied. - No inter-trigger coordination. Triggers cannot see or influence each other; the only shared state is the latch and triggers don't read it, only the core does.
- No retry, rollback, or escalation logic in the core. Execution is one-shot because the latch is one-shot; anything more elaborate belongs in the payload script, not in the module.