Vulnerability avoidance

Software is complex, so it has bugs, and some of those bugs affect security. Lots of effort is spent finding and fixing these security bugs (vulnerabilities) before they are exploited to harm people or companies.

In 2023, io_uring accounted for 60% of valid exploits in Google's bug bounty.

After fixing them, they:

  1. Turned off io_uring on ChromeOS
  2. Made it unreachable for Android apps
  3. Disabled it on production servers

Every io_uring vuln that nobody had found yet stopped mattering after that.

That's vulnerability avoidance: work that makes vulns impossible, unlikely, or irrelevant, before anyone finds them. This article is about the ways you get there.

A whack-a-mole machine

Four kinds of avoidance

Avoidance comes in four kinds. Each asks a different question:

  1. Delete — Does this need to exist? (the code, feature, dependency, or data)
  2. Prevent — Can we make this kind of bug impossible to write? (the class of bug)
  3. Contain — When an attacker breaks this code, what can they get? (the damage)
  4. Harden — How expensive is it to turn a bug into an exploit? (the exploit)

Delete > prevent > contain > harden. Deletion is best because the code is gone. Prevention is next because the bug class is gone. Containment and hardening are fallback layers.

Workplace safety has the same idea. The NIOSH Hierarchy of Controls goes elimination (delete), substitution (prevent), engineering controls (contain, harden), and puts training and protective gear last, because those depend on people getting it right every time.

1. Delete

Delete asks: Does this need to exist?

You can delete code, features, dependencies, and data.

Code — No code, no vulns. OpenBSD treats deletion as a culture. Its release notes list removals right next to new features, as accomplishments: Bluetooth, Apache, and Kerberos (5.6); sudo swapped for the much smaller doas (5.8); Linux binary emulation (6.0); and SMT off by default, because threads on one core can leak data to each other (6.4). After Heartbleed, OpenBSD forked OpenSSL into LibreSSL and deleted about 90,000 lines of C out of 388,000 in the first month¹. Anthropic's attack surface reduction work is a great recent example. Claude searched 25 open-source projects for code that attackers could reach but deployments didn't need. In OpenSSH, that included about 4,500 lines of experimental post-quantum code reachable before login. The fix cut about a fifth of sshd's pre-auth code.

Dependencies — Dependencies are code you run but rarely read. The xz backdoor reached sshd through a chain: some Linux distros patched sshd to depend on libsystemd; libsystemd depended on liblzma; and liblzma came from xz, which had the backdoor. The fix was deletion: OpenSSH 9.8 added its own small systemd notification code, so sshd no longer needed libsystemd for that job. Most codebases have dependencies like this: imported for one helper, kept forever, and reachable from places that matter.

Features — Apple's Lockdown Mode deletes features from a whole phone: most message attachments, JIT JavaScript, and more. It blocked BLASTPASS. In March 2026, Apple said it knew of no successful mercenary spyware attack against a device running it. Microsoft found that about 45% of V8 CVEs were in the JIT, so Edge added a mode with no JIT. After Log4Shell, Log4j 2.16 removed message lookups entirely and turned JNDI off by default, because "it would be safer for users to completely disable it by default."

Data — You can't leak what you don't have. When a court orders Signal to hand over user data, Signal can provide two things: when the account was created, and when it last connected.

The cost: users lose features, and sometimes performance. The usual compromise is "off by default." Nextcloud ships previews for PDF, SVG, Office docs, and video turned off "due to security and performance concerns."

Try this: ask your AI agent to "list every input format, protocol version, endpoint, feature flag, and dependency in this repo, who uses each one, and what breaks if we remove it." Delete what nobody uses. Put what few people use behind a flag that's off by default.

2. Prevent

Prevent asks: For the code that remains, can we make this kind of bug impossible to write?

Defaults are powerful — One changed default can protect everyone at once. In 2020, Chrome made SameSite=Lax the default for cookies that didn't set the attribute, so those cookies stopped going out on cross-site POST requests. That shut down classic POST-based CSRF for Chrome users, with no code change from site owners.

New code, safer language — Memory-safety bugs were 76% of Android vulns in 2019, 24% in 2024, and under 20% in 2025. Google didn't rewrite its old C and C++. It wrote new code in memory-safe languages, mostly Rust, and let the old code age. Bugs pile up in new code, so changing what new code is made of was enough to bend the curve.

Safe APIs — You can kill bug classes in old languages too. OpenBSD added strlcpy in 1998. Later came reallocarray, for integer overflows in allocation sizes, and explicit_bzero, so that compilers can't remove the code that wipes secrets. The trick is to make the safe API the easy one, and then make the unsafe one a lint error.

The web — Parameterized queries make SQL injection impossible to write. Google's Trusted Types does the same for DOM XSS, and Google reported zero DOM XSS in the apps that moved to it. Secure-by-default frameworks always need an escape hatch, so give the escape hatch a scary name. React's dangerouslySetInnerHTML is the classic. In 2024, Pillow, the Python imaging library, deprecated ImageMath.eval() and split it into a safer lambda_eval() and an unsafe_eval() that keeps the old behavior. Nobody types those by accident, and they're easy to grep for.

Proofs — Formal verification is a form of prevention, albeit expensive and rare but it happens. In 2019, about 90% of Chrome's secure connections used fiat-crypto, which has a proof of correctness². Most of us will never write a proof, because proofs are slow, specialized work. But maybe AI makes this cheaper and saves the day here.

The cost: migrations, plus the cognitive load of programmers learning a new API.

Try this: "Sort our past security bugs into classes. For each class with two or more bugs, what API, type, default, or language would make it impossible to write?" Then move every call site, and add a lint rule that rejects the unsafe form.

3. Contain

Contain asks: When an attacker breaks this code, what can they get?

Big platforms do this now — After Stagefright, Android 7.0 split its media server into several processes, each locked down with SELinux and seccomp. iOS 14 put iMessage parsing in BlastDoor, a sandboxed service written in Swift. Windows 10 moved font parsing out of the kernel.

OpenBSD makes it easy — pledge and unveil let a program lock itself down after it starts:

unveil("/var/www/uploads", "r");
unveil(NULL, NULL);
pledge("stdio rpath", NULL);

After those three lines, the program can read one directory. It can't write files, open network connections, or start other programs. If it tries, the kernel refuses or kills it. Because the API is easy, adoption was fast: in the first release with pledge, 453 of the 707 programs in the base system used it.

The evidence for containment is indirect but strong. In 2019, Project Zero looked at 5 iOS exploit chains from the wild. Together they needed 14 bugs: browser bugs, kernel bugs, and sandbox escapes. Just keep raising the cost for the attacker.

The cost: complexity and overhead. More processes, more IPC, and policies that break when the code changes. Chrome's Site Isolation used 9–13% more memory when it shipped.

Try this: "Apply Chromium's Rule of 2 to this codebase. List every component that handles untrusted input, in a memory-unsafe language, with high privilege. For each one, propose a change that removes one of the three."

4. Harden

Harden asks: How expensive is it to turn a bug into an exploit?

This level doesn't remove bugs. It breaks the techniques that attackers use to exploit them. A lot of it comes from your platform (the OS, the compiler, the hardware), but not all of it.

Hardening comes fourth because the bug is still there, and attackers adapt. In April 1997, Solar Designer posted a Linux patch that made the stack non-executable. In August, the same author posted a way around it: return-into-libc. Cost still matters, though. Hardening is a big part of why a modern exploit chain needs many bugs instead of one.

The cost: some performance, which you should measure. Some latent bugs turn into crashes, which is arguably a feature.

Try this: "Compare our compiler, linker, and allocator settings to the OpenSSF hardening guide. What are we missing, and what would each change cost?"

End to end example

Imagine you develop software that has image upload functionality. Media parsers are behind a lot of famous attacks: Stagefright (2015), ImageTragick (2016), FORCEDENTRY (2021), BLASTPASS (2023). Here is how these four may play out.

Level Question For the image upload
Delete Does this need to exist? PNG and JPEG only, checked by content. Unused ImageMagick coders and untrusted libvips loaders off. Strip EXIF. Don't keep originals you don't need.
Prevent Can this bug be impossible to write? A memory-safe decoder (Wuffs, Rust).
Contain What can an attacker get? The decoder runs in its own process: no network, no files, no cloud role. Uploads are served from a separate domain.
Harden How expensive is an exploit? Hardening flags, a hardened allocator, memory tagging.
Find & fix Where is the next bug? Fuzz the decoder. Ask an AI to review it. Use each bug to improve a row above.

Why nobody does this

Vulnerability avoidance is, in my opinion, underdiscussed and underinvested in. Most security work is a treadmill: find a bug, fix it, find the next one. This is not new.

In the 1990s, djb wrote "I started working on qmail because I was sick of this cycle of doom."

4 reasons:

It's hard to measure. Finding bugs: exciting, and countable. You get CVEs, bounty payouts, conference talks, and a graph that goes down. The avoidance of bad outcomes is always harder to measure.

It has no names. Teams don't plan, measure, or ask for work they can't name. "Rewrite it in Rust" is the closest we've got.

It used to be expensive. Avoidance usually means a big, boring migration across the whole codebase. Removing variable-length arrays from the Linux kernel took more than 150 commits over several releases.

You have to do it yourself. Not all infosec teams are comfortable changing product code. No vendor can sell you something to do this.

CISA and the FBI have pushed vendors to publish memory-safety roadmaps, which is a good start. But I'd bet few Fortune 500 security roadmaps have items like "delete features nobody uses" or "sandbox every parser."

Conclusion

The treadmill won't stop, and AI is speeding it up for both sides. Keep finding and fixing. But every time you fix a bug, ask which of the four levels would have killed its whole class, and go do that too.

There's lots of good news. AI is just as good at avoidance because a lot of this work is just big refactors. And there's a nonzero chance that you can point your AI at this article and have it suggest some projects for your codebase. The "try this" prompts are a decent place to start.

"Rewrite it in Rust" indeed!


Footnotes

  1. Bob Beck's slides describe what went: "tens of thousands of lines of awful hacks and #ifdef horror (Hello VMS!)" LibreSSL's change log later listed OpenSSL CVEs that "did not apply," like FREAK and Logjam. The change log doesn't always say why.

  2. Python 3.12 and later also use verified HACL* code for the built-in hash functions it falls back to when OpenSSL isn't available. In 2011, researchers fuzzing C compilers found hundreds of bugs in GCC and LLVM, and no wrong-code bugs in the proven part of CompCert. They wrote: "This is not for lack of trying: we have devoted about six CPU-years to the task."