RC RANDOM CHAOS

Kees Cook deleted strncpy from the Linux kernel

Linux removed strncpy after six years and 360+ patches. The mechanism: missing NUL termination, out-of-bounds reads, kernel info leaks, and why strscpy replaces it.

· 7 min read
Kees Cook deleted strncpy from the Linux kernel

strncpy is gone from the mainline Linux kernel. The last in-tree call site was converted during the 6.x series. The work ran six years, crossed more than 360 patches, and was pushed through the Kernel Self-Protection Project by Kees Cook. The function still ships in glibc. It no longer exists anywhere in kernel source. That distinction is the entire point.

strncpy was never the safe form of strcpy. The name implies a bounded copy. The behaviour is two separate hazards welded into one signature. Neither is obvious from the call site, and that is why the function survived for decades.

Hazard one - no guaranteed NUL termination. strncpy(dest, src, n) copies at most n bytes. When strlen(src) is greater than or equal to n, it writes exactly n bytes and stops. No terminator. dest is now a character array with no zero byte inside the bytes that were written. Hazard two - unconditional padding. When strlen(src) is less than n, strncpy fills the remainder of the destination with NUL bytes, every time, to the full length n. On a large fixed buffer that is a silent memset of dead bytes on every call. The padding is a performance tax. The missing terminator is the security bug.

This is CWE-170, improper null termination. The damage comes from the consumer, not the copy. Downstream code treats dest as a C string. strlen, snprintf, printk with %s, any memcpy whose length is computed from strlen - each walks memory until it finds a zero byte. When the terminator is missing, the first zero byte lives in whatever allocation sits next. The read runs past the end of the object. CWE-125, out-of-bounds read. In kernel context that read crosses into a neighbouring slab object or an adjacent stack frame.

The consequence chain has two ends. The first is disclosure. Over-read content gets copied somewhere reachable - copy_to_user, a sysfs attribute, a netlink reply, a debug log. Adjacent kernel memory reaches an unprivileged process. If that memory holds a pointer, the leak defeats KASLR and feeds straight into a privilege-escalation chain, T1068, exploitation for privilege escalation. The second end is corruption. An unterminated buffer later passed to a copy into a smaller fixed destination produces a length, from strlen, that is longer than the intended string. The copy overflows. CWE-787, out-of-bounds write - the classic route to slab corruption and a controlled overwrite.

The over-read mechanics depend on allocator layout. The SLUB allocator packs same-size objects into contiguous slabs. An unterminated string in one slab object means strlen walks forward into the next object in the same slab, into slab metadata, or into a freelist pointer. Embedded kernel pointers are the high-value leak. A single disclosed pointer collapses kernel ASLR for that allocation class and gives an attacker the base they need to aim a later write. The access is deterministic when the heap is groomed, and slab grooming is well understood. The read does not crash. It returns plausible bytes. That silence is what makes the info leak useful - it produces data, not an oops, and the path back to userspace carries it out cleanly.

The reason strncpy persisted is a misread of its contract. Developers reached for it as strcpy with a length cap, assuming the n bound implied a terminated result. It does not. The n bounds the write, not the string. A copy that exactly fills the buffer is the failure case, and it is also the case least likely to surface in a quick test with short inputs. The bug hides at the boundary, which is precisely where attacker-controlled length fields push it.

The cleanup was not a blind search and replace. Many strncpy call sites were correct by intent. They were padding fixed-width fields that are not C strings - struct members written to hardware registers, firmware command blocks, on-wire protocol fields with a fixed length and no terminator. Those fields must be zero-padded and must not stop at the first NUL. Swapping them for a terminating copy would break ABI and corrupt the wire format. So the deprecation needed more than one replacement, and every call site had to be read and classified. String, or byte field. That classification is why this took six years and not six weeks.

strscpy is the survivor. It always NUL-terminates the destination when the size is non-zero, and it returns the number of bytes copied, or -E2BIG when the source did not fit. Truncation stops being a silent unterminated buffer and becomes a return value the caller can check. strscpy_pad covers the cases that genuinely need the trailing zero fill. strtomem and strtomem_pad cover copies into fixed-size, deliberately unterminated members, where the destination is a byte array and never a string. strlcpy was deprecated in the same effort - it always terminates, but it reads the entire source to compute its return length even when it truncates, an over-read of the source buffer. Both legacy functions are gone. One safe primitive remains.

FORTIFY_SOURCE sits underneath all of this. With CONFIG_FORTIFY_SOURCE enabled, the fortified string.h uses __builtin_object_size to check copies where the destination size is known at compile time, and traps the violation at build or at runtime. Removing strncpy means the fortify layer no longer has to model its broken termination semantics as a special case. The hardening surface gets simpler as the dangerous API leaves.

The strncpy removal is one front in a longer hardening campaign. The same project drove the FORTIFY_SOURCE rework, the conversion of one-element and zero-length arrays to C99 flexible-array members, struct_group to make sub-struct memcpy bounds explicit, and the broader migration of the str* family onto checkable semantics. Each of these closes a category of silent out-of-bounds access the compiler could not see before. strncpy was among the most entrenched because its signature looks bounded and its danger is in the cases developers never tested - the exact-fit and the overflow, not the short string in the demo. Three hundred and sixty call sites across drivers, filesystems, networking, and architecture code each needed a human decision about intent.

None of this is academic. CWE-170 has hundreds of entries in NVD, and the dense concentration is in embedded and IoT firmware - fixed buffers, attacker-controlled length fields, parsers that assume input is terminated when it is not. Network daemons handling untrusted protocol data hit the same pattern. The kernel itself has shipped missing-termination over-reads across drivers, filesystems, and netlink paths that became info-leak CVEs. The bug class is consistent, repeatable, and old. The 360 patches are the measure of how widely a single unsafe primitive propagates once it is treated as standard.

Telemetry is where this separates from a normal CVE write-up. There is no EDR alert for strncpy. No Sysmon event ID. No SIEM correlation rule. A defender watching production sees nothing at the moment an unterminated buffer is created. The telemetry that catches this lives upstream of production. KASAN, the Kernel Address Sanitizer, reports the over-read as slab-out-of-bounds or stack-out-of-bounds, with the faulting address, the access size, and the allocation that was overrun. syzkaller drives the kernel into these paths and turns the KASAN splat into a reproducer. FORTIFY_SOURCE raises a runtime warning or a panic when a known bound is crossed. All of these are CI and fuzzing signals. None of them reach the SOC. That is the detection gap - a memory-safety regression in a driver is invisible to runtime telemetry until an exploit built on it does something two stages later that an EDR can finally see, by which point the info leak already happened.

The patch boundary is mainline. Removal landed incrementally across releases, and current mainline carries no in-tree caller. Residual exposure is everywhere the mainline tree does not reach - out-of-tree drivers, vendor and downstream kernels, long-lived stable backports, and the entire userspace ecosystem. glibc still exports strncpy. Millions of userspace call sites still carry both hazards, unchanged. The kernel cleaned its own source. The class is not closed.

One last point on what strscpy does and does not fix. It guarantees termination and surfaces truncation as -E2BIG. It does not decide whether truncation is acceptable. A truncated, correctly terminated string is safe memory holding wrong data, and a caller that ignores the return value has traded a memory-safety bug for a logic bug. The API removed the unterminated-buffer primitive from one codebase. It did not remove the obligation to check whether the copy fit. Six years bought a smaller class of bug, not the end of the class.

Share

Keep Reading

Stay in the loop

New writing delivered when it's ready. No schedule, no spam.