-
-
Notifications
You must be signed in to change notification settings - Fork 0
synguard and synapse_kmod
The security half of the system. synapse_kmod is a DKMS kernel module that
kprobes syscalls and exports telemetry and AI scheduling hints through sysfs.
synguard is the userspace monitor: it classifies those events, scores
threats, and publishes verdicts on a feed that synui subscribes to live.
lsmod | grep synapse_kmod
cat /sys/kernel/synapse/status
systemctl status synguardsysfs surface: /sys/kernel/synapse/ — syscall_log, ai_hints, stats,
status, config, version.
- Path / argument rules on syscalls.
-
Keylogger detection — alerts on
/dev/inputaccess. -
Worm / trojan detection — a
connect()probe recording destination IP:port, anetwatchper-PID fan-out alert (≥20 distinct hosts in 10s), persistence- write rules, and anAF_UNIXfilter onsocket(). -
Escalation rules for exec out of
/procand/dev(softened from deny to escalate, with/proc/self/exeexplicitly allowed).
chibi has a read-only sentinel aspect that watches the same feed.
There are two entirely different things synguard can do about a rule match, and
the difference decides whether anything was actually prevented.
| post-hoc kill | in-kernel gate | |
|---|---|---|
| Mechanism | kprobe sees the syscall, daemon sends SIGKILL
|
BPF-LSM file_open hook returns -EPERM
|
| Timing | after the fact | before the file is opened |
| Prevents the access? | no — it punishes it afterwards | yes |
| Needs | nothing |
--bpf-enforce, and a kernel with the BPF LSM active |
synguard --mode enforce --bpf-enforceMode is separate from the gate. --mode decides whether a DENY verdict does
anything at all; --bpf-enforce decides how:
| Mode | Meaning |
|---|---|
audit |
log only, never block — the default, deliberately |
enforce |
act on DENY verdicts |
learning |
build a baseline profile, flag anomalies |
lockdown |
block everything not explicitly allowed |
Without --ai-enforce the classifier is advisory: a model verdict is clamped
to alert and cannot reach a kill. Only rule verdicts can.
-
It runs after the ordinary permission check.
security_file_openis reached only once DAC has already said yes, so an open that fails withEACCESorENOENTnever reaches the gate at all. The gate can refuse an access that would otherwise have succeeded — it cannot "catch" one the kernel already refused. -
The
bpfLSM runs last in the stack and is additive. It can add a refusal; it cannot overrule an AppArmor or SELinux decision made earlier. Returning0means no opinion, not allow. -
It cannot suspend a process. An LSM hook has no way to
SIGSTOP, so quarantine is never lowered to the kernel — that verdict is userspace-only. - The deny target must exist. BPF never sees an open of a path that isn't there, and the post-hoc path sees it too late to matter. A rule pointed at a file that does not exist looks armed and does nothing.
Enforcement declines to kill in exactly two cases, and both fail toward enforcing — when in doubt it still acts:
- The syscall failed. Nothing was accessed, so a kill prevents nothing.
- The gate is armed and live for that exact rule — the kernel already refused it, and a userspace kill only widens the blast radius.
An unknown outcome still kills, because "unknown" is not "failed". A rule the kernel was never given still kills, because then the post-hoc path is the only enforcement there is.
Detection is untouched in both cases: the alert still fires and the audit line is
still written, with the reason enforcement stood down. Two counters make it
visible rather than silent — failed-syscall-skips and kernel-enforced-skips
in the stats line.
Why this matters. Before the kernel module reported the syscall's return value, an event said "this process opened this file" when what had happened was "it asked, and the kernel said no". Both were treated as an access. A process could be killed for an open it was never allowed to complete — and
ld.soprobes a nonexistent/etc/ld.so.preloadon every exec, so a deny rule on that path is a rule against starting programs.
1. Check the kernel already offers it. On stock Arch it does:
cat /sys/kernel/security/lsm
# capability,landlock,lockdown,yama,bpf ← 'bpf' must be in this listIf bpf is absent, add it to the active LSM list on the kernel command line
(lsm=capability,landlock,lockdown,yama,bpf) and reboot. It needs
CONFIG_BPF_LSM=y and kernel BTF, both of which Arch's linux ships. bpf
sits last in that list on purpose — the stack is additive, so this can add a
refusal but never overrule an AppArmor or SELinux decision made earlier.
2. Know that nothing ships armed. SynapseOS installs 55 rules and not one
deny or quarantine. The daemon runs --mode enforce, so acting is
permitted; nothing loaded ever asks for it. As installed, synguard detects and
alerts — it does not kill.
3. Write a rule the kernel can actually run. A BPF program gets map lookups
and bounded string work — there is no fnmatch. synguard compiles each deny
rule down to an exact string or a prefix and refuses anything it cannot express
exactly, because approximating in either direction is a bug you would not find.
| Enforceable in-kernel | Userspace only |
|---|---|
path /etc/ld.so.preload — exact |
path /tmp/*.sh — * anywhere but the end |
path /etc/profile.d/* — a directory, one level
|
path /etc/*/shadow |
comm sshd — exact |
path /dev/input/event? — ? and […] have no lowered form |
comm python* — a prefix |
a deny rule with no path (it would match every open) |
Four things must all be true, or the rule stays on the userspace path:
-
event openorevent exec— the hooks arelsm/file_openandlsm/bprm_check_security. socket, ptrace, module, mount and setuid have no hook yet. -
verdict deny, notquarantine— a hook can return-EPERMand nothing else. - the patterns lower (table above).
-
no earlier rule may pre-empt it. Rules are first-match-wins, and the kernel
only knows the rules it was handed, so synguard refuses to lower a deny rule
unless it can prove no earlier rule matches the same input. This is the part
most likely to bite: the stock
00-base.rulesopens with allow rules at priority 1.
event execmatches the binary, andcommis the process CALLINGexecve— the kernel does not install the new name until after the hook. Socomm nginx+path /usr/bin/shmeans "nginx ran a shell". A rule naming the target binary incommnever matches. An exec also opens its binary internally; synguard skips those (FMODE_EXEC) so anevent openrule does not accidentally block execution.
4. Arm it. --bpf-enforce is deliberately not in the shipped unit —
loading a policy and arming it are separate decisions, because this path
prevents rather than reacts:
sudo systemctl edit synguard
# [Service]
# ExecStart=
# ExecStart=/usr/bin/synguard --foreground --mode enforce \
# --rules /etc/synguard/rules.d/ --bpf-enforceWithout it, an enforceable rule is still compiled and loaded into the kernel maps — the daemon says so — but the gate stays shut.
5. Read back what actually happened. Every outcome has a distinct line:
journalctl -u synguard | grep bpf-lsm| Line | Meaning |
|---|---|
policy loaded — N enforceable rules |
lowered and in the maps |
enforcement ARMED |
the gate is live |
N rules loaded but NOT armed (--bpf-enforce …) |
loaded, gate shut |
policy NOT loaded — rule '…': path pattern cannot be expressed in-kernel: "…" |
that rule named, userspace path unaffected |
unavailable — synguard is detect-only this boot |
no BPF LSM on this kernel |
The way out, if an armed rule locks you out of your own machine: add
synapse.bpf_enforce=0
to the kernel command line at the boot menu. synguard then never loads the BPF object and comes up detect-only. Do that before reaching for a live USB. The gate also fails open if synguard dies or wedges, and refuses to act for the first 30 seconds after it attaches, so a badly wrong rule still lets you log in.
A false positive costs a process tree. A kill takes the matched process and its descendants. Only PID 0/1, kernel threads and the SynapseOS daemons are protected — your desktop, editor and browser are not.
The authoritative long-form version ships on the system, with worked examples:
/etc/synguard/rules.d/40-enforce.rules.example. It ends in .example, so
nothing parses it until you copy it to .rules — and every rule in it is
commented out.
synguard: stats — events=… rules=… ai=… ai-skipped=… denials=… alerts=…
quarantines=… protected-skips=… stale-pid-skips=… failed-syscall-skips=…
kernel-enforced-skips=… dropped=… suppressed=… lag=Nms/Nmax
-
droppedabove zero means the ring overflowed and events were lost — a detector that misses what it was watching for reports nothing, which reads identically to a quiet system. -
lagis how far behind the reader is. It exists because a blocking model call used to run inside the ring-drain loop and could stall it for seconds; classification now runs on its own worker thread, and the reader keeps draining. -
denialscounts verdicts, not preventions. Cross-check againstkernel-enforced-skipsto see how many were actually refused in-kernel.
Run the thing you expect to be denied as a transient unit, never from your own shell:
systemd-run --collect --pipe --wait -- cat /path/covered/by/the/ruleA DENY that resolves to a SIGKILL kills the process tree, and a shell you
typed the command into is part of that tree. Testing this from a terminal is how
you lose the terminal — and, if it was your login session, everything in it.
-
Kernel-side protected-PID guard on
ai_hints— previously anyone could writeHINT pid=1 class=idleand DoS the system. - Probe self-integrity watchdog.
-
Lockdown self-pin —
rmmodreturnsEBUSY. - Tightened sysfs permissions;
kptr_restrictraised0 → 2. - An opt-in
modules_disabledunit.
Why
sig_enforceis not on by default here: with Secure Boot off — which is the shipped state — enablingmodule.sig_enforce=1refuses to load unsigned out-of-tree modules, which on an NVIDIA machine means no GPU driver.syn-secureboothandles this properly — see Secure Boot.
For the entire life of the project, every path and argument rule was silently
dead. ai was 0 throughout.
The kmod's kprobes read regs->di as the first syscall argument. But at a
kprobe on a syscall entry point, regs->di is not the first argument — it's a
pointer to the user's pt_regs. Every rule that inspected a path or an
argument was therefore reading garbage, matching nothing, and reporting clean.
The fix (a6ce919, kmod + synguard pkgrel 6) was a syscall_uregs() deref. It
resurrected detection wholesale and armed a pile of rules that had been
dormant since they were written.
The lesson: a security monitor that reports "clean" is indistinguishable from a security monitor that is broken. If you add a rule, you must also add a positive test that makes it fire — otherwise you have no evidence it works, and silence will convince you everything is fine for months.
The security feed leaked a client slot per departed subscriber. It wedged at
16 clients and then refused every new subscriber ("at capacity") while spamming
the log about it forever. Fixed with a poll() reap and a 60s log throttle
(pkgrel 3, 33d8272).
Stale DKMS modules survived kernel upgrades, failing to load with ENOEXEC
while dkms status insisted all was well. See
Development Notes —
the short version is never trust dkms status; compare vermagic to
uname -r.
Hardened systemd units didn't take effect because stale copies in /etc
shadowed the packaged ones in /usr/lib. systemctl show -p FragmentPath.
See also: Secure Boot, Troubleshooting.
Using it
- Installation
- Updating
- Software
- Files
- Keybindings
- Commands
- Nix
- Gaming
- DaVinci Resolve
- Secure Boot
- Troubleshooting
Customising it
Components
Apps
Hacking on it