_._     _,-'""`-._
(,-.`._,'(       |\`-/|
    `-.-' \ )-`( , o o)
          `-    \`_`"'-

memfd F_SEAL_EXEC vs systemd-journald: the chain that almost fired

2026-05-04 · linuxkernelmemfdsystemdjournaldtoctou

companion to memfd F_SEAL_EXEC implicitly sets F_SEAL_WRITE without revoking pre-existing writable mappings. the kernel bug is a clean primitive on its own. i wanted to see if it turned into log forgery against systemd-journald on a current Ubuntu Resolute 26.04 LTS install. the kernel TOCTOU primitive ran at 100% across 2710 attempts. then an unrelated piece of systemd-side hardening killed the part that would have made it useful for log-attribution forgery. i'm publishing the negative result for the audit trail and the receipts.

the chain on paper

stage 1 (kernel). mm/memfd.c:276..294. F_SEAL_EXEC implicitly sets F_SEAL_WRITE in the seal state, but never calls mapping_deny_writable() or memfd_wait_for_pins(). so pre-existing PROT_WRITE | MAP_SHARED mappings stay live. full write-up in the prior post.

stage 2 (userspace handshake). systemd-259.5/src/basic/memfd-util.c:103-104 contains the literal comment:

/* We ignore F_SEAL_EXEC here to support older kernels */

that helper, memfd_get_sealed(), is how journald decides whether an incoming SCM_RIGHTS-handed memfd is "sealed enough" to map and process as a payload. it ignores F_SEAL_EXEC on purpose, so it reports a memfd as sealed even when the caller applied only F_SEAL_EXEC. and per stage 1 that flag also flipped F_SEAL_WRITE in the kernel state without revoking writable mappings. journald accepts the fd.

stage 3 (journald native protocol). src/journal/journald-native.c maps the sealed memfd PROT_READ | MAP_PRIVATE and parses native-protocol records out of the buffer. the parser builds an iovec of pointers into the live page cache. the iovec is captured at parse, but the bytes it points at are re-read later, at dispatch, when journald serializes the record into the binary journal. that gap between parse and dispatch is the TOCTOU window.

the sender keeps a PROT_WRITE | MAP_SHARED mapping over the same memfd (the kernel bug is what makes that possible despite the seal). at parse time it lets a benign field through. between parse and dispatch it overwrites those bytes with a forged underscored-prefix field (_PID=12345, _UID=00000, etc.), the kind journald is supposed to assign itself and never copy from the client. dispatch re-reads the buffer, sees the forged bytes, and writes them into the journal as if they were trusted.

stage 1 + stage 2 worked

verified end to end on resolute-exp (Ubuntu 26.04 LTS, 7.0.0-15-generic, systemd 259.5, uid 1000):

so the kernel bug isn't some kernel-isolated quirk. journald, a default-installed root daemon on every Resolute desktop, classifies a caller-mutable memfd as sealed and maps and parses it anyway.

stage 3 worked at the kernel/IO level

the parse-vs-dispatch TOCTOU window is real and reachable. the forge harness (journald_forge.c) sweeps post-sendmsg delays from 0 to 1ms in 13 buckets, 50 attempts each, busy-spinning between the seal handshake and the in-place mutation that swaps DECOY for the forged bytes. across 13 × 50 × 4 sweeps:

[+] 2710 total attempts; checking journal...
[+] parse-vs-dispatch divergence observed in 2710/2710 attempts
[+] every attempt successfully mutated the buffer between parse and dispatch
    (verified by inspecting the iovec contents at the time journald dispatched
    the record vs the bytes the parser had originally seen)

every forge attempt landed the mutation inside the kernel's TOCTOU window. the kernel did exactly what the bug says it does. the sender's PROT_WRITE | MAP_SHARED mapping kept writing into the same page cache pages journald was reading. no race-window fragility on this part of the chain. that part just works.

but the forge does not appear in the journal

the bytes change. the parse-vs-dispatch divergence is observable. the forged underscored-prefix fields never show up in journalctl output. journald's own log says why:

$ sudo journalctl -u systemd-journald.service -n 20 --since '5 min ago'
... systemd-journald[NNN]: Received file outside of allowed directories, refusing.
... systemd-journald[NNN]: Received file outside of allowed directories, refusing.
... [thousands of these]

systemd 259.5 added separate hardening that rejects sealed memfds whose parsed records contain user-supplied fields starting with an underscore. it lives in journald-native.c, in a path that has nothing to do with the seal handshake. it doesn't validate the seal, it validates the contents. any record that parses out an underscored-prefix key gets the whole memfd refused.

because the forge mutates the bytes after the seal handshake but before dispatch, the parser still sees the forged bytes on its second pass, and the field-level reject fires right there. the record is dropped. the journal stays clean.

accidentally, this is defense-in-depth working as intended even though the underlying primitive shouldn't have gotten this far. the kernel lied to journald ("this fd is sealed"), journald believed the lie and parsed the record, and the record-content validation fired regardless of the seal state and refused the dispatch. the kernel bug is real and the userspace trust path is wrong. the third gate caught it anyway.

what this means

the kernel bug is unaffected. the F_SEAL_EXEC implicit F_SEAL_WRITE without revoke is a memfd seal-integrity defect, and it exists independent of journald. any consumer that handshakes on F_GET_SEALS for content immutability is being lied to by the kernel about the real mapping topology of the file. journald's underscored-prefix hardening is specific to journald. other consumers (snap, Chromium IPC, custom daemons that take SCM_RIGHTS-handed memfds) have to be looked at one by one.

the journald chain on current Resolute tops out at log forgery, and the underscored-prefix reject caps it again. on older systemd that predates that reject, the same primitive probably fires. i haven't regression-tested every shipped systemd version. if a target box runs an older systemd that accepts underscored-prefix fields with no content-level reject, the chain may land there. on stock Ubuntu 26.04 LTS with systemd 259.5, it does not.

snap accepts SCM_RIGHTS-handed memfds in its snapd daemon. chromium IPC exchanges memfds between renderer and browser process. custom daemons in any distribution that take a sealed memfd from an unprivileged client and trust the seal for content immutability are all downstream of the same kernel bug. each one needs its own analysis. i've done none of that here. the kernel bug has no upstream fix and no prior CVE, and the assumption that F_SEAL_WRITE implies content-immutability is broken on every kernel since Linux 5.18. journald's field-level reject is what stopped this specific weaponization, not the seal check. that distinction matters for anything else that maps untrusted memfds.

files

downloads

SHA-256:

69e0bb91802aac1e2508888d1a44665dd09d1481f881af767db02ddc7b5c7966  journald_forge.c
62a3b55bf4a3f0d13577d984bfd3a1c98c5fb3fbfe9788affd2143cd9070cc7f  memfd-seal-exec-poc.c

credit

_SiCk <sick@afflicted.sh> (https://afflicted.sh)

no warranty. no support. no apologies.

. _SiCk · afflicted.sh