Vivian Voss

Ubuntu Replaced Its coreutils

unix universe freebsd ubuntu coreutils

On 14 September 2026 a maintainer of GNU coreutils opened a bug against Ubuntu 26.10. He had built 32,768 directories inside one another, typed rm -rf a, and received Segmentation fault (core dumped). The directory was still there afterwards. His report quoted POSIX, which requires that rm "shall be able to descend to arbitrary depths in a file hierarchy, and shall not fail due to path length limitations", and it ended on a question that is hard to improve upon: "It is a bit premature to switch, when 'rm' can't handle all possible directory entries, no?" The reply came the same day: thirty thousand subdirectories were not expected to be common, and a system utility was expected to survive them all the same; it pointed to an upstream issue describing the same failure, open since January 2022, and a fix reached the archive on 25 September. Three weeks after that, with "the full adoption of Rust-based coreutils" in the words of the announcement, Ubuntu 26.10 was released.

That is the small version of the story. The larger one has an audit in it, three commands held out of an LTS, a fallback package, and a few thousand shell scripts whose authors have not been told that anything moved.

Why there was something to replace

The GNU project was announced in September 1983 to write a Unix that people were allowed to copy, and it began where every Unix begins, with the small commands. ls, cp, rm and their relations were written afresh because the originals belonged to AT&T, and when a kernel arrived from Finland in 1991 the tools were already waiting for it. That is the origin of the slash in GNU/Linux, and of an arrangement most people have stopped noticing: the kernel is one project, the commands a shell script calls are another, and the distribution chooses which copy of the second to ship. Alpine ships BusyBox and Debian ships GNU. Since Thursday, Ubuntu ships uutils.

Who owns the commands your scripts call Ubuntu 26.10 FreeBSD 15.1 your shell scripts call ls, cp, mv, rm by name coreutils: a second project GNU 9.10 swapped for uutils 0.12 Alpine takes BusyBox instead the distribution chooses the copy Linux kernel 7.3 a third project, kernel.org three layers, three owners your shell scripts call ls, cp, mv, rm by name one source tree bin/ 42 programs, usr.bin/ 273 cp.c 1988, ls.c 1989, rm.c 1990 sys/ the kernel built, tested and released together one version number, one day two layers, one owner

The reason was given on 12 March 2025 by Jon Seager of Canonical, under the title "Carefully But Purposefully Oxidising Ubuntu": "it's the enhanced resilience and safety that is more easily achieved with Rust ports that are most attractive to me". The plan made uutils the default in 25.10, and then in the 26.04 LTS "if the conditions are right". 25.10 did it in October 2025. For the LTS the conditions were right for most of the hundred-odd commands and wrong for three: cp, mv and rm shipped from GNU, because the audit had found eight time-of-check-to-time-of-use flaws in them that were still open when the LTS went out. An operating system that holds back its rm because it is not yet safe enough to delete things is, if nothing else, admirably candid about the state of the work.

For anyone who would rather wait, the way back is one line:

apt install --allow-remove-essential coreutils-from-gnu coreutils-from-uutils-

The flag deserves a second look. Removing an essential package needs explicit consent, and the essential package is now the Rust one.

Nineteen months from announcement to full adoption 12 Mar 2025 Seager's post "resilience and safety" Dec 2025 to Mar 2026 Zellic: 113 issues, 44 CVEs two audit rounds 6 and 30 Jun 2026 cp breaks the builds packages, then images 15 Oct 2026 26.10 released "full adoption" 9 Oct 2025 25.10 released uutils becomes the default 23 Apr 2026 26.04 LTS: cp, mv, rm stay GNU eight TOCTOU issues open 14 Sep 2026 rm -rf segfaults fixed 25 Sep Sources: jnsgr.uk, Canonical's update of 22 April 2026, Launchpad #2155763, #2158691, #2167206, the 26.10 announcement

What the audit found

Canonical engaged Zellic for two rounds, December 2025 to January 2026 and February to March 2026, starting with the utilities that touch permissions and files. 113 issues across both rounds, 44 with CVE numbers, 30 pull requests sent upstream, and Canonical's own update in April said the internal review had surfaced "serious concerns" before the auditors arrived. A GNU coreutils maintainer, writing on oss-security in May, added that the affected-version fields in the CVE records were not accurate, which is the kind of detail one notices when the tools being replaced are one's own.

The class most of the file-handling findings belong to deserves a paragraph, because it decides what memory safety does and does not buy. A time-of-check-to-time-of-use flaw works like this. The tool looks at a path, decides the path is a plain file in the right directory, and then acts on the path. Between the looking and the acting, another process replaces a directory on the way with a symbolic link, and the tool, still holding its earlier opinion, deletes or overwrites whatever the link now points to. The mkfifo CVE from the audit, CVE-2026-35352, is this shape: the fifo was created with world-readable and world-writable permissions and tightened afterwards with a chmod() on the path, and in the gap between the two calls another user can swap the fifo for a symbolic link, so that the chmod() lands on whatever the link points to. Every line of it is sound Rust, and the permissions still end up on the wrong file. The borrow checker never sees any of this, because a symlink is the kernel's business and the compiler never meets one. GNU's mkfifo, for comparison, has handed the mode to mknodat() at creation for years, which leaves no gap to stand in.

One call or two: the mkfifo race uutils mkfifo -m 700 mknodat(path, S_IFIFO|0666) fifo exists world-writable the gap rm fifo; ln -s /etc/target path by any other user chmod(path, 0700) follows the link lands on /etc/target GNU mkfifo -m 700 mknodat(path, S_IFIFO|0700) mode set at creation fchmodat2(path, 0700, AT_SYMLINK_NOFOLLOW) no gap to stand in Both traces: Collin Funk, oss-security, 1 May 2026, uutils 0.8.0 against GNU 9.7 on Ubuntu 26.04 Memory safety has no opinion on either lane; the difference is in the design of the calls.

The same four files since 1994

Open bin/cp/cp.c in FreeBSD 15.1 and the fourth line reads Copyright (c) 1988, 1993, 1994 The Regents of the University of California. ls.c and mv.c say 1989, rm.c says 1990. They came to FreeBSD with 4.4BSD-Lite in 1994 and have been in the same tree as the kernel ever since: 42 programs under bin/ and 273 under usr.bin/, built by the same make buildworld that builds the kernel and released on the same day under the same version number. 105 of those directories carry a tests/ subdirectory in the tree; the one for ls runs to 1,027 lines and 38 test cases, and the one for cp has 41. When 15.1 ships, the rm on it is the rm the release was tested with, and a fix between releases arrives as a patch to the file that has carried that copyright line for thirty-two years.

The cp in that tree has, so to speak, read the paragraph above. Its recursive mode sets a flag called beneath, and with it every file the copy creates is opened with openat() relative to a handle on the destination directory, with O_RESOLVE_BENEATH set, which asks the kernel to refuse any path that would resolve to somewhere outside that directory. A symlink planted in the destination halfway through a copy earns an error, and the file it pointed at is left alone.

There is a second answer to the question of what happens when a tool is wrong, and it is built on the assumption that one day it will be. 46 of the programs in bin/ and usr.bin/ enter Capsicum capability mode once they have opened what they need, cat, echo, head, tail, tee, tr, uniq, wc, dd, sleep, diff and hexdump among them. From that moment the process cannot open a new file or a socket by name, in any language. Should a bug ever turn up in cat on FreeBSD, it gets cat and nothing else; the process cannot be talked into opening /etc/master.passwd, because it gave up the right to open anything at all before it read its first byte.

Thirty-two thousand directories

I repeated the September experiment on 19 October 2026, in a jail running FreeBSD 15.0-RELEASE. 32,801 directories, each inside the last (the loop built them in chunks of 400 with mkdir -p and did not divide evenly; I kept the number). Then:

# rm -rf /tmp/deep ; echo $?
0
# ls -d /tmp/deep
ls: /tmp/deep: No such file or directory

The rm is 13,112 bytes, walks down with fts(3) and climbs back with rmdir(), and its largest resident size was 15 MB. GNU rm 9.9, installed from the ports collection for the comparison, cleared the same tree with the same exit status. Both have done this since before the uutils issue was opened, and FreeBSD's since 4.4BSD.

The advisory page tells the rest. FreeBSD has published 728 security advisories from 1997 to this month. 38 of them carry the name of a program from bin/ or usr.bin/, and the names are instructive: bzip2, gzip, xz, file, telnet, ftp, ncurses and cpio, mostly code that FreeBSD imports from other projects and tracks. sort appears once, in 2001. ls, cp, mv, rm, cat, chmod and mkdir do not appear at all, in twenty-nine years.

728 FreeBSD advisories, 1997 to October 2026 690 kernel, libraries, daemons, the rest of the system 38 bin, usr.bin The 38, by name bzip2, gzip, xz, file, telnet, ftp, fetch, ncurses, cpio, ktrace, iconv, tty, top, man, ee, compress, chpass, random mostly code imported from other projects and tracked; sort once (2001), realpath once (2003), chflags once (1999) ls, cp, mv, rm, cat, chmod, mkdir: 0 advisories in twenty-nine years against 44 CVEs from one audit of the rewrite, own count on the advisory page, 19 October 2026

The strongest objection

A Linux administrator will say that all of this is C, and that C is the problem the rewrite exists to solve: GNU's own sort and uniq collected CVEs this year, CVE-2025-5278 and CVE-2026-56391, for reads past the end of a buffer, in the very tools uutils replaces, and memory safety removes that whole class at compile time. It is the strongest card in the hand, and it is worth seeing exactly what it covers.

It covers one class, and the tools it indicts are GNU's. The audit found 113 issues in code that had already paid for memory safety, and the ones that held three commands out of an LTS and made the news in September were of other kinds: races on paths, options parsed in the wrong order, a cp --parents that panicked on a missing value, a traversal that recursed until the stack ran out. Those are settled at the design table, and the design FreeBSD's tools inherit is the one the *at calls and Capsicum were made for: act on handles, and give up rights early. The four files under discussion have had thirty-two years in production and 728 advisories in which to be named, and were named in none. A rewrite can adopt the design, and uutils has begun to, which is how its rm learned to survive a deep tree in September. FreeBSD did not have to learn it in production.

The same four commands, two ways of keeping them row FreeBSD 15.1 Ubuntu 26.10 origin one tree with the kernel since 1994 second project, GNU then uutils tests 105 test dirs in tree; ls 38, cp 41 GNU suite: 653 pass, 21 fail of 692 deep tree rm cleared 32,801 levels, exit 0 rm segfaulted at 32,768 until 25 Sep path races cp: openat, O_RESOLVE_BENEATH 8 TOCTOU open in cp, mv, rm at LTS sandbox 46 tools in Capsicum; not ls, cp, mv, rm none in the coreutils advisories 0 for ls, cp, mv, rm in 728 44 CVEs from one audit Own counts on releng/15.1 and the advisory page, 19 October 2026; Canonical's update of 22 April 2026; uutils 0.12.0 release notes

The limit

The four files are C. They were written when C was the only language a Unix tool could be written in, and they have stayed C, like the kernel beside them; their protection lies in design, in fts(3) and directory handles, and in the record above. Capsicum fences 46 of the base tools, the ones that read untrusted input once they have opened it, and ls, cp, mv and rm stay outside it by the nature of their job, which is to open files by name until they exit.

FreeBSD's tools are not GNU's, and never were. ls has 44 switches on FreeBSD and 60 on GNU, with the 26 of POSIX on both, so a script written for GNU's extras wants a glance before it moves; once moved, it stays moved, because these tools do not change under it.

No times are given. The measurement tested whether rm comes back, and both did; it is not a benchmark.

The licence changed too. uutils is MIT-licensed and GNU coreutils is GPL, so Ubuntu's essential set has moved from one licence family to another. This piece notes it and scores neither.

The bill

Today's default on a Linux fleet: the commands every shell script calls belong to a project that is not the kernel's, chosen by the distribution, and the distribution can change its mind inside a support window. Ubuntu has just finished doing exactly that.

What it cost, in the order it was paid: an external audit over four months, 113 issues, 44 CVEs; three commands held out of an LTS; a cp that broke the package builds on 6 June and the image builds on 30 June, a kerfuffle fixed by cherry-pick within a fortnight or so each time; an rm that segfaulted on a deep tree in September; a package called coreutils-from-gnu for anybody who would rather have last year's tools back; and a fleet of scripts that will learn from an error message which cp they have been talking to.

On FreeBSD it costs what it has cost since 1994: one tree, one build, one release, and the rm that was tested beside the kernel it ships with.

FreeBSD has never needed a new rm.