Wrong Boot (codename: wrong8007)

September 16, 2023

7 min read

TL;DR

Wrong Boot (codename: wrong8007) is about when and why a system reacts.

It's a programmable, kernel-space dead man's switch for Linux: a last-resort execution framework that activates instantly when predefined low-level conditions are met.

Instead of focusing on data destruction, Wrong Boot is now explicitly trigger-centric and execution-agnostic. Keyboard phrases, USB events, and network signals act purely as detectors. They don't coordinate, don't execute, and don't make decisions. The kernel core does: once, deterministically, and without user-space dependencies.

This blog documents the evolution of the project from an early "self-destruct" mindset into a modular, auditable kernel framework built around strict ownership, fail-closed behavior, and predictable execution semantics. It covers my dev journey, from concept to a working Linux kernel module with architectural pivots, design constraints, kernel-space pitfalls, bugs (so many of them) and security boundaries that shaped Wrong Boot into what it is today: a defensive tool for high-risk environments where reaction time matters more than recovery.

What happens after the trigger fires is entirely up to the operator. Wrong Boot only guarantees one thing: when the line is crossed, it will act exactly as configured, without hesitation.

Concept: Why build a dead man's kernel?

Wrong8007 wasn't born from curiosity alone. It came from a fixation on control, erasure, and what it means to truly own your system ...especially when everything else feels unstable. More on that later.

As a privacy enthusiast, I kept circling one question:

Thought

"What if system response didn't depend on screens, processes, or even user space ...and could be triggered from anywhere, simply by causing the right condition to exist?"

That question is the core of Wrong Boot.

For people operating in high-risk environments: whistleblowers, journalists, or activists, hesitation is a liability. User-space tools assume the system is still cooperative. GUIs can hang, panic buttons can be visible, daemons can be killed, and processes can be inspected or blocked.

Wrong Boot was built to live below all of that. Instead of relying on applications, windows, or operator interaction at the moment of crisis, it embeds itself in the kernel and waits. Triggers don't ask permission and don't negotiate. A phrase typed on a keyboard, a specific USB event, or a carefully crafted network packet is enough to cross the line.

At that moment, the kernel executes a single, pre-authorized action ...exactly once. Crucially, Wrong Boot no longer presumes what that action is. It might be data sanitization, system lockdown, alerting, shutdown, or something entirely custom. The project is intentionally agnostic about payloads. Its responsibility ends at fast, silent, deterministic execution when control of the system is threatened.

A dead man's kernel isn't about destruction by default. It's about guaranteed reaction when everything else can no longer be trusted.

Related:

The build begins

This section walks through how Wrong Boot was built: not as a single leap, but as a series of hard-earned corrections driven by kernel realities. The project didn't start as a polished trigger framework. It started as a very specific goal, hit several classic kernel pitfalls, and then slowly hardened into something reliable, auditable, and extensible.

The 1.0 Goal

The initial v1.0 goals were intentionally narrow:

  • Run silently as a Loadable Kernel Module (LKM)
  • Listen to global keyboard input
  • Match a secret phrase exactly
  • Execute a predefined script (e.g. /usr/local/bin/wipe.sh)
  • Be loadable and removable via insmod / rmmod

At this stage, the project was still payload-focused. The keyboard trigger was the system, and execution happened as soon as the phrase matched.

First approach: Keyboard Notifiers

Linux exposes keyboard notifier chains which allows kernel modules to observe key events globally. This appeared to be an ideal mechanism for phrase detection.

The early implementation:

  • Hooked into register_keyboard_notifier()
  • Intercepted keyboard_notifier_param events
  • Translated keycodes to characters using a US keymap
  • Compared keystrokes against the configured phrase, character by character
Warning

This approach was conceptually valid but not what it appeared to be. Two critical bugs surfaced almost immediately.

Two critical bugs surfaced almost immediately.

Testing protocol

Dry-run: point exec at a harmless echo-script. Ensure test_exec.sh is executable (chmod +x). Load/Unload cleanly, confirm dmesg logs. Avoid trivial phrases; include symbols and mixed case.

Post-mortem in brief

Bug #1: Page faults from module parameters

The trigger phrase and executable path were initially supplied directly via module_param() and dereferenced later during keypress handling.

This resulted in page faults.

The underlying issue was not string handling, but lifetime and context.

Why?

  • Module parameters storage is kernel-managed and lives in memory that is not guaranteed to remain valid long-term
  • Keyboard notifiers may execute on any CPU, long after module initialization and can fire asynchronously, under interrupts
  • Dereferencing those pointers later can hit freed or unmapped memory

In practice, this failed because module_param() storage is not designed to be accessed asynchronously from arbitrary kernel contexts. While the parameters are valid during module initialization, they are not protected against concurrent access, lifetime assumptions, or deferred dereferencing from interrupt-adjacent paths such as notifiers.

By the time the keyboard notifier attempted to read phrase, the pointer could already reference memory that was unmapped, repurposed, or simply unsafe to touch in atomic context resulting in a page fault.

Fix: Fixed by copying user-supplied parameters into guaranteed kernel memory. Clone at init time:

  • phrase_buf = kstrdup(phrase, GFP_KERNEL);
  • exec_buf = kstrdup(exec, GFP_KERNEL);

From that point on, the module only touched memory it owned.

Info
This was the first hard lesson: never trust the lifetime of module params. Even seemingly read-only exposure can become a liability, which is why module_param(..., 0000) is used (rather than relying on flags like S_IRUGO) to fully restrict userland access to phrase and exec, avoiding leakage through /sys/module/.

Bug #2: Scheduling while atomic

The second crash was more subtle ...and more dangerous.

The keyboard notifier runs with preemption disabled. That means:

  • You cannot sleep
  • You cannot block
  • You definitely cannot call functions that might sleep

Unfortunately, call_usermodehelper_exec() can sleep.

Notifier callbacks execute in a constrained context. They may run with preemption disabled and must obey atomic-context rules: no sleeping, no blocking, and no calls into code paths that might wait on user-space or scheduler events.

Warning

Bug: scheduling while atomic

The kernel detects this class of bug aggressively. Any attempt to sleep while preemption is disabled triggers a hard failure, because allowing it would risk deadlocks, scheduler corruption, or stalled CPUs.

Fix: Execution had to be deferred.

At this point, it became clear that the problem was not the keyboard trigger itself, but the assumption that detection and execution could safely live in the same code path. In the kernel, those responsibilities must be separated explicitly.

Info
Kernel Invariant: Notifier callbacks must never sleep. Any execution path reachable from a notifier must be safe in atomic context, or deferred explicitly to process context.

The workqueue pivot

The solution was to split detection from execution.

  • Keyboard notifier → detect only
  • Execution → deferred to process context

This introduced a workqueue:

   static DECLARE_WORK(exec_work, do_exec_work);

The notifier now does exactly one thing on match:

   schedule_work(&exec_work);

And do_exec_work() runs safely in process context, where sleeping is legal.

Usermode helper lifetime pitfalls

Once execution was deferred into a workqueue, the next failure surfaced in the usermode helper path. This one did not crash immediately: it failed nondeterministically.

Early versions constructed argv dynamically using heap allocation and invoked call_usermodehelper_exec() with UMH_NO_WAIT.

The crash was introduced by a refactor: removing the per-call heap copy of argv while continuing to use UMH_NO_WAIT.

Earlier versions unintentionally masked the lifetime bug by leaking the command string, keeping it alive long enough for the asynchronous helper to run. In other words, the original heap leak accidentally masked a lifetime bug.

The bug

Short version: Removing the heap-allocated argument copy while keeping UMH_NO_WAIT introduced a classic lifetime race.

Importantly, this race does not require module unload to trigger. With UMH_NO_WAIT, argument copying may occur after the work handler returns, meaning even a still-loaded module can hand the kernel invalid pointers.

  • UMH_NO_WAIT returns immediately
  • The helper executes asynchronously in user space
  • Kernel buffers backing argv can be freed while the helper still runs

The core mistake was combining UMH_NO_WAIT with a stack-based argv. With asynchronous execution, the kernel may not copy arguments until after the work handler returns, at which point stack memory is already invalid.

In other words: the kernel was handing user space pointers to memory it no longer owned.

The fix

The smallest correct fix was to make execution synchronous and guarantee buffer lifetime.

   int ret = call_usermodehelper_exec(info, UMH_WAIT_PROC);

By switching to UMH_WAIT_PROC, the work handler blocks until the helper exits. This guarantees that exec_buf and argv remain valid for the duration of execution.

Calling flush_work() only waits for the kernel work item to finish scheduling the helper: it does not wait for an asynchronously started usermode helper to complete.

Avoiding heap allocation

With synchronous execution in place, argv no longer needed heap allocation. It was refactored into a fixed-size stack array:

   static char *argv[] = {
      "/bin/sh",
      "-c",
      exec_buf,
      NULL,
   };
  • No kmalloc_array()
  • No partial cleanup paths on failure
  • Only exec_buf requires cleanup at module exit

Although call_usermodehelper_setup() expects char * const argv[], casting from a stack-defined array is common practice in kernel code and safe here due to controlled lifetime.

Why not UMH_NO_WAIT?

Using UMH_NO_WAIT requires keeping all command buffers valid until the helper fully completes: a non-trivial lifetime management problem that introduces subtle teardown hazards.

During debugging, execution was made synchronous (UMH_WAIT_PROC) to surface exit codes and eliminate lifetime races. Once the implications were fully understood, the design was finalized to always block until completion rather than reintroducing asynchronous execution hazards. The trigger frequency is low, and correctness is worth the trade-off.

This change is captured in commit 6244abe9 .

An alternative would have been UMH_WAIT_EXEC, which blocks only until execve() succeeds and arguments are copied. For Wrong Boot, full completion tracking was acceptable, so UMH_WAIT_PROC was chosen for simplicity.

Supporting UMH_NO_WAIT safely would require explicit lifetime management and deferred cleanup hooks, which was intentionally avoided.

Additional hardening notes

  • Notifier paths may run in atomic context; any allocation there must use GFP_ATOMIC.
  • An in-flight guard can prevent repeated trigger storms or recursive activation.
  • Helper scripts should be non-interactive to avoid blocking worker threads.

Execution uses /bin/sh -c intentionally; operators who do not need shell semantics should invoke a fixed binary path directly to avoid parsing risks.

Phrase matching wasn't actually deterministic

Once crashes stopped, correctness bugs appeared.

Keyboard notifiers can:

  • Run concurrently on multiple CPUs
  • Re-enter
  • Interleave execution

Early phrase matching also used simple counters which led to:

  • Lost updates
  • Partial matches
  • Non-deterministic behavior under load

This was fixed by:

  • Introducing a small spinlock around phrase state
  • Explicitly handling reset-on-mismatch
  • Guarding against negative or non-printable key values
  • Optimizing hot paths (e.g., avoiding strlen() scans)

By the end of it, phrase matching was:

  • Race-safe
  • CPU-safe
  • Deterministic

The evolution

Idea
At last, Wrong Boot discovered what it was meant to be: a kernel trigger framework, a tool for those who understand that security is action, and control is responsibility.

Once the keyboard path was stable, the architecture was rethought.

The big shift:

  • Execution policy moved into the core
  • Triggers became pure detectors
  • A single core-owned API was introduced
   wrong8007_activate();

This enabled:

  • USB trigger
  • Network trigger
  • Multiple triggers loaded simultaneously
  • A one-shot atomic latch (first trigger wins)

Wrong Boot (codename: wrong8007) - System architecture

USB trigger

With the execution core stabilized, Wrong Boot gained its second trigger class: USB events. Unlike keyboards, USB provides physical-world signals that do not rely on user interaction, sessions, or even an unlocked system.

The USB trigger is implemented as a notifier-based detector that observes device insertion and removal events and forwards a single boolean decision to the core.

Design goals

  • No execution in notifier context (detect only)
  • Declarative rule configuration via module parameters
  • Whitelist or blacklist semantics
  • Zero dynamic allocation in the per-trigger hot path

Rule model

USB rules are parsed once at module initialization into a fixed-size table:

   struct usb_dev_rule {
      u16 vid;
      u16 pid;
      enum { USB_EVT_INSERT, USB_EVT_EJECT, USB_EVT_ANY } event;
   };

Rules are supplied as strings:

usb_devices=0x0781:0x5567:insert,0x0951:0x1666:any

During init(), strings are copied, bounded, parsed, validated, and converted into numeric rules. Invalid input causes the trigger to refuse initialization.

Notifier path

The USB trigger hooks the global USB notifier chain:

   usb_register_notify(&usb_nb);

The callback is intentionally minimal:

In practice, the callback first validates the action and object type before accessing device descriptors, ensuring it only operates on real device events and avoids undefined behavior from hub or interface notifications.

   static int usb_notifier_callback(struct notifier_block *self,
      unsigned long action,
      void *dev)
   {
      struct usb_device *udev = dev;
      u16 vid = le16_to_cpu(udev->descriptor.idVendor);
      u16 pid = le16_to_cpu(udev->descriptor.idProduct);

      if (match_rules(vid, pid, action))
         wrong8007_activate();

      return NOTIFY_OK;
   }

No allocation, no sleeping, no logging-heavy paths. The notifier only decides whether the observed event crosses the configured boundary.

Whitelist vs blacklist

A single boolean parameter flips semantics:

  • Blacklist mode (default): listed devices trigger
  • Whitelist mode: any device not listed triggers

This enables both “panic on known device” and “panic on unknown device” threat models without changing code.

USB notifier safety notes

  • USB notifier callbacks are chatty; they fire for hubs, interfaces, and non-device events
  • Early filtering is critical to avoid unnecessary matching work
  • The callback must defensively validate dev before casting
   if (!dev)
      return NOTIFY_OK;

Further filtering (e.g. ignoring hub-level events) can reduce noise and overhead:

   if (!udev || !udev->parent)
      return NOTIFY_OK;

Performance considerations

The USB bus can emit multiple notifications per physical action (ADD, REMOVE, bind transitions, interface churn). For this reason:

  • Rules are pre-parsed and stored in fixed arrays
  • Matching is a simple linear scan over a bounded set
  • No string comparisons occur in the hot path

More aggressive optimizations (hash tables, VID/PID bitmaps) were intentionally avoided: they add complexity, memory pressure, and audit surface for minimal real-world gain.

Like the keyboard trigger, the USB trigger is a detector only. Once it fires, control transfers irrevocably to the core-owned activation path.

Designing for failure

Over time, the focus shifted to safety and predictability:

  • Strict parameter validation at module load
  • Fail module initialization on malformed input
  • No runtime mutation of parameters
  • Silent disablement of misconfigured triggers
  • Clean teardown ordering
  • No residual sysfs, procfs, workqueues, or timers after unload

If something is wrong, the module refuses to load.

This is deliberate.

Tests, tooling, and auditability

To keep the kernel honest:

  • Smoke tests for load/unload
  • Cleanup tests verifying no leftover kernel state
  • Trigger isolation tests
  • Static analysis (checkpatch, smatch)
  • Multi-kernel CI builds

By late 2025, Wrong Boot wasn't just functional. It was boring in the best possible way.

Where this left 1.0

What started as:

Warning

"Type a phrase, wipe the disk"

Ended as:

Info
"A kernel-resident, trigger-agnostic, one-shot execution framework with strict trust boundaries."

It's modularity means it can perform shutdowns, encryption, or any other action you configure. The benefit is flexibility without compromising safety whether you're in public, at risk of device theft, or facing any unpredictable scenario, execution happens exactly as you intended.

Wrong Boot (codename: wrong8007) - Demo

The keyboard trigger still exists ...but it no longer serves as a gimmick.

That's the real milestone.

Looking forward

With the core execution model stabilized and trust boundaries clearly defined, the future of Wrong Boot is no longer about adding features for their own sake: it's about sharpening guarantees.

The current architecture deliberately draws hard lines: triggers detect, the core decides, and execution happens exactly once. That separation makes the system predictable, auditable, and extensible without compromising safety.

Near-term evolution focuses on hardening and operator ergonomics rather than expanding attack surface:

  • Packaging improvements such as DKMS support and install/uninstall tooling for safer deployment
  • Clearer build-time and load-time configuration paths, reducing operator error in high-stress environments
  • Optional dry-run or mock execution modes to allow safe testing without irreversible actions
  • Continued tightening of parameter validation and failure semantics to ensure fail-closed behavior

On the trigger side, the existing model already supports composition. Future work is less about inventing exotic triggers and more about refining what's already there i.e. improving correctness under stress, simplifying configuration, and documenting edge cases that only appear in real deployments.

Longer-term, Wrong Boot hints at a more radical idea: a minimal, bootable environment designed explicitly for operational survivability. In that world, the kernel isn't just a platform, it's the last trusted actor, and everything above it is disposable.

Wrong Boot is intentionally not stealth malware, not a persistent implant, and not a command-and-control system. It's future is guided by the same constraints that shaped its present: explicit operator intent, minimal complexity, and deterministic behavior under pressure.

If the project grows, it will grow in depth, not breadth.

The North Star of Wrong Boot

The guiding philosophy behind Wrong Boot is that it's engineered for what comes after the point of no return, Wrong Boot is meant to feel like:

  • A last-resort mechanism, not a participant in the unfolding situation.
  • A system that does not hesitate, deliberate, or reconsider.
  • A final will. No discretion. No appeal.

It should never feel like:

  • It does not accept instructions, negotiate outcomes, or adjust behavior once armed.
  • It does not execute conditional logic beyond its predefined trigger.
  • It must never feel like an agent, orchestrator, or control plane.
  • Not a command-and-control system.

Wrong Boot will almost always favor deterministic, explainable behavior over adaptive or intelligent behavior.

Closing thoughts

Wrong8007 came out of a period where my personal life was unraveling. What began as an urge to erase everything eventually evolved into a project that clarified what I want to build, and why.

In the age of surveillance and data overreach, having tools that respect your wishes is not paranoia, it's hygiene. Until then, type safe.

— @wrongboot

Appreciation Corner

I'd like to close with sincere thanks to the resources which helped bring this vision to life:

And a special thanks to low-level Linux docs that helped me navigate edge cases mere mortals have no business exploring.

Similar Posts

Here are some other articles you might find interesting.

Subscribe to my newsletter

A periodic update on my projects, ideas, how-tos, and discoveries.

No spam. Just occasional existential thoughts in your inbox. Opt out anytime.

piyush raj emblem

I'm Piyush and I research on all things computer science, with a special focus on applied computer security.

© 2025 Piyush Raj