Linux 7.2 quietly deleted a function that has been in C since before most working engineers were born. As Phoronix reported, the last users of strncpy are gone from the kernel, the per-architecture implementations are removed, and the whole effort took roughly 362 commits across six years. The merge landed on a Friday with the understatement these things usually get.

Most people will never notice. They should, because the story of why strncpy had to die is a clinic in API design, and almost every lesson in it applies to code you are shipping right now.

Two sins, one root cause

strncpy(dst, src, n) does two things that surprise the people who reach for it.

First, it does not guarantee NUL-termination. If the source is at least n bytes long, the copy stops and the destination is left without a terminator. The next function that treats dst as a C string walks off the end. This is the bug class that has fed buffer-overread CVEs for decades.

Second, when the source is shorter than n, it pads the rest of the destination with zeros, all the way to n. Copying a four-byte string into a 4096-byte buffer writes four bytes you wanted and 4092 bytes you did not. The cost is proportional to the buffer, not the data, every single time. Phoronix's framing was blunt and correct: counter-intuitive termination behavior plus redundant zero-filling made it both a persistent source of bugs and a performance drag.

Here is the part worth sitting with. Neither of those is really a bug. They are the documented behavior. strncpy was built for fixed-width fields, the kind of thing you found in ancient Unix directory entries where a name occupied exactly fourteen bytes and was not expected to be terminated if it filled the field. For that job, no terminator and full zero padding are correct.

That job stopped mattering a long time ago. The function survived anyway, advertising a different job through its name.

The "n" was always a promise it couldn't keep

Look at the family. strcpy is the unbounded one everyone knows is dangerous. strncpy sits right next to it with an n in the middle, and the entire industry read that n as "this is the bounded, safe one." It is not. It is the fixed-width-field one that happens to take a length.

When correct use of a function requires you to remember a footnote, the interface has already failed. The name is the most-read piece of documentation any API has, and strncpy spends that budget telling you something untrue. You can write a perfect man page underneath it. People read the name, pattern-match to strcpy, assume safety, and ship the overread.

This is the lesson I would attach to everything you build. Name a function for the contract it actually honors, not for its resemblance to a sibling. A good name closes a bug class. A misleading one opens one and keeps it open for thirty years.

The fix was decomposition, not a warning label

The kernel did not solve this by adding a doc comment that said be careful. Documentation does not survive contact with thousands of contributors. It split one overloaded primitive into two honest ones.

strscpy is the safe bounded copy people always thought they were getting. It always terminates as long as the buffer is non-zero, it never pads, and it tells you the truth about what happened:

/* strncpy: returns dst. Truncation? Silent. Termination? Maybe. */
char *strncpy(char *dest, const char *src, size_t n);

/* strscpy: returns bytes copied, or -E2BIG on truncation.
   Always NUL-terminates. Never zero-pads. */
ssize_t strscpy(char *dest, const char *src, size_t count);

That return value is the second quiet win. strncpy hands you back the destination pointer, which tells you nothing. You cannot ask it whether the data fit. Every caller who cared about truncation had to re-derive the answer by hand, and most either got it wrong or never asked. strscpy returns the copied count or -E2BIG, so the one error condition you actually care about is sitting right there in the return value, impossible to ignore by accident.

And for the rare code that genuinely needs the old fixed-width padding, there is strscpy_pad. The legitimate use case did not get abandoned. It got its own function with a name that says what it does. That is the whole move: make the safe thing the default, and make the unusual thing explicit and separately named. The footgun disappears by construction instead of by vigilance.

Why it took six years

You cannot #define a load-bearing function out of existence. Six years and 362 commits is not bureaucratic drag, it is the honest cost of removing something foundational from a codebase with hundreds of maintainers and ownership boundaries you have to respect.

The reason you cannot automate it is that every call site is an audit, not a substitution. You cannot blindly run s/strncpy/strscpy/ because for each one you have to answer a real question. Did this code rely on the zero padding? Did it depend, correctly or not, on the missing terminator? Was it already subtly broken in a way nobody had hit yet? The migration is not just cleanup. It is a defect-discovery pass disguised as cleanup, and a lot of latent bugs only surface because someone finally had to read the line closely to convert it.

This is the part that maps straight onto how I think about agent pipelines and large refactors generally. A sweep that requires an explicit rationale at each site is a different artifact than a sweep that does not. The first one produces understanding and a record of the genuinely-hard cases. The second produces a diff and a false sense of safety. The discipline of "say why, per site" is what turns a mechanical pass into an audit, whether the thing doing the pass is a contributor or an agent.

When the reader is an agent

I walked into a version of this trap on a relationship platform we run, and the coding agents in our pipeline made it worse in a way I did not see coming.

We had modeled the same underlying idea a few times over, an entity that represents a person's partner in a relationship. The successive versions ended up with similar names but genuinely different persistence and behavior. As the model evolved we layered the new shapes on top without tearing out the old ones, and we did the responsible-looking thing. We left comments on the legacy models asking that new code not be written against them.

It did not hold. Our implementing agents kept getting pulled toward the deprecated models, and they kept getting pulled there for a reason that is almost funny in hindsight. The old models had the early, clean, elegant names. The replacements were verbose precisely because they had to differentiate themselves from the originals that already owned the good names. So every session, an agent would survey the code, find the prettier abstraction, and reach for it. The review layers usually caught the invalid new uses, but usually is not always, and either way we were paying for it. Every run burned tokens relearning a lesson the codebase refused to actually enforce, and every reviewer, human or agent, spent attention re-flagging the same mistake.

That is the part worth underlining. Deprecation-in-place assumes a reader who remembers the warning. A human team mostly does. An agent shows up cold each session and re-derives its priors from the strongest signals in the code, and "this is the clean, established name" is a very strong signal. A do not use comment is a weak probabilistic nudge pointed straight into that prior. Worse, the verbose naming we used to mark the replacements made the deprecated path the more attractive one. We had inverted our own incentives. The tools that work tolerably for human readers, annotate it, rename around it, tolerate it through a slow migration, were ineffective for the agents and in the naming case actively made things worse.

So we made the decision that felt dangerous. We removed the deprecated codepath, migrated its persistence, and stopped tolerating it in the tree at all. It took several high-attention pull requests with a large blast radius, the kind you schedule and watch closely. And it killed an entire class of agent confusion in one move.

The cost structure is the real lesson. For a human team, tolerating a deprecated path through a long migration is roughly a one-time cost you amortize. For an agent codebase it is a per-session tax, paid in tokens and review attention, every run, until the path is gone. The kernel could afford to let strncpy linger for six years because its readers could hold "deprecated, don't touch" in their heads across the whole window. When your readers are agents, that luxury evaporates. The deletion stops being the reward at the end of the migration and becomes closer to the only intervention that works. The large-blast-radius PR you are avoiding is a bounded, one-time cost. The confusion you are tolerating is unbounded and recurring.

The shape of removing anything that matters

Strip the C specifics away and you are left with a repeatable pattern for retiring foundational code.

Ship the replacement first, and make it strictly better, so adoption is a no-brainer rather than a favor. Make the safe path the default and give the rare legitimate need its own honest name. Convert incrementally, per owner, treating each site as a small audit. Hand-handle the cases that are actually different instead of forcing them through the common path. Delete the symbol last, only after the final user is gone. The deletion is the reward, not the work. And the more your readers are machines rather than people, the earlier in that sequence the deletion has to move, because a machine does not accrue the institutional memory that makes a slow migration tolerable.

There is one more reason to care, and it is the contrarian one. strncpy was a mistake made at the very bottom of the stack, and the bottom of the stack is where mistakes compound. It propagated into every libc, every project that mirrored one, every codebase that learned C from the one before it. The kernel can fix its own copy. The rest of the world is still living with the original.

So when you are designing the primitive that everything else will sit on, design it like every layer above you will inherit the contract exactly as written, caveats and all. Because they will, and they will inherit the lie in the name right along with it.

Sources

  1. Michael Larabel, “Linux 7.2 Drops strncpy() With Its Final Users Removed,” Phoronix — the report this piece draws on.
  2. Kees Cook, announcement of the final removal (Mastodon) — primary source for the headline numbers: 362 commits by 70 contributors over six years.
  3. kernel.git removal log — the actual commits behind the deletion.
  4. Kernel Self-Protection Project, issue #90, “Remove all strncpy() uses” (2020) — the tracking issue that drove the six-year campaign; frames strncpy as actively dangerous for its non-termination behavior.
  5. Linux kernel documentation, Deprecated Interfaces, Language Features, Attributes, and Conventions — the “actively dangerous” label and the full replacement decision tree (strscpy, strscpy_pad, strtomem_pad, memcpy_and_pad, memcpy).

Want to talk shop on API design, large refactors, or building software with AI agents in the loop? Get in touch.