SSD Nodes Learn Hosting plans →
Guides Matt ConnorBy Matt Connor

Rust rewrites in your Ubuntu base system

Find which base tools on your Ubuntu server are Rust rewrites, which package installed them, and what that changes for your shell scripts and cron jobs.

Verified Every command ran end-to-end on a fresh Ubuntu 24.04 server, August 23, 2026.

What changed under your scripts

The Rust rewrites in your Ubuntu base system are new implementations of old tools, ls, sort, date, sudo and their neighbours, written in Rust instead of C and installed under the same names at the same paths. Nothing in your shell looks different. Your scripts still call sort -u, and they still find something at /usr/bin/sort.

That is the operational problem in one sentence. A program that keeps the name and the path but differs in one flag, one exit status or one line of output will pass the checks you run by hand and then behave differently inside a cron job at 04:00.

So this page is an inventory method, not a list of results. Releases change, and a server upgraded in place is not the same as a fresh install, so the only answer that is true for your machine is the one your machine gives you. Below are the commands that produce it.

Two names first. GNU coreutils is the C implementation that has provided ls, cp, sort and about a hundred other programs on Linux for decades. uutils is a separate project that reimplements those same programs in Rust and aims for compatible behaviour. sudo-rs does the same for sudo and su. Different codebases by different authors, so compatibility is a goal rather than a guarantee.

Which Rust rewrites are on this Ubuntu box?

Start with one tool and work outwards.

type -a sort
command -v sort
readlink -f "$(command -v sort)"
sort --version | head -n1

type -a goes first because the shell can answer before the filesystem does. An alias or a shell function means you are not running the binary at all. command -v prints the path that would actually run. readlink -f follows every symlink to the real file on disk, which matters because the entry in /usr/bin is often a link. --version prints the implementation's own claim about itself, so read that line rather than assuming it. Do not compare version numbers between implementations either, because the projects number their releases independently, so a lower number does not mean an older program.

One result is worth recognising on sight.

readlink -f /usr/bin/ls /usr/bin/cat /usr/bin/sort /usr/bin/wc

If those four lines print the same path, one executable is serving all four commands. That is a multi-call binary: the program reads the name it was invoked under and behaves accordingly, the way busybox does. It means those tools move together on every upgrade, and it means one dpkg -S on the resolved path tells you about all of them at once.

Which package provides a given tool?

Ask dpkg -S twice, once with the symlink path and once with the resolved path.

dpkg -S /usr/bin/sort
dpkg -S "$(readlink -f "$(command -v sort)")"

The output is package: /path, or package:arch: /path on a system that installs packages for more than one architecture, so the field before the first colon is the package name in both forms. If the two commands name two different packages, something has replaced or diverted the file, and that is the case worth understanding.

A diversion is dpkg's own mechanism for letting one package take over a file owned by another. It renames the original and records who did it, so the file at /usr/bin/sort can come from a package other than the one that declares it.

dpkg-divert --list '*coreutils*'
dpkg-divert --truename /usr/bin/sort

Each line from --list names the diverted path, the path the original file was moved to, and the package that owns the diversion. --truename prints where the original went. No output from either means no diversion is in play, and the file belongs to the package dpkg -S named.

Now look at the packages themselves.

apt-cache policy coreutils rust-coreutils sudo sudo-rs
dpkg-query -W -f='${binary:Package} ${Version} ${Essential} ${Priority}\n' coreutils
dpkg -L rust-coreutils | grep '/bin/'

apt-cache policy prints an Installed: line and a Candidate: line for every name you give it. Installed: (none) means the package is not on the box. A Candidate: version means your configured sources still carry it and apt install could fetch it, which is the rollback question answered in one line. A name that produces no output at all is not in your sources under that spelling. dpkg -L lists the files a package installed, which turns "what is this package" into "which commands did it put in my PATH".

When the package is not installed and you want to know what would provide a path, search the archive index instead.

sudo apt install -y apt-file && sudo apt-file update
apt-file search /usr/bin/sort

Then snapshot the whole set, so you can compare two machines instead of reading one.

for t in ls cp mv rm cat sort head tail wc du df date cut tr sudo; do
  p=$(command -v "$t") || { printf '%s\tMISSING\n' "$t"; continue; }
  r=$(readlink -f "$p")
  pkg=$(dpkg -S "$r" 2>/dev/null | tail -n1 | cut -d: -f1)
  ver=$("$t" --version 2>/dev/null | head -n1)
  printf '%s\t%s\t%s\t%s\n' "$t" "$r" "$pkg" "$ver"
done | tee "/tmp/toolmap-$(uname -n).tsv"

Run that on the current server and on a test build of the release you are moving to, then diff the two files. Every line that differs marks a tool whose callers deserve a second look. The check takes a minute, and it is the part most people skip.

Why scripts break: flags, exit status, output

Four differences matter, and they are not equally visible.

A missing flag is easy to find. The command exits non-zero, the script stops, and you see it the first time it runs. Probe the flags your scripts really use.

for f in -u -r -n -k2 --stable --parallel=2; do
  if sort "$f" /dev/null >/dev/null 2>&1; then s=ok; else s="rejected ($?)"; fi
  printf '%-12s %s\n' "$f" "$s"
done

A flag that is accepted and ignored gives you no signal at all, which is why you should test values rather than exit codes. It's FOSS reported on October 27, 2025 that the Rust date on Ubuntu 25.10 accepted -r and returned the current time instead of the file's modification time, which stopped the system's automatic update check from running. The argument parser knew the flag. Nothing was wired behind it. So assert the answer:

touch -d '2020-01-01 00:00:00' /tmp/probe
date -r /tmp/probe '+%Y'

That must print 2020. Anything else means every script on the box that reads a file timestamp through date -r is silently wrong.

A different exit status changes which branch of your script runs, because a script under set -e cares about the number and not the message.

sort /nonexistent-file; echo "exit=$?"
cp /nonexistent-file /tmp/x; echo "exit=$?"

Record those numbers on both machines. A tool that exited 1 and now exits 2 takes a different path through any case or if that inspects $?.

Different error text breaks any script that greps stderr for a phrase. Redirecting with 2>/dev/null and testing the exit status survives a rewrite. A grep -q 'No such file' does not. That one is worth fixing whichever implementation you end up on, because the wording is not a stable interface in either project.

Locale and formatting differences

Two implementations can both be correct and still print different bytes. Field padding, thousands separators, the rounding in -h human readable sizes and the sort order are the usual places.

Sort order is the one that damages data quietly. sort orders by the collation rules of the current locale, and en_US.UTF-8 treats punctuation and case differently from C. Any script that sorts a list, or that feeds sort into uniq or comm, has to pin the locale so both sides agree.

export LC_ALL=C
sort -u hosts.txt > hosts.sorted

LC_ALL=C is the right default in scripts under any implementation, because byte order is the one rule that reproduces everywhere, and it removes the locale from the list of things that can differ between two machines. Interactive shells and service units often carry different locales, which is why a pipeline gives one order when you type it and another order under systemd. Check what a unit really receives.

systemctl show-environment
sudo systemd-run --pipe env | grep -E '^(LANG|LC_)'

date belongs to coreutils, so every backup filename and log parser built on a date +%... format string is in scope for this swap and deserves a side by side run. If the timestamps come out wrong rather than differently formatted, look elsewhere first: clock drift and time sync on a VPS produces wrong times from every implementation equally, and it is far more common than a formatting change.

Can you go back to the GNU tools?

Usually yes, and the time to confirm it is before you need it.

The first question is whether your sources still carry the implementation you want, and apt-cache policy answers it. The second is what depends on what, and the safe way to ask is to simulate. apt-get -s prints the complete plan, including every package it would remove, and changes nothing on disk. It says so in its own output, which notes that this is only a simulation.

apt-cache policy coreutils-from-gnu coreutils-from-uutils
apt-get -s install coreutils-from-gnu

Those two metapackage names come from a Canonical Foundations update posted to the Ubuntu Discourse on April 22, 2026, which describes swapping one for the other as the supported way to choose an implementation. Whether they exist on your release is what the apt-cache policy line above is for.

Read the removal list in that simulation carefully, because coreutils is not an ordinary package.

dpkg-query -W -f='${binary:Package} ${Essential} ${Priority}\n' coreutils bash

If the Essential column reads yes, dpkg refuses to remove that package without a force flag, and it refuses for a good reason. The programs inside coreutils are what apt and your recovery shell use to do anything at all. Do not reach for dpkg --remove --force-remove-essential to get past that refusal. Make the change with apt, from a second root session that is already open, and have your provider's console ready. Use the same care you would use for a firewall rule change.

sudo is the higher stakes half of the same question, because a sudoers file that the new implementation rejects takes away your route to root rather than one command. Keep that second root session open while you test, and read what changes when sudo-rs replaces sudo for the directives that do and do not carry across.

You will also find posts recommending oxidizr, a tool for toggling these implementations on older releases. Its repository was archived on October 1, 2025, and its own README warns that it "may cause a loss of data, or prevent your system from booting". It is a tool for experiments. On a server you care about, use the packages.

What Canonical has said is coming

Every claim in this section belongs to someone else and carries the date it was made, because a plan is not a shipped release.

Jon Seager of Canonical set out the direction on the Ubuntu Discourse on March 12, 2025, in "Carefully but purposefully oxidising Ubuntu". That post names uutils coreutils, uutils findutils, uutils diffutils and sudo-rs as targets, and states the goal this way:

My immediate goal is to make uutils' coreutils implementation the default in Ubuntu 25.10, and subsequently in our next Long Term Support (LTS) release, Ubuntu 26.04 LTS, if the conditions are right.

It's FOSS has tracked each step since. Sourav Rudra reported the sudo-rs decision on May 8, 2025, noting that sudo-rs reimplements sudo and su, and that its developers do not intend to carry every feature of the original across. The same site reported on September 16, 2025 that testing found cksum slower than the GNU version under the hyperfine benchmark tool, and sort producing no output on very large single-line files, while base64 came out faster.

The most recent statement is the Foundations update of April 22, 2026 linked above. It says that cp, mv and rm continue to be provided by GNU coreutils in 26.04 because of open TOCTOU (time of check to time of use) issues, counted at eight on the day of that post, and that the team targets Ubuntu 26.10 for "100% rust-coreutils".

Read all of that as reporting, not as a description of your server. What runs on your machine is set by your release and your upgrade history. If you are deciding when to meet these changes at all, that is the ordinary choice between LTS and interim releases on a server. Interim releases exercise a change like this first. An LTS gives you a longer gap between changes of this kind.

A routine that catches this before your users do

Keep the snapshot file from the loop above with the rest of your server notes. When you plan an upgrade from 24.04 to 26.04, build the target release in a throwaway virtual machine, run the same loop there, and diff the two files. For each tool whose line changed, run the scripts that call it against real input under set -euo pipefail, not against --help.

Give timers and cron jobs one extra step: run them in the environment they will really get, using sudo systemd-run --pipe, because locale and PATH differ there from your interactive shell.

The failure this prevents is not dramatic. It is a backup script that exits 0 after writing an empty archive, or a rotation job that sorts in a new order and overwrites the file it meant to keep. Nothing is logged, because from the system's point of view nothing failed. A tool answered a slightly different question than the one the script asked.

FAQ

How do I tell if ls on my server is the GNU version or the Rust one?

Run readlink -f "$(command -v ls)" to get the real file, then dpkg -S on that path to get the package that owns it, then ls --version | head -n1 to read the implementation's own name and version string. Check type -a ls as well, because an alias or a shell function answers before the binary does. If several tools resolve to the same file, you have a multi-call binary serving all of them, so they share one package and move together.

Are the GNU coreutils still installable if the Rust ones break my scripts?

Check on the box rather than trusting an article. apt-cache policy coreutils-from-gnu coreutils-from-uutils prints an Installed: and a Candidate: line for each name, and a Candidate: version means your sources still carry it. A Canonical Foundations post on the Ubuntu Discourse dated April 22, 2026 describes those two metapackages as the way to choose. Simulate first with apt-get -s install coreutils-from-gnu and read the removal list, because coreutils is an Essential package and forcing its removal leaves you without the programs apt itself needs.

Which differences actually break shell scripts?

Four: a flag that is missing, a flag that is accepted but does nothing, a different exit status on error, and different output text or sort order. The second is the dangerous one, because the command still succeeds. It's FOSS reported on October 27, 2025 that date -r returned the current time instead of a file's modification time on Ubuntu 25.10, which stopped automatic update checks with no error anywhere. Assert the value your script depends on, not only the exit code: touch -d '2020-01-01' /tmp/probe && date -r /tmp/probe +%Y has to print 2020.

Does this affect sudo as well?

Yes. sudo-rs is a separate reimplementation of sudo and su, and It's FOSS reported on May 8, 2025 that its developers do not plan to carry every feature of the original across. That makes sudoers compatibility the thing to test, rather than flag coverage. See what changes when sudo-rs replaces sudo, and keep a second root session open while you test, because a rejected sudoers file removes your route to root and not just one command.