
Systemd Now Accepts Rust: Where the C/Rust Boundary Still Matters
Introduction
On 2026-10-24, Linuxiac and Phoronix both reported the same upstream change: systemd has merged Rust support and will accept Rust for new components. For a fifteen-year-old C codebase full of hand-rolled macros standing in for generics, containers, and error handling, that is a real policy shift — and the first thing worth pinning down is how narrow it actually is.
My position, up front: the boundary that moved is the language boundary. The ABI boundary did not move. Everything systemd exposes to the rest of the system — D-Bus interfaces, varlink definitions, sd_* symbols in libsystemd, the journal format, the unit file grammar — is still a C-shaped contract. Rust can make the inside of a component safer. It cannot make org.freedesktop.systemd1.Manager anything other than the exact signature set clients compiled against.
What follows is how that distinction plays out in practice: how a Rust component fits into the Meson build, where the FFI seams land, how to check the boundary on your own Linux machine instead of arguing about it, and why the practical risk this cycle is packaging and toolchain drift rather than memory safety.
What the Reports Say — and What They Leave Unconfirmed
Confirmed, from those two reports:
- Linuxiac, 2026-10-24, "systemd Officially Adds Rust Support for New Components."
- Phoronix, 2026-10-24, "systemd Merges Support For Rust Programs."
- Both describe the same upstream decision: Rust support is merged, and it is scoped to new components.
Not established in what I have, and I won't guess at any of it:
- Which merge request or commit carried it, and whether it is an RFC, a PR, or a mailing-list thread.
- The minimum supported Rust version systemd will declare.
- Which component gets written in Rust first — the provided reporting names no subsystem.
- Which systemd release tag the merge lands in, or whether existing C components are candidates for porting.
Worth saying plainly: Linuxiac and Phoronix are almost certainly working from a shared upstream source — likely the same merge or announcement — so two headlines are not two independent confirmations. That is my inference, not something either article states, but it matters for how much weight "both outlets say so" should carry. The scoping to new components is load-bearing too: none of this suggests a rewrite of PID 1, systemd-journald, or systemd-udevd.
What 'Accepts Rust' Means Inside a Meson and C Codebase
systemd builds with Meson. Rust builds with Cargo. Neither fact is negotiable, so "Rust in systemd" has only a few realistic shapes:
- A Cargo subproject wired into the Meson graph. Meson invokes Cargo through
custom_target()or arun_command()probe; the built artifact is astaticlib. - A static library linked into existing C binaries, exposing a small
extern "C"surface declared in a C header. The Rust side never exports a Rust ABI. - A standalone Rust binary in the systemd package that talks to systemd only through public interfaces (socket activation,
sd_notify, D-Bus).
The third is cheapest to land and safest to package, so I'd expect it first. The second is where the interesting engineering lives.
The dependency direction is the hard part. Rust code inside systemd eventually wants the same helpers the C code uses: logging macros, the hashmap, siphash24, safe string building, sd_bus wrappers. Those live in libsystemd-shared-<version>.so, a C library with internal symbols that are not part of any exported ABI. A Rust component either reimplements them (duplicated logic, slow divergence) or links them and maintains hand-written unsafe extern "C" declarations. Some of those functions — log_debug and friends — are static inline C macros, so bindgen won't reliably produce bindings, and the C side needs manual FFI shims.
Two Build Systems, One Artifact
The friction points are predictable:
Toolchain probe. Recent Meson can add_languages('rust'), but systemd will more likely use an explicit find_program('rustc') and gate the feature behind a Meson option. Every distro buildroot then has to decide whether it satisfies the probe, and the package has to remain buildable when it doesn't.
Feature plumbing. A Meson option has to reach Cargo as a Cargo feature, a cfg, or a generated file. The clean version is a generated config.rs written by Meson: one source of truth, no silent disagreement between Meson and Cargo.
Link line ownership. The C side keeps owning the final link. Cargo produces libfoo.a; Meson's executable() or shared_library() links it, and the link line must carry Rust's system dependencies (-lpthread, -ldl, -lm, -lgcc_s) because staticlib output does not encode them. Get this wrong and the link errors look like C problems when they aren't.
Offline builds. Distro builders are network-isolated. A Cargo project resolving crates from crates.io is a non-starter; you need vendored sources and --offline. This is the most common reason a Rust component fails to enter a distribution, and it's entirely solvable — if you plan for it.
Where the C/Rust Boundary Still Matters in systemd
Rust removes whole bug classes inside a component: use-after-free, buffer overflow in safe code, data races in safe code. It removes none of the boundary problems below.
D-Bus APIs Stay C-Shaped in Rust Components
The introspection data on the bus is the contract, not the implementing language. A client compiled against org.freedesktop.systemd1.Manager.StartUnit expects the method to take sss, return o, and use the error names org.freedesktop.systemd1.NoSuchUnit and org.freedesktop.systemd1.UnitExists mapped into its own error types.
If a Rust component serves that interface with zbus or sd-bus bindings, the observable contract has to be byte-identical to what the C implementation emitted, including the RESULT/VALUE column in introspection output and argument types that are signature-exact even where order doesn't matter. Renaming an error to "improve" it is a breaking change for every client. Type safety inside the implementation doesn't prevent that.
varlink and the Layout-Sensitive Wire Formats People Forget
One correction to the usual framing: varlink's wire format is text, in the JSON family. A varlink interface is not layout-sensitive; the contract is the .varlink interface file and its field names. The C-shaped risk there is naming and error conventions, not struct packing.
The genuinely layout-sensitive surfaces are the ones people forget:
- The journal file format — the on-disk entry arrays and header offsets in
systemd-journaldare read by third-party readers. Adding or reordering a field is an ABI decision no matter what language wrote it. - Manager state under
/run/systemd/— serialized structs read across a systemd upgrade or by tools that poke at them. - The EFI stub and boot loader protocol structs.
For all of those, changing field order or adding a field is an ABI decision. #[repr(C)] will faithfully reproduce whatever you tell it — including a mistake.
Legacy Subsystems and the libsystemd Shared Library Surface
libsystemd exports sd_notify, sd_notifyf, sd_journal_send, sd_journal_sendv, sd_bus_*, sd_event_*, sd_daemon. Those symbols are the public ABI. They are C and are not being rewritten. The PID 1 → manager → systemd-journald path is C and is not being rewritten either.
So a new Rust component almost always reaches systemd through FFI. The seam looks like this:
use std::os::raw::{c_char, c_int};
extern "C" {
fn sd_notify(unset_environment: c_int, state: *const c_char) -> c_int;
}
fn notify_ready() -> std::io::Result<()> {
// SAFETY: `state` is a NUL-terminated byte string with static lifetime,
// valid for the duration of the call.
let rc = unsafe { sd_notify(1, b"READY=1 ".as_ptr() as *const c_char) };
if rc < 0 {
// sd_notify returns a negative errno-style value, not -1 + errno.
return Err(std::io::Error::from_raw_os_error(-rc));
}
Ok(())
}That last comment is the whole game. sd_notify returns a negative errno-style error code. write() returns -1 and sets errno. sd_bus_call returns a negative errno-style code. Three conventions in one header file. Translating each into a Result once, at the seam, is the difference between a clean component and one where an error turns into EIO three frames later.
A Practical Check: Auditing the C Boundary on Your Own Machine
Instead of arguing about the boundary, measure it. My environment for the output below: Fedora 42, x86_64, systemd 257.5, meson 1.6.2, rustc 1.86.0. Your numbers will differ; the shapes shouldn't.
Record what you have:
$ systemctl --version
systemd 257 (257.5-2.fc42)
+PAM +AUDIT +SELINUX +APPARMOR -IMA -SMACK +SECCOMP +GCRYPT +GNUTLS +OPENSSL +ACL +BLKID +CURL +ELFUTILS +FIDO2 +IDN2 -IDN +KMOD +LIBCRYPTSETUP +LIBFDISK +PCRE2 +PWQUALITY +P11KIT +QRENCODE +TPM2 +BZIP2 +LZ4 +XZ +ZLIB +ZSTD +BPF_FRAMEWORK -XKBCOMMON -UTMP +SYSVINIT +LIBARCHIVE
Check whether a Rust toolchain probe appears in the build configuration. The systemd 257 tree I have predates the merge, so I can't show a rustc line from an installed build. That's a limit on what I can observe, not evidence about the merge.
$ meson setup build --prefix=/usr
...
C compiler for the host machine: cc (gcc 15.1.1 "cc (GCC) 15.1.1 20250521", linker: ld.bfd 2.44)
C++ compiler for the host machine: c++ (gcc 15.1.1, linker: ld.bfd 2.44)
...
Look for a rustc line next to those. Also grep the options file for a feature toggle before trusting a distro's build switches:
$ grep -rniE 'option\(.*rust|rustc' meson.options meson_options.txt 2>/dev/null
Then inspect what the installed binaries actually link:
$ readelf -d /usr/bin/systemctl | grep NEEDED
0x0000000000000001 (NEEDED) Shared library: [libsystemd-shared-257.so]
0x0000000000000001 (NEEDED) Shared library: [libsystemd.so.0]
0x0000000000000001 (NEEDED) Shared library: [libcap.so.2]
0x0000000000000001 (NEEDED) Shared library: [libselinux.so.1]
0x0000000000000001 (NEEDED) Shared library: [libcrypto.so.3]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
Observed: no Rust runtime. When a Rust component lands, readelf -d on it will still show no libstd — Rust's standard library links statically by default, so naive checks for libstd-*.so won't find one. Look instead for libgcc_s.so.1 and a larger binary, and run nm -D --defined-only to confirm only extern "C" symbols are exported.
Do not treat a vendor's or distro's build documentation as independent evidence that the component is portable — it describes one toolchain they tested. And do not run meson setup/meson install experiments on a production host's systemd; a test install into the running boot path can replace the manager that is currently supervising your session.
What Distro Packagers Have to Change
Concretely, the list:
- Rust toolchain in the buildroot. You need
rustc/cargoat a version meeting systemd's declared MSRV. Rolling-release distros are fine. LTS distros aren't, and will need a bundled toolchain or a build with the Rust component disabled. - Version skew against declared MSRV. systemd pins a minimum Rust version; your buildroot's rustc will drift behind it. Same class of problem as a C++ standard bump, with worse error messages.
- Vendored crates versus offline builds. Ship a crate tarball with a checksum manifest and set
CARGO_NET_OFFLINE=true. If the build reaches for crates.io, it's broken. - Reproducible builds. Cargo embeds absolute paths in panic messages unless you pass
--remap-path-prefix. Build-IDs and debuginfo splitting need the same treatment C gets. Expect the first few builds to be non-reproducible. - Subpackage splitting. Put the Rust component in its own subpackage so its dependency closure doesn't bloat
systemd. Rust binaries also have fewerNEEDEDentries, which can quietly change what automatic dependency generators infer. - Boot-path risk. A component that runs early and fails before
systemd-journaldis up fails silently. New components in that position need explicit fallbacks and an easy way to disable them at boot.
What Systems Developers Should Do Differently
Short and scoped:
- Keep the C ABI at the edge. Export only
extern "C"symbols from anything other components link. Everything else stays internal to the crate. - Keep
unsafesmall and audited. Put every FFI call behind a safe wrapper with a// SAFETY:comment stating the invariant: pointer validity, lifetime, thread-safety, and whether the callee retains the pointer. - Translate error conventions exactly once.
-errno,-1pluserrno, andNULLpluserrnoall need distinct handling at the seam. Do it in the wrapper, never at call sites. - Don't assume a Rust D-Bus implementation changes the observable contract. It doesn't, and clients will notice if you think otherwise.
- Verify before and after. Capture
busctl introspect org.freedesktop.systemd1 /org/freedesktop/systemd1before and after any component swap and diff it. Same for any varlink interface viavarlinkctl introspect.
busctl introspect org.freedesktop.systemd1 /org/freedesktop/systemd1 \
> before.txt
## ... swap the component ...
busctl introspect org.freedesktop.systemd1 /org/freedesktop/systemd1 \
> after.txt
diff -u before.txt after.txt
An empty diff is the only evidence that satisfies "the D-Bus contract did not change." Release notes are not evidence.
The Position: Rust Inside, C at the Contract
I think this is overdue, and I'm glad it happened. systemd's C code carries a decade of defensive macros that exist because C can't express ownership. Rust removes that entire category of problem inside new components without asking anyone to rewrite PID 1.
But this is not an ABI migration, and not a language migration. The installed base talking to libsystemd and the D-Bus interfaces doesn't change because a new component's implementation language changed. The language boundary moved inward; the contract boundary stayed where it was.
The durable skill is reading the C boundary accurately: knowing which surfaces are contracts, which are implementation details, and which conventions a header file actually promises. The practical risk this cycle is packaging and toolchain drift — an MSRV that outruns an LTS buildroot, a crate fetch that breaks an offline build, a non-reproducible binary, or a new early-boot component failing before the journal can record it. Memory safety is what Rust buys. Toolchain supply chain is what you pay for.
Further Reading
- Linuxiac — publisher of "systemd Officially Adds Rust Support for New Components" (2026-10-24). I'm linking the outlet rather than the article because the only URL I have is an aggregator redirect.
- Phoronix — publisher of "systemd Merges Support For Rust Programs" (2026-10-24). Same caveat on the URL.
- systemd repository — the merged change,
meson.options, and the D-Bus vtable sources live here; this is where to confirm the specifics I marked unknown above. - systemd project documentation — official build and interface documentation for
sd-bus,sd_notify, and the journal formats.


