neuron-explorer
Steering-first neuron explorer for Qwen3.5-4B: push every MLP neuron, keep every rollout, browse the results
Injects out-of-tree kernel modules into a SquashFS initrd and writes it to bootable removable media. You bring the image and the modules.
Developer toolsPython · ShellGitHub @stoatworks-labsOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Puts the kernel modules you built into a SquashFS initrd, without losing ownership or setuid bits, and writes it to a bootable UEFI USB stick. You bring the image and the modules. Downloads: https://stoatworks-labs.com/software/system-graft/
Taken from the repository README
Loading the full README. The saved excerpt is shown below.
AI-assisted project. This codebase was created with Claude
(Anthropic), directed and reviewed by a human author. The patch path has 120 tests that run
on Ubuntu and macOS in CI, and I've run it against a real appliance firmware image. One image
it made, a Waves SGS 16.5 initrd with three injected drivers, has booted in a KVM virtual
machine, where those drivers loaded and drove the emulated NICs. No image has been booted
on real hardware. The USB writer, on macOS and on Linux, has only ever written to an
attached disk image, never a physical stick.Read the full README ↗AI-assisted project. This codebase was created with Claude (Anthropic), directed and reviewed by a human author. The patch path has 120 tests that run on Ubuntu and macOS in CI, and I've run it against a real appliance firmware image. One image it made, a Waves SGS 16.5 initrd with three injected drivers, has booted in a KVM virtual machine, where those drivers loaded and drove the emulated NICs. No image has been booted on real hardware. The USB writer, on macOS and on Linux, has only ever written to an attached disk image, never a physical stick. It repartitions and erases block devices, so read the safety notes and check the target twice before you use it on anything you care about.
System Graft puts out-of-tree kernel modules into a SquashFS initrd, adds a load hook to the image's init scripts, and writes the result to bootable removable media.
You bring the image and the modules. This tool ships neither, and it contains no third-party firmware or binaries. It never modifies its input either: it always writes a new file.
The usual reason you'd want it: an appliance runs a Linux image with a monolithic kernel and a
short list of built-in drivers, and the vendor, in its wisdom, didn't include one for the
hardware you've actually got. This unpacks the initrd, drops in a .ko you built, wires it
into the init sequence and packs it back up faithfully.
v0.2.3 — prebuilt for macOS and Linux. Pick your platform:
| Build | Download | Size |
|---|---|---|
| Universal (Apple Silicon + Intel) · .dmg disk image | system-graft-0.2.3-macos-universal.dmg | 37 MB |
| Universal (Apple Silicon + Intel) · .pkg installer | system-graft-0.2.3-macos-universal.pkg | 19 MB |
| Build | Download | Size |
|---|---|---|
| x64 · .deb package (Debian/Ubuntu) | system-graft_0.2.3_amd64.deb | 26 MB |
| ARM64 · .deb package (Debian/Ubuntu) | system-graft_0.2.3_arm64.deb | 25 MB |
| x64 · .rpm package (Fedora/RHEL) | system-graft-0.2.3-1.x86_64.rpm | 26 MB |
| ARM64 · .rpm package (Fedora/RHEL) | system-graft-0.2.3-1.aarch64.rpm | 25 MB |
| x64 · .tar.gz archive | system-graft-0.2.3-linux-x86_64.tar.gz | 25 MB |
| ARM64 · .tar.gz archive | system-graft-0.2.3-linux-aarch64.tar.gz | 25 MB |
All builds, checksums and release notes: github.com/stoatworks-labs/system-graft/releases.
macOS builds are signed and notarised by Apple, so they open normally — no Gatekeeper warning and no quarantine step.
unsquashfs + mksquashfsBecause doing it by hand gives you a broken image and doesn't mention it. Three things bite:
unsquashfs run as a normal user can't restore uid/gid, so after
a repack every file belongs to you rather than root./bin/busybox
is -rwsr-xr-x. Round-trip it as a normal user and it comes back as -rwxr-xr-x,
without a word.So the tool reads the full mode/uid/gid table from the source image, hands it back to
mksquashfs as a pseudo-file, sets the root inode with -root-uid/-root-gid/-root-mode,
then re-reads the output and diffs it against the source. It won't call it a success
unless the two permission tables are identical.
It also matches the source's compression and block size, so the repack isn't subtly different from what the bootloader expects.
A module only loads into a kernel whose vermagic it matches in full: kernel version,
SMP, preemption model, mod_unload, modversions. If the image's kernel is
6.12.11 SMP preempt_rt mod_unload modversions, a module built against stock Debian
6.1.0-19-amd64 SMP mod_unload modversions gets refused at load time. Silently, at boot,
on a machine you may well not have a console on.
So before it injects anything, the tool reads the vermagic of every .ko already in the
image and of every .ko you're adding, and blocks on a mismatch by default:
ERROR: vermagic mismatch for fakenic.ko:
module: '6.1.0-19-amd64 SMP mod_unload modversions'
image: '6.12.11 SMP preempt_rt mod_unload modversions'
The kernel will refuse to load this module.
Rebuild the module against a matching kernel tree, or enable the override
if you know what you are doing.
There's an override, off by default. It exists for people who know why they want it.
If CONFIG_MODVERSIONS is in the vermagic, matching the string isn't enough on its own: the
symbol CRCs have to match too. Build against the same kernel source and config as the
image's kernel.
| Doc | Contents |
|---|---|
| docs/USER-GUIDE.md | The workflow, the vermagic wall, what gets silently dropped, writing a stick, troubleshooting |
| docs/API.md | Both CLIs, the Python API, and the profile system |
| docs/DEVELOPING.md | The safety rules, the status split, and the design decisions to preserve |
squashfs-tools: brew install squashfs on macOS, your package manager elsewhere.
The build has to support the image's compressor (often zstd).python3 gui.py
Two stages, sharing one progress bar and one log:
.ko files
and press Patch.Stage 1 also has five Inspect buttons. They sit above the module list because they're how you work out which module you need in the first place, and none of them write an image:
| Button | Does |
|---|---|
| Report | describes the image and reads the kernel's build settings and embedded .config |
| Hardware… | opens a box to paste lspci -nn / lsusb output from the target, and reports coverage per device |
| Find modules… | pick a folder of modules or driver archives; every .ko in it is ranked against this image |
| Find online… | is this a stock distro kernel? which package holds the modules? Asks before downloading anything |
| Build spec… | pick a destination; writes the recovered .config and build instructions there |
The Hardware… box takes a paste rather than a file because of where that listing comes from: a machine that won't boot. It's far more often read off a screen or copied out of a console than sitting in a file on the computer running this tool.
The log records every command it runs, every check it makes and every file it touches. "Save log…" writes the whole transcript out.
The screenshots in this README predate the Inspect controls and show the older window.
python3 patcher.py /path/to/image-dir -m ./igb.ko -p sgs
-m, --module PATH module to inject (repeatable)
-o, --output PATH output image (default: <image>.patched)
-p, --profile {generic,sgs} target profile
--allow-vermagic-mismatch inject anyway; the module will not load
--allow-unsigned inject unsigned modules into an all-signed image
--allow-missing-deps inject with a dependency absent from the image
--keep-xattrs preserve xattrs (see note below)
inspection (writes no image):
--report kernel, vermagic, module count, signing, firmware
--build-spec [DIR] read the kernel's build settings; with DIR, write
its .config and build instructions there
--hardware FILE device listing from the target ("-" for stdin)
--alias-db FILE a modules.alias to name drivers this image lacks
--scan PATH rank candidate .ko files/archives (repeatable)
--want NAME restrict --scan to these module names
--find-drivers is this a stock distro kernel? which package has
the modules? (network, metadata only)
--fetch-drivers DIR download those packages into DIR and rank them
Injecting the right module is the easy half. The tool answers the other half from what the image already carries.
Which driver. Every .ko declares the PCI/USB IDs it drives, depmod collects those
into modules.alias, and drivers compiled into the kernel put theirs in
modules.builtin.modinfo. The tool reads all three, so if you hand it a device listing from
the target machine it tells you, device by device, whether this image can drive it:
lspci -nnmm > target.txt # on the target, or any Linux booted on that hardware
python3 patcher.py ./image-dir --hardware target.txt
NOT COVERED — no driver in this image claims these (1)
10ec:8168 Ethernet controller: Realtek Semiconductor Co., Ltd. RTL8111/8168
hint: Realtek Ethernet — r8169 (or Realtek's out-of-tree r8168)
covered by a module in the image (1)
8086:1533 Ethernet controller: Intel Corporation I210 Gigabit Network Connection
igb
covered by a driver built into the kernel (1)
8086:15b8 Ethernet controller: Intel Corporation Ethernet Connection (2) I219-V
e1000e (built into the kernel)
Reading modules.builtin.modinfo is what gets that third case right. Without it, hardware
that works fine because its driver is compiled in gets reported as unsupported, and off you
go building a module you never needed.
Naming the driver for hardware the image doesn't cover needs a mapping the image has no
reason to contain. --alias-db borrows one from any Linux system, which beats guessing:
python3 patcher.py ./image-dir --hardware target.txt \
--alias-db /lib/modules/$(uname -r)/modules.alias
Without one you get a coarse family hint, labelled as a hint.
Which build. Vendor driver packs and unpacked kernel packages hold builds for lots of
kernels. Point --scan at one and it ranks everything it finds against this image. It looks
through directories, tarballs, zips and .debs, and handles .ko.xz/.ko.gz/.ko.zst too:
python3 patcher.py ./image-dir --scan ~/Downloads/driverpack --want r8169
MATCH — vermagic is identical; these will be accepted by the kernel (1)
r8169 driverpack/r8169.ko
same kernel release, different build flags — will NOT load as-is (1)
ixgbe driverpack/ixgbe.ko
vermagic: 6.12.11 SMP mod_unload modversions
the image's kernel has preempt_rt; the module was not built with it
That last line's the useful bit: a mismatch names the build flag to change, rather than leaving you to diff two strings by eye.
This one has two answers, and only one of them is "go looking":
--find-drivers tells you which of the two you've got. It goes over the network, but only
for metadata:
Kernel release: 6.1.0-19-amd64
Looks like a Debian kernel (amd64).
'amd64' is a Debian kernel flavour, and the compiler string agrees (Debian).
Found 1 package(s), 65.6 MB total:
linux-image-6.1.0-19-amd64_6.1.82-1_amd64.deb (65.6 MB)
https://snapshot.debian.org/file/2ee9caba092c6e95ac73c886d04b83af7559b2df
sha1 2ee9caba092c6e95ac73c886d04b83af7559b2df — will be verified
--fetch-drivers DIR then downloads those packages and runs --scan over what's inside
them, so you end up with a ranked list of modules rather than a folder full of .deb files.
The release string decides this, not the compiler. The compiler in the banner tells you
what the build host was, which is a different question. Appliance vendors build their own
kernels on Debian boxes all the time, so gcc (Debian 12.2.0-14) on a kernel called
6.12.11 means someone compiled it on Debian, not that Debian ships it. Get that backwards
and you'd be hunting an archive for a package that was never published, so the tool spells
it out:
Kernel release: 6.12.11
!! Not a stock distro kernel.
built on Debian, but '6.12.11' is not a Debian package name — this is a custom
kernel compiled on a Debian machine, not one Debian ships.
Only Debian and Ubuntu get resolved to actual URLs, because both publish a machine-readable API I can query rather than scrape. Debian's also returns a SHA-1, and the download is checked against it. The RHEL rebuilds, Alpine and Arch are identified and named down to the exact package, but the tool doesn't build a URL for them. Their layouts vary by rebuild vendor and mirror, and an untested URL fails later, further from the cause, where it looks like a bug in this tool.
The initrd only hints at what its kernel expects. The kernel image itself (normally sitting
right next to the initrd as boot/vmlinuz) says it outright, in two places:
Linux version <release> (<builder>) (<compiler>) <build> in its rodata. That's the exact compiler, which doesn't appear
in vermagic at all, so a compiler mismatch never gets caught at load time.CONFIG_IKCONFIG carries its whole .config,
gzipped, between the markers IKCFG_ST and IKCFG_ED.Both usually sit inside a compressed payload, so the tool does what the kernel's own
scripts/extract-ikconfig does: scan for each compression format's signature, decompress
from every offset that matches, and look again in the result. It handles gzip, xz, lzma,
bzip2 and zstd, but not lz4 or lzo.
python3 patcher.py ./image-dir --report
Kernel image: demo/boot/vmlinuz-6.12.11
release: 6.12.11
compiler: gcc (Debian 12.2.0-14) 12.2.0, GNU ld 2.40
config: embedded, 9 settings recovered
What this means for a module you inject:
!! CONFIG_MODULE_SIG_FORCE=y — this kernel loads ONLY signed modules...
! CONFIG_MODVERSIONS=y — matching the vermagic string is not enough...
! CONFIG_TRIM_UNUSED_KSYMS=y — this kernel exports only the symbols its own...
CONFIG_TRIM_UNUSED_KSYMS is the one people lose days to. The kernel only exports the
symbols its own built-in code and modules use, so an out-of-tree driver can fail to resolve
a symbol that's sitting right there in the source.
--build-spec DIR writes the recovered .config, a build-spec.json and a
HOW-TO-BUILD.md with the steps to follow. The .config is the precious bit. It's the one
thing you can't reconstruct from anywhere else, and without it "build against a matching
kernel tree" is advice rather than an instruction.
Matching the vermagic is necessary, but it isn't enough. Alongside that check, the patch path refuses or flags these:
ENOKEY however well the vermagic matches, and you can't fix that without the signing
key. When the tool can read the kernel's config, this is a fact
(CONFIG_MODULE_SIG_FORCE=y). When it can't, it falls back to an inference: if every
module the vendor shipped is signed, the kernel very likely enforces it. The log says which
of the two you got, since a refusal you can check is worth more than one you can't. Blocks;
--allow-unsigned overrides.firmware= whose blob isn't in /lib/firmware
loads, then fails to bring the device up. Warns, and names the blob.insmod doesn't resolve dependencies the way
modprobe does; it fails outright on an unresolved symbol. Dependencies are checked
against the image's modules and its built-in drivers, and the injected modules are put in
order so each loads after the ones it needs. Blocks when the image ships a
modules.builtin to check against, warns when it doesn't; --allow-missing-deps overrides.python3 -m unittest discover -s tests -v
The suite builds a synthetic appliance image from scratch, with root-owned files, a setuid binary and a directory owned by a uid with no passwd entry, then patches it and checks that all of it survives. It runs as an ordinary user and doesn't need any fixtures in the repo.
Covered: setuid/ownership/root-inode preservation, module injection, hook placement and indentation, hook placement inside the correct init branch, idempotency across three consecutive re-patches, vermagic blocking and override, input immutability, output-overwrite refusal, non-SquashFS rejection, and compressor/block-size preservation.
The discovery suite builds an actual (if minimal) ELF object with a proper .modinfo
section, not just the byte-scan stand-in, so the parsing path that runs against every real
module is the one being tested. Covered: modinfo/alias/firmware extraction, signature
detection, compressed modules, modalias tokenising and three-state matching, lspci -nn /
lspci -nnmm / lsusb / modalias / bare-pair parsing, module-vs-built-in-vs-uncovered
verdicts, external alias databases, candidate ranking through directories and archives, and
each of the three boot-time checks with its override.
The kernel-spec suite builds synthetic kernels carrying a banner and an IKCFG payload, and
runs the extractor against each of them wrapped in gzip, xz, lzma and bzip2. A real kernel is
a stub around a compressed payload, so a test that only fed it an uncompressed blob would
never touch the part doing the work. It also checks that a fact beats an inference both ways
round: a config saying signing is off has to overrule an all-signed image, and with no
kernel present the inference still has to apply.
CI runs the suite on Ubuntu and macOS on every push.
A profile says where the initrd lives, which init script to hook, where in that script to put the hook, and what a module load looks like in that image's idiom.
| Profile | Image | Init script | Load line |
|---|---|---|---|
generic | boot/initrd, initrd, *.img | etc/init.d/system | insmod <path> |
sgs | boot/initrd | etc/init.d/system | Run "insmod <path>" |
The sgs profile exists because that image wraps every init command in Run "…" from
/etc/functions. Writing the load the same way keeps the patched script consistent with the
rest of the file, and keeps its logging behaviour.
Profiles are a dozen lines in patcher.py, so adding one for another image isn't much of a
job.
Immediately before the image's first existing modprobe/insmod, at that line's
indentation. That spot matters: it puts the load inside whichever case branch already loads
modules (so it doesn't also run on stop), and it runs before the network comes up.
If there isn't one, it falls back to the first ifconfig/ip link, and failing that to just
after the shebang. That last one gets flagged loudly in the log, because it's probably wrong
for your image.
The block is fenced off with sentinels:
# >>> system-graft BEGIN (generated — safe to delete this block) >>>
Run "insmod /lib/modules/6.12.11/updates/igb.ko"
# <<< system-graft END <<<
Re-patching an already-patched image strips the old block first, so it's idempotent. That's verified over three consecutive passes, with the init script coming out byte-identical to the original apart from the block itself.
It loads by absolute path with insmod rather than modprobe, so it doesn't depend on
regenerating modules.dep.bin, which you can't do portably off-target. modules.dep still
gets a text entry, for tidiness.
After patching, stage 2 writes the whole boot tree to removable media as a UEFI-bootable volume, with the patched initrd swapped in for the original. The input tree is never modified; the swap happens on the way through.
python3 usbwriter.py --list
python3 usbwriter.py --device disk4 --image-dir /path/to/image-dir \
--replace boot/initrd=/path/to/initrd.patched \
--label BOOT --yes-destroy-device
UEFI only. The volume boots through the removable-media fallback path
\EFI\BOOT\BOOTX64.EFI, which needs no NVRAM boot entry and no boot sector. If your tree
has that file, the written volume is bootable. If it doesn't, the tool warns you before
writing.
Legacy BIOS boot would need a boot sector installed (syslinux --install), which this tool
doesn't do. Nothing stops you doing it yourself afterwards.
The disk is partitioned GPT with a single FAT32 volume. If sgdisk is available and can
elevate, the partition type is set to EFI System (ef00); otherwise it's left as Microsoft
Basic Data, which virtually all firmware accepts on removable media.
This code can destroy a disk, so:
--allow-virtual.
That flag is there so the write path can be tested against a throwaway disk image instead
of a real stick, and the GUI doesn't offer it.disk4) to confirm, after showing you its
model, size, bus and current mount points.macOS (diskutil) and Linux (sgdisk/mkfs.vfat/mount, needs root) are both tested
paths, but only against attached disk images. On Linux the image is a loop device: attach it
with losetup -P so the kernel scans its partitions, then pass --allow-virtual.
Windows isn't supported for writing. The patch path does run under
MSYS2 with its squashfs-tools and python packages, as long as
MSYS=winsymlinks:lnk is set in the environment (the default symlink emulation can't create
a link to a target that doesn't exist yet, and an initrd is full of those). An initrd patched
that way booted identically to one patched on Linux. Plain Windows Python can run the
inspection commands (--report, --build-spec, --hardware, --scan) with MSYS2's
usr\bin on PATH. I haven't tried patching from it.
com.apple.*
attributes, and baking those into a Linux image is noise at best. Use --keep-xattrs if
your image does use them (SELinux labels, capabilities), but run it on Linux if you do.security.capability) live in xattrs, so they get dropped with
them. If your image relies on file caps rather than setuid, use --keep-xattrs on Linux.macOS builds are Developer ID-signed and notarised by Apple, so they open normally, with no Gatekeeper warning and no quarantine step. There's no Windows build. Windows isn't supported for writing, and the patch path runs there from source under MSYS2 (see Platform support).
Checking the macOS signature, and installing the unsigned Linux packages: docs/UNSIGNED.md.
This is a general-purpose SquashFS initrd tool. It reads an image and modules you provide and writes a new image. Whether you're allowed to modify a particular image depends on that image's licence terms, and that's your call to make. This tool doesn't decide it for you or work around it.