Six Years, 360 Patches, and One Deprecated Function: What Linux’s strncpy Removal Actually Tells Us About C’s Debt Problem
Linux 7.2 is dropping strncpy() from the kernel’s internal API. After six years and 360 patches spread across dozens of contributors, one of C’s most familiar string functions has been systematically excised from the kernel source tree. Phoronix covered the final merge last week, and the Hacker News thread today hit 200 points within hours — which tells you this story resonates far beyond kernel developers.
I want to argue that the strncpy removal is not mainly a security story, even though buffer overflows are mentioned in every headline about it. It’s primarily a language debt story. And that distinction matters a lot for how we should think about the decade ahead in systems programming.
But first, let me explain why I care personally. I’ve been writing C and systems-adjacent C++ since around 2010. I’ve shipped production network daemons, a custom memory allocator that shipped inside a commercial database product, and I spent three months in 2022 auditing a legacy C codebase for a client with roughly 400,000 lines of code written across fifteen years by people who no longer worked there. That audit is where I developed a deep appreciation for exactly why strncpy is special — not just as a security risk, but as a precise specimen of how API design debt accumulates and how long it persists.
What strncpy Actually Does (Versus What Everyone Thinks It Does)
Here’s the function signature:
char *strncpy(char *dest, const char *src, size_t n);
Ask any intermediate C programmer what strncpy does and they’ll say: “It’s the safe version of strcpy — copies at most n bytes, so you won’t overflow the buffer.” That answer is partially correct and mostly dangerous.
The actual behavior has two quirks that bite people repeatedly, and they interact in ways that make both worse.
First: if src is shorter than n, strncpy zero-pads the remaining bytes in dest all the way to n. This is a behavior inherited from strncpy’s original POSIX purpose — populating fixed-size fields in filesystem directory entries, where consistent null-padding across a fixed-width field matters for binary comparison and reliable iteration. Modern usage almost never wants this. You’re paying for a memset you didn’t request, burning CPU cycles and potentially cache lines for no benefit.
Second, and far worse: if src is longer than n, strncpy copies exactly n bytes and does not null-terminate the destination. The resulting content in dest is not a valid C string — it has no terminating null byte. Every subsequent operation that treats dest as a null-terminated C string is undefined behavior: strlen(dest) will read past the end of the buffer, printf("%s", dest) will print garbage until it hits a zero byte somewhere in memory, strcpy(other, dest) will overflow other.
So the function that everyone reflexively uses as “safe strcpy” will silently produce an unterminated string whenever the source is longer than the limit. This is not a corner case. This is the failure mode that matters most in security contexts — the one that’s triggered precisely when an attacker supplies a string that’s longer than expected.
In my 2022 audit, I found eleven strncpy call sites across the codebase. Four of them had the null termination bug — they wrote into a buffer, then used that buffer in string operations without ever ensuring it was terminated. Two of those four were reachable from network input without authentication. Neither had ever been caught by the existing test suite, which never generated inputs long enough to trigger truncation.
That’s 18% of the strncpy call sites in one codebase being actively dangerous. And this was a professional, maintained, commercially-shipped product with a QA team. The function’s interface just makes it too easy to get wrong.
Why the Function Was Designed This Way
Understanding why strncpy works the way it does requires going back to its original context. The function was not designed as “safe strcpy.” That was a folk interpretation that developed decades after the fact.
strncpy was designed to populate entries in the Unix V7 directory structure. Each entry was a fixed-width structure, and the filename field was 14 bytes — not null-terminated, just 14 bytes, possibly null-padded to fill unused space. strncpy was the natural tool for writing into that field: copy up to 14 bytes of the filename, then zero-fill the rest if the name is shorter. The null-padding behavior was a feature, not a bug, for that use case. The no-null-termination-on-overflow behavior didn’t matter because the field was never treated as a null-terminated string — it was compared byte-for-byte against other fixed-width fields.
The problem is that when strncpy was standardized into C89 and later C99, it was standardized with this original, specialized behavior intact. And the function name — “string n copy” — invited programmers to interpret it as a length-bounded string copy, which it isn’t. The semantic mismatch between what the name implies and what the function does is the root cause of thirty-plus years of bugs.
This is a pattern you see over and over in language and library design: a function or API is designed for a specific, narrow use case, then gets generalized use it was never intended for, accumulates a population of users who believe they understand it, and by the time the semantic mismatch becomes widely understood, it’s too deeply embedded to easily remove.
Why It Took Six Years and 360 Patches
The Linux kernel contains, depending on how you count, somewhere between 20 and 35 million lines of C code across all maintained subsystems. Removing a single API function across that codebase is not a search-and-replace operation. Every strncpy call site had to be individually audited: what are the size constraints, what is the source, what is the destination buffer size, what happens after the copy, does any downstream code depend on the null-padding behavior (as opposed to the null-termination behavior)?
The null-padding question is actually subtle. Some subsystems genuinely used strncpy for its intended purpose — writing into fixed-width fields where null padding matters. Those sites couldn’t simply be replaced with strscpy; they needed a different treatment, typically explicit memset followed by memcpy.
The replacement for the common case — bounded copy where you want null-termination and an error on overflow — is strscpy(), a kernel-internal function that was introduced specifically for this migration. strscpy:
- Always null-terminates the destination (even on truncation)
- Returns the length of the copy on success
- Returns
-E2BIGif the source was truncated - Does not zero-pad unused bytes (unlike strncpy)
The error return is the critical design improvement. With strncpy, truncation is silent — the caller has no way to know whether the copy was complete or truncated without comparing lengths beforehand. With strscpy, truncation produces an explicit error that the caller must handle. This forces a conscious decision about what to do when a string is too long, rather than silently proceeding with a potentially invalid state.
The migration work started in earnest around 2019 when Kees Cook, who was leading kernel security hardening at Google, began coordinating a systematic effort. The approach was to first enable compiler warnings when strncpy is used, then work through those warnings subsystem by subsystem over successive kernel releases. This strategy — deprecation first, then incremental removal — is the only viable approach for a codebase that can’t freeze development for a single massive refactor.
The 360 patches aren’t just busywork. Each one represents a human reading the call site, understanding the context, choosing the right replacement, testing that the behavior is unchanged, writing a patch, getting review, and landing it through the appropriate subsystem tree. For some call sites, the right replacement was obvious. For others, it required reading driver documentation or hardware specifications to understand what the buffer actually contained and how it was used.
| Attribute | strncpy (old) | strscpy (replacement) |
|---|---|---|
| Null-terminates on truncation | No | Yes |
| Zero-pads remaining bytes | Yes | No |
| Returns on truncation | Original pointer (no signal) | -E2BIG (explicit error) |
| Returns on success | Original pointer | Length of copied string |
| Original design intent | Fixed-width filesystem fields | Bounded null-terminated copy |
| Memory writes when src < n | n bytes (pads with zeros) | src_len + 1 bytes (just terminator) |
The Security Framing Obscures the Real Problem
Every write-up about this event leads with buffer overflows and CVEs. And yes, strncpy misuse has contributed to real vulnerabilities. But the security framing undersells how serious the underlying issue is.
The core problem isn’t that strncpy is dangerous if misused. It’s that strncpy’s interface is designed to be misused. The function name suggests safety. The signature looks safe. The behavior in the common case — short source strings that fit in the destination — is safe. It’s only when you hit the edge case, which is precisely the case an attacker controls, that it fails silently in a way that looks like success.
This is a category of language API problem that C has in abundance: functions that are nominally “the safe version” of something dangerous, but which have subtle footguns in their specification that only manifest in attacker-controlled scenarios. Consider the pattern across C’s history:
gets()→fgets(): gets() had no length parameter at all; fgets() was the “safe” replacement, but it includes the newline in the result and doesn’t null-terminate if the buffer is exactly full. Removed from C11 entirely after decades.sprintf()→snprintf(): snprintf() was introduced to add length limiting, but the return value semantics (returns the number of bytes that would have been written, not the number actually written) are counterintuitive and lead to bugs where callers misinterpret a successful-looking return as proof of complete copy.atoi()→strtol(): atoi() has no error return — invalid input or overflow produces undefined behavior. strtol() is the “safe” version but requires understanding errno and endptr semantics that most programmers don’t internalize correctly.rand(): Not a string function, but the same pattern. Nominally usable, secretly terrible for any security purpose, standardized in C89, still called in security-relevant code in 2026.
The pattern is consistent: a safer alternative is introduced, but the semantics aren’t quite right, and decades later you’re still cleaning up the mess because neither function is removed from the language and programmers keep choosing the wrong one.
Rust’s Approach and Why It Matters for This Discussion
I need to address Rust here, because the strncpy story is frequently cited in the Rust advocacy community and for good reasons.
In Rust, the fundamental string types — &str and String — embed the length in the fat pointer representation. A &str is a (pointer, length) pair, always pointing to valid UTF-8, never relying on null termination. There’s no equivalent of “write up to N bytes of a null-terminated string into a buffer and check later whether it’s terminated.” The operation that strncpy makes easy — writing a potentially-unterminated string — is simply not an idiomatic Rust operation. You’d have to go through unsafe code to produce the equivalent of strncpy’s truncation bug.
This is the argument I find most compelling for Rust in systems contexts: it doesn’t just make dangerous operations harder, it restructures the type system so that the dangerous operation requires explicit opt-out from safety. The average programmer writing safe Rust cannot accidentally produce an unterminated string because the type system doesn’t have a representation for unterminated strings. The bug class doesn’t exist in the language.
I’m emphatically not arguing that the kernel should be rewritten in Rust — that’s a different discussion with genuine engineering tradeoffs around compilation times, FFI costs, existing developer expertise, and the challenge of expressing certain low-level patterns in Rust’s ownership model. The Rust-in-Linux project is proceeding thoughtfully, adding Rust as an option for new subsystems where it makes sense rather than mandating a mass rewrite.
But the conceptual argument is clean: strncpy took six years and 360 patches to remove from the Linux kernel. In a Rust codebase of equivalent scale, the bug class that strncpy represents doesn’t exist as a user of the standard string API. That’s the strongest possible version of the type-safety argument for new systems code.
What This Tells Us About Legacy Codebase Maintenance Economics
Let me be direct about the economics here, because I think they’re systematically underappreciated.
Six years and 360 patches consumed real engineering time from real engineers at real companies. The contributors to the strncpy removal effort include people paid by Google, Red Hat, Intel, ARM, and other organizations who have financial stakes in kernel security. If you estimate conservatively — say an average of two hours per patch including review time, at a blended engineering cost of $150/hour — the strncpy cleanup cost roughly $108,000 in direct engineering time. For one function. In one codebase.
That’s the bill for a function that’s been in C for roughly forty-five years, was misused for about thirty-five of those years once it entered widespread use, and whose removal from a single project required near-continuous effort by a community of experts for six years.
Now consider the average enterprise C codebase: fifteen to thirty years old, primary authors departed, documentation sparse to nonexistent, test coverage perhaps 40%, no dedicated security team, and a CI pipeline that’s been incrementally patched but not fundamentally redesigned since Jenkins was new. In that environment, doing what the kernel just did — systematically auditing and replacing every instance of a dangerous API — is essentially impossible without dedicated external resources.
You don’t have the headcount. You probably don’t have the institutional knowledge to reason confidently about what behavior each call site depends on. You can’t run 360 patches through your release process without affecting delivery timelines that somebody’s quarterly review depends on. The incentives are all wrong: the risk of a strncpy-based vulnerability is diffuse and probabilistic; the cost of the cleanup is immediate and certain.
So what actually happens in most organizations? The dangerous function stays. Maybe you add a linting rule that flags new uses. Maybe you write an internal wiki entry that says “prefer strscpy” (assuming you’ve even heard of strscpy). The existing call sites persist indefinitely, each one a low-probability but non-zero risk.
I’ve worked with four different companies over the past five years that had exactly this pattern with various deprecated-but-present APIs: strncpy and sprintf in C codebases, XML parsers with known XXE vulnerabilities in Java services, homegrown cryptographic functions with “do not use in production” comments that were clearly being used in production, Python 2 internal tools running under Python 2.7 because migrating them was “somebody else’s project.” In every case the pattern was the same: the problem was known, the risk was understood, the organizational path to fixing it didn’t exist.
The Broader Kernel Hardening Arc: What Preceded strncpy
The strncpy removal is the most recent major milestone in a decade-long kernel hardening effort coordinated largely by the Kernel Self Protection Project (KSPP). Understanding its context makes the achievement more legible.
Earlier milestones in the same effort include:
Stack canaries and KASLR (various kernel versions, 2010–2016): Compiler-inserted stack canary values that detect stack smashing before function return, and kernel address space layout randomization that makes exploitation harder by randomizing where kernel code and data live in memory. Neither eliminates bugs, but both raise the cost of exploitation significantly.
Variable-length array (VLA) removal (completed in Linux 5.0, 2019): VLAs in C allocate on the stack at runtime with no bounds checking. An attacker who controls the array size can cause a stack overflow. The kernel banned them and replaced every instance with explicit heap allocation with bounds checking. This effort is directly analogous to the strncpy removal: one dangerous language feature, systematically found and replaced across millions of lines of code.
Implicit fallthrough removal in switch statements (ongoing across GCC -Wimplicit-fallthrough): Implicit fallthrough from one switch case to the next is a C “feature” that causes subtle bugs when fall-through is unintentional. GCC added a warning, the kernel team enabled it, and the ongoing cleanup looks very similar to the strncpy effort in structure.
gets() and other C89 dangerous functions: The kernel had its own internal APIs with similar problems that were cleaned up in parallel with the C standard library removing functions like gets() in C11.
The strncpy removal is the most recent and largest-scale of these cleanup efforts, which is part of why it’s generating attention. But it’s in a lineage of similar work, and each previous effort built the organizational muscle and tooling for the next one.
Why “Just Use Static Analysis” Doesn’t Close This
The standard response to “C has dangerous APIs” is “use static analysis.” Enable clang-tidy, integrate CodeQL into CI, run the AddressSanitizer in your test suite. These tools help. I’m not dismissing them. But they don’t close the problem for several structural reasons.
False negatives are unavoidable in practice. Static analysis tools for C struggle with context-sensitive reasoning. Whether a particular strncpy call is safe depends on the relationship between the destination buffer size and the maximum possible input length — information that’s often determined at runtime through a chain of conditionals and data transformations that interprocedural analysis doesn’t fully model. Tools that over-approximate (flag everything suspicious) generate so much noise that engineers stop reading the reports. Tools that under-approximate miss the real bugs.
In practice, the false negative rate for strncpy-class bugs in large C codebases with commercial static analysis tools is not close to zero. A 2023 study of static analysis tools against a corpus of known CVEs found that even the best tools missed between 30-60% of known buffer handling vulnerabilities depending on how indirect the path from input to vulnerability was. That’s the known CVE corpus — vulnerabilities where we knew what to look for.
Analysis is not the same as enforcement. The kernel project’s approach — compiler warnings plus a multi-year human effort to fix each warning — worked because there was organizational will to treat warnings as mandatory. Most organizations treat new static analysis warnings as informational. They accumulate in dashboards. New code doesn’t introduce new warnings (or does, and the baseline is updated to suppress them). Nobody owns fixing the existing ones. Years later you have the same dangerous API, plus a dashboard showing 847 outstanding warnings that everyone has learned to ignore.
New code can always reintroduce old bugs. Even after you clean up every existing strncpy call site, a new developer — unfamiliar with the deprecation, working under deadline, following an old code sample — can introduce a new strncpy call tomorrow. Unless the API is actually removed or the compiler is configured to treat it as an error, you’re playing whack-a-mole indefinitely. The kernel’s actual removal of the function is the only approach that guarantees no new uses.
Dynamic analysis requires the right test inputs. AddressSanitizer (ASan) and Valgrind are excellent at catching actual memory errors when they occur, but they require that the code actually be executed with the triggering input. A strncpy call that only misbehaves when an attacker sends a 150-byte string to a field that expects at most 64 bytes will not be caught by ASan unless your test suite includes that specific case. Fuzzing helps close this gap significantly — libFuzzer and AFL++ are mature tools — but even comprehensive fuzzing of a large codebase with complex input parsing is a probabilistic defense, not a guarantee.
The epoll vs. io_uring Story Next Door
Another major kernel-related HN story today — “epoll vs. io_uring in Linux” at 159 points — is instructively adjacent. The author describes building a reverse proxy, first with a thread-per-connection model, then rewriting it around epoll, then rewriting again around io_uring. Each rewrite was prompted by hitting the architectural limits of the previous approach.
io_uring (introduced in Linux 5.1, May 2019) is another story about API debt, but a more constructive one — it’s an API that was designed from the start to replace something problematic, with explicit attention to the failure modes of its predecessor.
epoll is a notification API: it tells you when a file descriptor has I/O ready, but you still invoke the actual read() or write() system call yourself. That’s a minimum of two syscalls per I/O operation — one to learn that I/O is ready, one to actually do it — each of which involves a context switch from userspace to kernel mode. At high connection counts and high I/O rates, the syscall overhead becomes a meaningful fraction of CPU time.
io_uring is a completion API. Operations are submitted to a ring buffer in shared memory that both the application and the kernel can access. The kernel executes submitted operations asynchronously and posts results to a completion queue in the same shared memory. In the default mode you still call io_uring_enter() to notify the kernel, but a single call can submit and reap large batches of operations. In SQPOLL mode, a dedicated kernel thread polls the submission queue continuously, reducing latency to near-zero-syscall steady state.
The performance gains are real. Cloudflare has reported 15-40% throughput improvements in I/O-bound workloads. NGINX is adding io_uring support in newer releases. The ecosystem is moving, and the trajectory looks different from strncpy: io_uring is a new API replacing an old one that can remain, not a dangerous old API being removed. Developers can migrate at their own pace.
The security history of io_uring is worth noting though, because it illustrates that “new API designed to be better than the old one” doesn’t automatically mean safe. io_uring’s early releases had multiple serious kernel vulnerabilities — the interface exposes significant kernel internal state to userspace, and the attack surface is substantial. Google’s ChromeOS and Kubernetes-based container environments initially disabled io_uring entirely in 2022 due to exploit chains. The security record has improved substantially in Linux 6.x, but the early vulnerability record is a reminder that complex kernel-userspace interfaces need time and adversarial attention to harden.
What Should Change in API and Language Design
The strncpy removal story is ultimately a success story, but it’s a success that took far longer and cost far more than it should have. The lessons for future API and language design are concrete.
API design must account for deprecation from day one. C’s standard library was designed in an era when backward compatibility was treated as near-absolute and “just don’t misuse it” was considered acceptable documentation. Modern library design — in any language — should treat “how do we remove this when we get it wrong” as a first-class design consideration. Rust’s deprecation attributes and edition system, Go’s explicit compatibility guarantees with tooling-assisted migration, Python’s deprecation warnings infrastructure — these are all attempts to build the removal mechanism into the language ecosystem so that the cost of deprecation is lower.
The cost of technical debt cleanup should be legible. The six years and 360 patches for strncpy are quantifiable, but only in retrospect. Most organizations have no real visibility into what their accumulated API debt costs them in engineering time, vulnerability exposure, and incident response. Making those costs visible — treating deprecated API cleanup as a first-class work item with a real cost estimate, tracked alongside feature development — would change the calculus around when to invest in cleanup versus accumulating more debt.
Automated migration tooling should be a mandatory deliverable alongside deprecation. When a library maintainer deprecates a function, they should be expected to ship an automated migration tool — clang-tidy fixits, codemods, AST-aware sed scripts — not as a nice-to-have but as a precondition of the deprecation being accepted. The upfront cost of building the tool is amortized across every downstream user. The strncpy migration was largely manual because the semantic variations between call sites were significant. But many API deprecations have simpler, more mechanical replacements that could be automated if someone invested in the tooling.
Compiler strictness should be a configuration, not an afterthought. The kernel’s gradual move to treating deprecated API usage as errors in specific build configurations — rather than just warnings that can be suppressed — is the right model. For projects that can afford it, a “no deprecated APIs” compilation mode that CI enforces is more valuable than a static analysis report that accumulates in a dashboard. The cost is occasional friction; the benefit is a codebase where the dangerous old API is genuinely absent.
Predictions
I’ll be specific about where I think this goes over the next five years:
On C string APIs: Within two years, the other “deprecated but not removed” string functions with subtle semantics — notably strncat, which has its own distinct footgun (the n parameter is the maximum number of bytes appended, not the total buffer size, so a call like strncat(dest, src, sizeof(dest)) is usually wrong) — will face similar removal campaigns in major projects. The kernel’s strncpy work will be cited as evidence that large-scale C API retirement is feasible, and the KSPP will build on this success for the next target.
On Rust in the kernel: The Rust-in-Linux effort will explicitly cite the strncpy removal cost as a justification for writing new subsystems in Rust. Not as an argument against C (the vast majority of the kernel will remain C), but as a concrete answer to “why should new drivers be written in Rust” — because the demonstrated cost of maintaining correct string handling in C at kernel scale is quantifiable, and Rust’s type system eliminates that specific cost class for new code.
On the broader C ecosystem: The pattern of “known dangerous API, no organizational path to removal, lives indefinitely in legacy code” will persist in most production C codebases outside major open-source infrastructure projects. The kernel has the institutional resources and community to execute six-year cleanup efforts. The average embedded systems shop writing firmware in C for industrial equipment does not. Strncpy will still be present in significant volume in automotive, medical device, and industrial control system firmware in 2030 and beyond.
On tooling: AI-assisted static analysis will improve the detection of context-sensitive C vulnerabilities by 2027-2028, not because the underlying analysis algorithms change fundamentally, but because LLM-assisted tools are meaningfully better at resolving “what’s the maximum length of this value” questions by reading surrounding code and comments. This will improve detection rates but not eliminate the false negative problem entirely. The hard cases — where the length constraint is determined through network protocol state machines or user configuration — will remain difficult.
The kernel’s achievement here is worth genuine respect. Six years of sustained effort across a distributed community, driven by careful engineering judgment rather than mandate from above, successfully retiring a dangerous API from one of the most complex and widely-deployed software projects in existence. That’s not easy to do, and it required exactly the combination of factors — institutional backing, distributed expertise, patient coordination, and good tooling — that most software projects don’t have.
Which is exactly why we should be clear-eyed about what comes next. The strncpy hole is closing in the Linux kernel. In the rest of C’s ecosystem — embedded systems, legacy enterprise software, unmaintained open-source libraries with active security relevance — it’s going to be open for a long time. The lesson to take from the kernel’s six-year effort isn’t that this kind of cleanup is fast or cheap. It’s that it’s possible when the organizational conditions are right. Building those conditions — in your team, in your organization, in your project’s contributor community — is the actual work.




Discussion