News8 days ago

Ubuntu 26.10 Finished Moving rm to Rust, and We Segfaulted It at 17,000 Directories

Ubuntu 26.10 moves the last three GNU holdouts to Rust coreutils 0.10.0. We pulled the image and had rm -rf dying with SIGSEGV in under four minutes, at a threshold between 16,000 and 17,000 nested directories.

The WJS Desk

Sep 16, 2026 · 6 min read

Photo by Castorly Stock on Pexels

Ubuntu 26.10 ships on 15 October, and its release notes carry one sentence that has been a long time coming: "The default core utilities now run entirely on the Rust-based uutils implementation. The remaining GNU utilities (cp, mv, and rm), previously retained due to compatibility issues, have now been migrated."

Those three were held back for a reason. So we pulled the ubuntu:26.10 image, checked what it is actually running, and had rm -rf dying with SIGSEGV inside four minutes. It reproduces every time, the threshold sits between 16,000 and 17,000 directories deep, and the upstream fix was still an open pull request when we wrote this.

What is actually in the image

Most coverage of this has been vague about what "transition" means, so here is the package state from a clean pull of ubuntu:26.10, which identifies itself as Stonking Stingray, development branch:

ii  coreutils              9.5-1ubuntu2+0.0.0~ubuntu29   coreutils meta package
ii  coreutils-from-uutils  0.0.0~ubuntu29                coreutils from the uutils project
ii  gnu-coreutils          9.10-1ubuntu2                 GNU core utilities
ii  rust-coreutils         0.10.0-1ubuntu1               Universal coreutils utils, written in Rust

Two things stand out. The first is that /usr/bin/rm is a symlink into /usr/lib/cargo/bin/coreutils/, a single multi-call binary exposing 117 entry points. The second is that GNU coreutils 9.10 is still installed, as 105 binaries under a gnu prefix: gnurm, gnucp, gnudd, gnumkdir. Canonical did not remove GNU. It demoted it and kept it on the disk, which turns out to matter a lot in the next section.

The version number matters too. Ubuntu is shipping uutils 0.10.0, released 5 August. Upstream shipped 0.11.0 on 31 August, three weeks before we ran these tests.

We segfaulted rm, then went looking for the edge

The claim came from Hacker News user collinfunk in the 250-comment thread on the release, who posted a four-line reproduction against the same image. We ran it. Then we did the part nobody had done, which is find out where the line actually is.

Building the tree needs GNU mkdir, and that is its own small finding: uutils mkdir -p gave up at 2,048 directories with File name too long, while gnumkdir -p on the same path walked all the way to 32,768. GNU chdirs down the tree as it goes. uutils, at 0.10.0, does not.

Nesting depthuutils rm 0.10.0GNU rm 9.10
8,000exit 0exit 0
16,000exit 0exit 0
17,000exit 139, SIGSEGVexit 0
20,000exit 139, SIGSEGVexit 0
32,768exit 139, SIGSEGVexit 0

Measured in the official ubuntu:26.10 arm64 image under Docker 28.5.1 on an Apple M4 Pro, 12 cores and 8 GB visible to the VM. That is a virtualised arm64 kernel rather than bare metal, so treat the exact crossover as indicative. The behaviour either side of it was not marginal: every run at 16,000 and below exited 0, every run at 17,000 and above dumped core.

The part that should worry you: after the segfault, the directory is still there. rm -rf reported failure by dying, which means any script that runs rm -rf without checking the exit code now silently leaves the tree in place and carries on. A cleanup step that no longer cleans up is a worse failure than a crash.

Why it happens, and why the fix is not in the release

Sylvestre opened pull request 14554 against uutils on 14 September, the day before we ran this. The description names the cause directly: safe_remove_dir_recursive_impl recursed once per directory level, so rm -rf on a hierarchy tens of thousands of levels deep overflowed the stack and died with SIGSEGV.

The fix replaces recursion with an explicit stack, holds only the deepest 16 directory descriptors open at a time, reopens parents by walking .., and verifies each one by comparing recorded device and inode numbers so the traversal cannot be redirected mid-delete. The PR's own benchmark reports a 24.39% improvement on rm_recursive_tree as a side effect.

It is a good fix. It was also still open, with review comments outstanding, one month before the release that needs it.

That gap is the actual story. Ubuntu is on 0.10.0, upstream is on 0.11.0, and the patch for this is in neither.

The dd number everyone is quoting, and what we measured

The other complaint in that thread came from linsomniac, who reported that on Ubuntu 26.04 with uutils 0.8, a Ganeti disk image pipeline using dd bs=1M fell from roughly 350 MB/s to 30 MB/s, with dd pinned on CPU. We wanted to know whether 0.10.0 still does that, so we measured it three ways with 4 GB per run.

Workloaduutils dd 0.10.0GNU dd 9.10
/dev/zero to /dev/null, bs=1M29 GB/s28 GB/s
Reading from a pipe, bs=32k1.9 GB/s1.9 GB/s
Reading from a pipe, bs=1M251 MB/s1.9 GB/s

So the regression is real, it survives into 0.10.0, and it is narrower than the thread suggests. Copying from a device or a regular file, uutils dd matched GNU within noise. The 7.5x gap only appears when the input is a pipe and the block size is large, which is exactly the partial-buffer case the original report described. Drop to bs=32k and the difference vanishes entirely.

That is worth knowing in both directions. If you pipe disk images through dd bs=1M, this will hurt you. If you do almost anything else with dd, it will not, and the version of this story going around ignores that.

GNU is still on the disk, and that is the real headline

Because gnu-coreutils ships alongside, reverting is one command. We tested it rather than assuming:

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

It completed cleanly through a set of dpkg diversions managed by a coreutils-switch package, after which rm --version reported GNU coreutils 9.10 and the 32,768-deep delete that had been segfaulting exited 0. If you want it to survive an upgrade, pin it:

printf 'Package: coreutils-from-uutils\nPin: release a=*\nPin-Priority: -10\n' \
  | sudo tee /etc/apt/preferences.d/uutils

Confirmed: the escape hatch works, and it is a supported package operation rather than a hack. One commenter noted that on 26.04, build-essential depends on coreutils-from-uutils, so the swap can block upgrades there. We did not hit that on 26.10.

What the threads got right, and what they got wrong

The reflexive "why is Canonical rushing this" replies are the least interesting part, and the Rust-versus-C framing misses the point twice over. The uutils project has 24,081 stars, 2,033 forks and 1,154 open issues. It is not a toy. The segfault we reproduced is a stack overflow from unbounded recursion, which is a design mistake available in any language, not a memory-safety bug that C would have had and Rust prevented.

The sharpest dissent in the thread came from a commenter pointing at Fil-C, which compiles unmodified GNU coreutils with stronger memory-safety guarantees than Rust at roughly a 2x runtime cost, and none of the compatibility surface a rewrite creates. That is a genuine architectural argument, and it is the one worth having.

But the honest summary of what we found is smaller and more boring than either side wants. uutils 0.10.0 is close. It handles 16,000 levels of nesting and hundreds of thousands of ordinary deletes. It falls over at a depth almost nobody generates on purpose, and it is slow in one specific pipe configuration. Those are late-beta bugs, and 26.10 is an interim release with nine months of support, which is precisely where you are supposed to find them. Anyone running this on a fleet in October should know the exit code of their cleanup scripts.

Your move

The one question we cannot answer from a laptop: does anything in your stack actually generate directory trees deeper than 16,000 levels, or pipe multi-gigabyte images through dd bs=1M? Those are the only two things we found that break, and both are workload-specific. If you have a build system, a backup job, or a VM migration path that hits either, we would genuinely like to know what the number was.

If you are tracking Rust rewrites landing in tooling you did not choose, our piece on Homebrew 7.0.0 demoting every Intel Mac while explaining its own Rust rewrite covers the same tension from the other side: a project that shipped the rationale before the breakage, instead of after it.

Share

Ubuntu 26.10 finished moving rm to Rust. We segfaulted it at 17,000 directories deep, where GNU rm exits clean, and the upstream fix is still an open PR. #Ubuntu #RustLang #Linux #OpenSource

Never miss a ship

The best stuff that shipped this week, delivered every Thursday. Free, no spam. We read all the boring stuff so you get the fun parts.

Keep reading