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 _pbuildThat 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.