Skip to main content
  1. Posts/

g2k26: Rust in CMake, and a Heartbeat for Old Daemons

·1511 words·8 mins· loading · loading ·
Rafael Sadowski
Author
Rafael Sadowski
Shut up and hack

OpenBSD Hackathon Netherlands 2026"
#

It feels a bit strange to start this post by thinking that this is my first blog post in 2026. I think it’s because time was limited this year and I focused on doing things in OpenBSD instead of writing about it. Maybe it’s because the year did not go as planned (When has it ever been like that?). While we’re on the subject of plans: I planned to ride my gravel bike from FRA to the Hackathon in Kloosterburen. But because of illness, I ended up travelling by car. Which, of course, is simply a waste of time in the grand scheme of my life. However, I had been really looking forward to the hackathon, as the idyllic coastal region of the Netherlands offered little in the way of distractions, meaning I could focus entirely on hacking.

KDE Plasma 6.7
#

A few days before g2k26 I started to work on updating KDE Plasma and I quickly realised that some KDE Plasma modules depend on Rust in CXX CMake projects. I knew straight away that this would be “fun”, and as I’d actually intended to spend my time at the hackathon working in /usr/src/usr.sbin, I jumped at the challenge.

In KDE and maybe in other CMake ports the Rust connection happens via devel/corrosion, so I ported it and realised that all the magic happens with cxx.rs and cxxbridge-cmd. So I ported devel/cxxbridge-cmd and I quickly came to the conclusion that it’s rather silly to stick to one devel/cxxbridge-cmd version when all the ports use, or may use, different versions.

I actually knew that already, but I also knew that I could use it to resolve our PORTS_PRIVSEP. PORTS_PRIVSEP? In the ports tree BUILD_USER defaults to _pbuild and FETCH_USER defaults to _pfetch. Only the FETCH_USER is able to fetch distfiles and this only happens during make fetch.

See the last part of the default /etc/pf.conf:

# Port build user does not need network
block return out log proto {tcp udp} user _pbuild

That means that BUILD_USER is no longer able to make network connections, BUT everything out there (Go, Rust and, indeed, a lot of CMake) needs to access the internet during the build process to fetch dependencies on-the-fly. This is simply not permitted in OpenBSD, and we have to ensure that EVERYTHING is present locally before we start the build. After feedback from tb@ I ended up with the following solution:

First of all we use our devel/cargo and point to the Cargo.toml in the outer CMake port and to crates.inc. That file contains a list of all crates needed to build the Rust parts.

MODULES +=		devel/cargo

MODCARGO_BUILD =	No
MODCARGO_INSTALL =	No

MODCARGO_DIST_SUBDIR = ../cargo
MODCARGO_CARGOTOML =	${WRKSRC}/kdeds/kameleon/qmk/kameleon-qmk-helper/Cargo.toml

BUILD_DEPENDS +=		devel/corrosion

Things are a bit different here than in our usual cargo ports. devel/corrosion needs cxxbridge-cmd during the build step and will conclude that there is no system-wide version, so it will try to build the version itself. First of all, it will try to determine the version from $MODCARGO_CARGOTOML. If there’s nothing there with cxx/cxxbridge-cmd, it will build the latest version. We need to find that out and then provide all crates needed to build this version.

We need two hacks for that. Because CMake/corrosion calls cargo a few times out of your control, we have to fake a CARGO_HOME. The most important part here is 'directory = "${MODCARGO_VENDOR_DIR}"'.

CONFIGURE_ENV +=	CARGO_HOME=${WRKDIR}/cargo-home
MAKE_ENV +=		CARGO_HOME=${WRKDIR}/cargo-home

pre-configure:
	mkdir -p ${WRKDIR}/cargo-home
	printf '%s\n' \
		'[source.crates-io]' \
		'replace-with = "modcargo"' \
		'[source.modcargo]' \
		'directory = "${MODCARGO_VENDOR_DIR}"' \
		'[net]' \
		'offline = true' \
		> ${WRKDIR}/cargo-home/config.toml

The second hack is to provide our own Cargo.lock file and override the cxxbridge-cmd-${CXXBRIDGE_V}/Cargo.lock, and to drop the Cargo.lock entry from .cargo-checksum.json because otherwise cargo’s integrity check fails the frozen build.

CXXBRIDGE_V =		1.0.194
CXXBRIDGE_LOCKFILE =	cxxbridge-cargo.lock

pre-configure:
	# We ship our own Cargo.lock for cxxbridge-cmd so the build resolves
	# against the vendored crates instead of reaching out to crates.io
	cp ${FILESDIR}/${CXXBRIDGE_LOCKFILE} \
		${MODCARGO_VENDOR_DIR}/cxxbridge-cmd-${CXXBRIDGE_V}/Cargo.lock
	# Drop Cargo.lock  entry from .cargo-checksum.json because otherwise
	# cargo's integrity check fails the frozen build.
	cd ${MODCARGO_VENDOR_DIR}/cxxbridge-cmd-${CXXBRIDGE_V} && \
		sed -i 's,"Cargo.lock":"[0-9a-f]*"\,,,' .cargo-checksum.json

Last but not least we have to merge MODCARGO_CARGOTOML and CXXBRIDGE_LOCKFILE crates in .include "crates.inc". With this approach, we are able to build Rust in CMake with corrosion, and the whole thing has already been adapted to:

  • graphics/cxx-rust-cssparser
  • x11/kde-applications/akonadi-search
  • x11/kde-applications/kdepim-addons
  • x11/kde-plasma/kdeplasma-addon
# This file includes MODCARGO_CARGOTOML and CXXBRIDGE_LOCKFILE crates.
.include "crates.inc"

A heartbeat in relayd(8) and httpd(8)
#

After spending three days more or less unintentionally diving into the world of Rust and getting everything ready so I could commit to KDE Plasma, I went back to working on relayd(8) and httpd(8).

There was a bug report about the errdocs feature that interests me. It allows you to specify a folder and place files there that represent the status code you want to send. For example 404.html for HTTP Status Code 404. Someone reported that the entire page isn’t being sent and is being truncated.

There was also an analysis by an anonymous user called “Lloyd” on a mailing list. The idea was that a line might refer to a U+0000 <Null> (NUL) Unicode character in the error file, a NULL character in UTF, which forces strlen(3) to calculate the length up to this point and treat this length as a constant length in the HTTP header.

That’s a good assessment. But that wasn’t the problem. Nevertheless, httpd(8) is vulnerable to such a file.

Nobody should ship an error file with a raw U+0000 (NUL) byte in it. If you really need that code point in the content, write U+FFFD (REPLACEMENT CHARACTER) instead. That’s what an HTML5 parser would turn it into anyway:

This error occurs if the parser encounters a numeric character reference that references a U+0000 NULL code point. The parser resolves such character references to a U+FFFD REPLACEMENT CHARACTER.

https://html.spec.whatwg.org/multipage/parsing.html#parse-error-null-character-reference

We won’t be adding any extra checks in httpd(8) errdocs path. That would only slow down performance, and the site admin is responsible for their own content.

So what actually broke? The NUL theory was tempting, but the truncation happened with perfectly clean HTML too, no funny UTF-8 characters required. So instead of chasing the content, I went looking at how httpd(8) actually puts an error document on the wire.

The path ends in server_dump(), and server_dump() turns out to be almost suspiciously simple. It takes a buffer, does a single non-blocking write(2) (or tls_write(3) under TLS), and that’s it. The interesting part is what it doesn’t do: the return value (the number of bytes actually written) is thrown away.

That wasn’t and isn’t a bad idea. It’s a design decision, because httpd(8) wanted to close the connection quickly. With small error packages, it doesn’t stand out. It’s only when the error packages get large that it becomes noticeable, just as in the bug report.

httpd(8), like relayd(8) with which it shares most of its plumbing, is built on libevent. Its whole life is an event loop (the heartbeat of the daemon) that wakes up when a socket becomes readable or writable and does a little work on each beat. Almost every response in httpd(8) already rides on a bufferevent.

The abort/error path was the one place that didn’t play along. It reached around the event loop and did that single manual write instead.

Following an idea exchange with claudio@, the fix is essentially “stop being special”: hand the error document to a bufferevent and let the event loop drain it like everything else.

On the way out of this round of relayd(8) and httpd(8) work, one more report landed, and this one touched the whole family at once. All four daemons (httpd(8), relayd(8), iked(8) and snmpd(8)) share the same privilege-separated proc.c machinery. (The others have already been converted.) Its IMSG_CTL_PROCFD messages carry a destination process id and instance number that were used to index internal arrays before anyone checked them. A malicious or buggy child could hand the parent bad values and trigger out-of-bounds reads or writes. The fix, reported by Andrew Griffiths and written together with martijn@, checks the fd, the process id and the instance range before indexing anything and, for good measure, refuses to honour an IMSG_CTL_PROCFD that didn’t come from the parent.

Back home, I also finally committed a pair of diffs that had been sitting on my disk for about a week. Both do the same thing: they switch the default TLS cipher set to “secure”. For httpd(8) it comes from compat, for relayd(8) from HIGH:!aNULL. The secure keyword only allows TLSv1.3 plus the TLSv1.2 AEAD ciphers with forward secrecy (ECDHE/DHE); see tls_config_set_ciphers(3) for the details. That’s noticeably stricter than the old defaults and drops everything without AEAD or forward secrecy, so the trade-off is real: very old peers that depend on those legacy ciphers may no longer connect with the default setup.

Thanks
#

A big thank you goes out to job@, casper@ and the OpenBSD Foundation for making this happen! A special thanks to all who support my work.