2026-11-01

OpenBSD is right on mitigations (in light of recent wave of serious vulnerabilities)

(I started writing this article during the cybersec events, but only had time to finish it now.)

The past week has been crazy in cybersec, with several high-severity exploits. The PHP CVE-2026-109618/CVE-2026-109619 was nuts: a very low-level system in PHP was vulnerable, leading to pop-a-shell. Beyond that, libbrotli had CVE-2026-111497 that sits in the decompression path. ALMOST EVERY WEB FRAMEWORK eventually calls libbrotlidec for decompression -- that included Drogon, the C++ web framework I maintain. HOLY it sucks. My Gemini services were all affected by this bug.

I asked my AI to read the exploit summary because there was too much noise for me to parse. The exploit is essentially a buffer overflow in the core decoder structure; then people (and their LLMs) found ways to either ROP or overwrite a callback pointer to eventually invoke system(). Poof, RCE.

During the weekend, I spent my entire morning patching my services and enhancing the defenses I deploy. Since TLGS is the most popular service I run, that was the first thing I had to patch. My heart dropped when I ran pkg_add -ui and saw that brotli still wasn't updated. Crap... For safety, I just removed brotli altogether and let drogon compile without brotli support. But after a bit more digging, I realized it's a much smaller issue than I had thought.

All the public PoCs require a known offset for the system() function in libc, or abuse some glibc-specific feature to carry out the attack. OpenBSD helpfully relinks libc every boot - everyone runs a different copy of libc. There's no shared offset that the attacker can download and extract. And the default hardened allocator makes the addresses that the exploit needs rarer. Reports I see online say the heap is aligned about 40% of the time on glibc. OpenBSD's malloc is a lot more random, with plenty of guard pages and much farther apart addresses. These defenses mean that even with the bug and a cybersecurity-enabled AI, the AI can't exploit the bug because of how far apart the addresses are and writes just land at the wrong place all the time. Plus the location of system() is not known, so there's no place to call to anyway.

So yeah.. OpenBSD is right. I used to think OpenBSD is paranoid and it's more theoretical than practical. Turns out mitigations are very important in practice, especially in the face of capable attackers. The defenses are mostly free by today's standards and stop new attacks before how the attacks work are even known.

So now, even on Linux, all my services run with GrapheneOS's hardened malloc, limited syscall (mostly banning io_uring and exec if the service doesn't need it).. and I need to figure out a way to relink my own libc. Somehow.

PHP is one of the services getting really hammered. This is the config I ended up using for FPM.

config
[Service]
# Enable CET (blocks ROP) on x64 and load hardned_malloc to make heao exploits harder
Environment="GLIBC_TUNABLES=glibc.cpu.hwcaps=SHSTK:glibc.cpu.x86_shstk=on:glibc.cpu.x86_ibt=on"
Environment="LD_PRELOAD=/usr/lib/libhardened_malloc.so:/usr/local/lib/libphp-legacy-no-deepbind.so"
# PHP tracks allocation via Zend. Disabling it stops PHP killing large memory users. So later
# we need to have memory control at the kernel level
Environment="USE_ZEND_ALLOC=0"
ExecStart=
ExecStart=/usr/bin/php-fpm-legacy --nodaemonize --fpm-config /etc/php-legacy/php-fpm.conf --php-ini /etc/php-legacy/php-fpm.ini
ReadWritePaths=/var/lib/nextcloud
ReadWritePaths=/etc/webapps/nextcloud/config
# This needs to be tuned for your application
MemoryHigh=6G
MemoryMax=8G
MemorySwapMax=0
OOMPolicy=continue

# Deny execution everywhere, then permit PHP-FPM and its shared libraries.
NoExecPaths=/
ExecPaths=/usr/bin/php-fpm-legacy /usr/lib /usr/lib64 /usr/local/lib/libphp-legacy-no-deepbind.so

# Ban syscalls nextcloud has no business with
NoNewPrivileges=yes
SystemCallArchitectures=native
SystemCallErrorNumber=EPERM
SystemCallFilter=~@clock @cpu-emulation @debug @module @mount @obsolete @raw-io @reboot @swap
SystemCallFilter=~io_uring_setup io_uring_enter io_uring_register
SystemCallFilter=~bpf fanotify_init fanotify_mark open_by_handle_at
SystemCallFilter=~userfaultfd process_vm_readv process_vm_writev

NoNewPrivileges=yes
LockPersonality=yes
ProtectClock=yes
ProtectHostname=yes
ProtectKernelLogs=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes

Note the special no-deepbind preload. It turns out some PHP modules (specifically curl and pq) use deep binding and therefore still call the glibc allocator even when hardened_malloc is preloaded. I'm sure they have a reason, but through real-world use, I've found no issues. So a shim is in place as a workaround.

c
 #define _GNU_SOURCE
 #include <dlfcn.h>
 #include <string.h>

 void *dlopen(const char *filename, int flags)
 {
     void *(*next_dlopen)(const char *, int) = dlsym(RTLD_NEXT, "dlopen");
     if (filename != NULL &&
         strncmp(filename, "/usr/lib/php-legacy/modules/",
                 sizeof("/usr/lib/php-legacy/modules/") - 1) == 0) {
         flags &= ~RTLD_DEEPBIND;
     }
     return next_dlopen(filename, flags);
 }

Hopefully this stops most bugs. The same principle can be applied to other applicatinos too. Hopefully the approach can help people to not get hacked.