strncpy Is Finally Gone: What Linux 7.2-rc1 Means for Kernel Security
When Linus Torvalds opened the Linux 7.2 merge window and tagged 7.2-rc1 on 20 June 2026, the release notes carried the usual flood of driver updates, scheduler churn and filesystem fixes. Buried in that noise was a one-line cleanup that is, quietly, one of the most satisfying milestones in the kernel’s recent history: the strncpy() API is gone. Not deprecated, not discouraged, not guarded behind a warning — physically removed from the source tree, every call site converted, the function itself deleted along with its per-architecture assembly implementations.
It took six years, roughly 362 individual commits, and the hands of around 70 contributors to get there. That is an absurd amount of effort to delete one function. This post is about why that function deserved to die, why killing it was so much harder than it sounds, and why “removed” is a categorically stronger guarantee than “deprecated.”
The function that was wrong by design
strncpy() has been in C since the 1970s. It looks like a safe version of strcpy() because it takes a length argument, and a generation of programmers reached for it precisely because of that n. The cruel joke is that the n does not mean what almost everyone assumes it means.
Here is the standard library behaviour, stripped to its essence:
char *strncpy(char *dest, const char *src, size_t n);
Two behaviours make it dangerous, and they pull in opposite directions:
- It does not NUL-terminate on overflow. If
srcisnbytes or longer,strncpy()copies exactlynbytes and stops. It does not append a terminating\0. You are left holding acharbuffer that is not a valid C string. - It over-pads on underflow. If
srcis shorter thann,strncpy()doesn’t just stop after the string — it keeps writing\0bytes until it has writtennof them total. Copy a 4-byte name into a 4096-byte field and you’ve memset 4092 bytes for no reason.
The first behaviour is the security bug. Consider:
char name[16];
strncpy(name, user_supplied, sizeof(name));
printk("user: %s\n", name); // reads until it finds a NUL...
If user_supplied is 16 bytes or longer, name has no terminator. The %s in printk — or any strlen, any strcmp, any later copy — walks straight off the end of the 16-byte buffer and keeps reading adjacent kernel memory until it stumbles onto a stray \0. That’s a linear read overflow: an information leak in the best case, and a building block for something worse in the wrong hands. The buffer didn’t overflow on the write; the danger is latent, planted for whatever reads the buffer next.
This is the entire problem in one sentence: strncpy() produces a value that looks like a string but isn’t one, and nothing at the call site tells you which case you got.
strlcpy, strscpy, and a decade of false starts
The kernel’s answer evolved in stages, and each stage taught a lesson.
The first fix was strlcpy(), borrowed from the BSDs. It always NUL-terminates and returns the length it tried to copy, so you can detect truncation. Better — but it has its own footgun: to compute that return value it calls strlen() on the source, reading the entire source string even when the destination is tiny. If the source isn’t NUL-terminated, strlcpy() itself commits the linear read overflow you were trying to avoid. strlcpy() was itself removed from the kernel in January 2024 after a roughly four-year campaign — the warm-up act for this one.
The replacement the kernel actually standardised on is strscpy():
ssize_t strscpy(char *dest, const char *src, size_t count);
It always NUL-terminates, it reads only as many bytes as it copies (closing the source-side overread for good), and it returns the number of bytes copied — or -E2BIG when it had to truncate. That negative-on-truncation return is the key design move: truncation becomes a value you can branch on, not a silent corruption.
strscpy() wasn’t the whole story, though, because strncpy() was doing several different jobs across the tree and a single replacement can’t capture all of them. Part of what made the cleanup slow was that every call site had to be read and understood before it could be converted. The modern toolkit that replaced one ambiguous function is a set of explicitly-named ones:
| Old intent | Modern replacement |
|---|---|
| Copy into a NUL-terminated string | strscpy() |
| NUL-terminated and zero-padded to width | strscpy_pad() |
| Fixed-width field that is deliberately not NUL-terminated | strtomem_pad() |
| Copy with explicit padding semantics | memcpy_and_pad() |
| Raw bytes, you already know the length | memcpy() |
Look at what that table does. The single biggest improvement isn’t any one function — it’s that the name now states the intent. A reviewer seeing strtomem_pad() knows the destination is a fixed-width non-terminated field on purpose. A reviewer seeing strncpy() knew nothing; they had to go measure the destination buffer and reason about every possible source length to figure out which of the two failure modes applied. Ambiguity at the call site was the root cause, and naming the intent is the actual fix.
Why deleting one function takes six years
If the replacements existed years ago, why did this drag on across six years and hundreds of patches? Three reasons, and they’re the interesting part.
There were a lot of call sites, and they were everywhere. strncpy() was scattered across core kernel, thousands of drivers, filesystems, networking, architecture-specific code, and staging. You cannot land that as one mega-patch: a 360-file diff is unreviewable, unbisectable, and will conflict with everything in flight. It has to be done subsystem by subsystem, each change routed through the maintainer who actually understands that code, each one reviewed on its own merits.
Every conversion is a judgement call, not a sed script. This is the crux. You cannot mechanically rewrite strncpy(a, b, n) into strscpy(a, b, n), because the two functions behave differently and the old code may have depended on the difference. Did this call site rely on the zero-padding? Then it needs strscpy_pad(), not strscpy(). Was the destination intentionally not NUL-terminated — a fixed-width field written to hardware or an on-disk structure? Then strscpy() would wrongly truncate the last byte to make room for a terminator, and you’ve introduced a bug. Did the old code use strncpy()’s return value (a pointer to dest)? strscpy() returns a count, so that has to change too. Each of the 362 commits represents a human reading the surrounding code and deciding which of five replacements preserves the intended behaviour.
The long tail is the hardest part. The first 90% of conversions are the easy, obvious ones. What’s left at the end are the genuinely weird call sites — the ones where the original author was relying on some subtle strncpy() quirk, or where the “right” answer required restructuring the surrounding code. Kees Cook, who leads the Kernel Self-Protection Project and flagged strncpy() as the next target right after strlcpy() fell, reportedly handled the final half-dozen instances personally. The tail is where the cleanup stops being mechanical and starts being archaeology.
Deprecation is a suggestion; removal is a constraint
Here’s the part that makes 7.2-rc1 worth writing about rather than shrugging at.
For years, strncpy() was deprecated. The kernel’s own Documentation/process/deprecated.rst told you not to use it. CONFIG_FORTIFY_SOURCE added compiler-assisted bounds checking around the str/mem family. Static analysers flagged it. And yet new uses kept appearing, because deprecation is fundamentally a social mechanism. It relies on every contributor knowing the guidance, every reviewer catching violations, and everyone choosing to comply. A developer copy-pasting a 2015 driver as a template reintroduces the function, and unless a reviewer notices, it lands.
Removal changes the category of the guarantee. With the function deleted from the tree, any patch that tries to use strncpy() does not compile. The symbol is gone. There’s no reviewer to slip past, no documentation to be unaware of, no judgement call — the build fails, full stop. You have converted a norm that people should follow into an invariant the toolchain enforces.
That’s the real headline. The kernel didn’t just clean up some code; it eliminated an entire bug class by construction. Unterminated-buffer overreads caused by strncpy() are not “rare now” or “discouraged” — they are impossible to write, because the API that produced them no longer exists. That’s the difference between hoping a mistake won’t happen and making it unrepresentable. It’s the same philosophy that drives the kernel’s gradual adoption of Rust and its long march of -Wflex-array-member-not-at-end and counted-by annotations: push correctness from review-time into compile-time, where it can’t be forgotten.
What this means if you write or build kernel code
If you maintain out-of-tree modules or carry downstream patches, the practical takeaways are short:
strncpy()is not available in 7.2. Code that used it will fail to build against the new tree. Convert it.- Don’t reach for the literal translation. Before swapping in
strscpy(), ask what the old code actually wanted. NUL-terminated?strscpy(). Terminated and zero-filled to width?strscpy_pad(). A fixed-width field that should not be terminated?strtomem_pad(). Check whether anything consumed the old return value. - Let truncation be loud. The whole point of
strscpy()’s-E2BIGreturn is that you can detect a too-small destination. Handle it instead of silently producing a wrong-but-terminated string. - Lean on
FORTIFY_SOURCE. It’s still your friend for the rest of the str/mem family. Removal killedstrncpy(); the compiler-side guards still catch a lot of the rest.
Linux 7.2 mainline is expected around the end of August 2026, and it’s already lined up as the target kernel for distributions shipping later in the year. When it lands, it will be the first mainline release in the kernel’s history that contains exactly zero uses of a function that shipped in C for nearly four decades.
Six years and 362 commits to delete one function sounds like a poor return on effort. It isn’t. The kernel didn’t spend that time deleting code — it spent it reading thousands of call sites, understanding what each one meant, and replacing ambiguity with intent. The deletion was just the last commit. The guarantee it bought — this bug class can no longer be written — is permanent, and it’s exactly the kind of unglamorous, high-leverage work that keeps a 30-million-line codebase alive.