[wrong8007] Wrong Boot

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.

Simplified architecture

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_block registered via register_keyboard_notifier(). Each keydown is mapped through a fixed US keymap table and compared against the next expected character of PHRASE, 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_block registered via usb_register_notify(). Parses USB_DEVICES into an array of {{vid, pid, event}} rules at load time and matches USB_DEVICE_ADD/USB_DEVICE_REMOVE notifications against them, honoring whitelist or blacklist mode.
  • network: An nf_hook_ops registered at NF_INET_PRE_ROUTING, priority NF_IP_PRI_FIRST. Matches on source MAC, source IP, a port+payload magic packet, or a per-host heartbeat timeout tracked with a timer_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.

Activation and one-shot execution

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:

User-mode execution chain

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; EXEC is 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.