Ubuntu 26.04 Rust coreutils (uutils) vs GNU: what changes for cron and shell scripts
Which uutils commands behave differently from GNU coreutils on Ubuntu 26.04 and 26.10, tested on 567 commands: mkdir locks, mv and mtime, timeout, date and DST, df, chown. How to check which you have and how to use GNU instead.
10 min read
In October 2025, Ubuntu 25.10’s automatic updates stopped. Nothing failed. date -r file was
supposed to print a file’s modification time, and in the brand-new Rust coreutils it printed the
current time instead. The apt daily job asked how old its stamp file was, got “zero seconds” every
day, and decided it had just run (LP #2127970). At least
one person’s backup cron job went quiet the same way. Every run exited 0.
That bug is fixed. But Ubuntu 26.04 LTS runs the same Rust coreutils, uutils, and 26.10 runs more of
it, and cron scripts are made of date, timeout, mkdir, mv and sort. So we took 567
commands of the kind cron scripts run, ran each one with uutils and with GNU coreutils on both
releases, and diffed the output, the exit code and stderr.
Short answer
Since 25.10, Ubuntu’s /usr/bin/date, timeout, mkdir, sort and most other coreutils
commands are uutils, a Rust rewrite. 26.04 keeps cp, mv, rm and df on GNU; 26.10 switches
those too. Most of what cron scripts do came out identical. What didn’t: on 26.04, mkdir as a
lock can let two runs in at once; on 26.10, mv across filesystems resets the file’s mtime, which
breaks find -mtime cleanup, and df -m is rejected. On both, timeout’s SIGKILL misses child
processes, chown user: leaves the group alone, and date -d resolves DST gaps and some relative
dates differently. GNU is still installed: call gnudate, gnutimeout, gnumv for the scripts
that matter, or put symlinks to them first in a crontab’s PATH. Exit codes won’t tell you when
any of this bites, so check what the job produced. RunWisp, an open-source cron replacement that
ships as a single binary, can fail a run on its output and kills
a timed-out job’s whole process group itself.
Are you running it?
$ date --version | head -1
date (uutils coreutils) 0.10.0
$ readlink -f /usr/bin/mv
/usr/bin/gnumv
| uutils | Still GNU | |
|---|---|---|
| Ubuntu 25.10 | 0.2.2 | cp mv rm true |
| Ubuntu 26.04 LTS | 0.8.0 at release, 0.10.0 since September | cp mv rm df true |
| Ubuntu 26.10 | 0.12.0 | none |
Debian, Fedora, Arch and Alpine are unaffected. find, xargs, diff, flock, logger and
setsid aren’t coreutils and are still the GNU and util-linux versions on every Ubuntu. GNU
coreutils is still installed next to uutils, with a gnu prefix on every command, which is how we
ran both side by side.
How we tested
We wrote 567 one-liners in the shape cron scripts use them: date arithmetic for backup names,
timeout around a job, sort | uniq -c, du -sh, stat -c %Y, mktemp, sha256sum -c,
cut -d:, ls -t | tail -n +8 for rotation, and so on. Each ran in a fresh copy of the same
directory, once with the system’s commands and once with a PATH pointing at symlinks to the
gnu* binaries, in Ubuntu 26.04.1 (uutils 0.10.0, GNU 9.7) and Ubuntu 26.10 (uutils 0.12.0,
GNU 9.10) containers with TZ=UTC and LC_ALL=C.UTF-8. We re-ran anything timing-dependent on
its own to rule out flukes.
On 26.10, 511 of the 567 came out byte-for-byte the same. 23 differed only in the wording of an
error message and 10 in timing or shell noise. The other 23 changed what a script sees, and they
come down to a few problems, worst first, plus three we tested separately: concurrent mkdir,
chown and df -m.
mkdir locks that two runs both get (26.04)
mkdir is the classic lock for shell scripts because it either creates the directory or fails,
atomically:
mkdir /run/lock/backup.lock || exit 0
trap 'rmdir /run/lock/backup.lock' EXIT
We started two mkdir calls on the same missing directory at the same moment, 300 times:
mkdir | Both exited 0 |
|---|---|
| GNU 9.7 | 0 of 300 |
| uutils 0.10.0 (26.04) | 125 of 300 |
| uutils 0.12.0 (26.10) | 0 of 300 |
Both runs think they hold the lock and both proceed, which is exactly what the lock was for. It’s
LP #2171333, fixed upstream in 0.11 and requested for
26.04 but not shipped yet. Use flock, which is util-linux, not coreutils; the overlap
post has the one-line version and its gotchas.
mv resets mtime across filesystems (26.10)
Scripts often dump to /tmp or /dev/shm and move the result into place. On 26.10, mv is
uutils, and when the move crosses filesystems it gives the file a new modification time:
$ touch -d '2026-09-01 03:00' /dev/shm/db.sql.gz
$ mv /dev/shm/db.sql.gz /backups/
$ stat -c %y /backups/db.sql.gz
2026-10-20 10:37:25.402711383 +0000
$ find /backups -name db.sql.gz -mtime +30 | wc -l
0
GNU mv kept 2026-09-01 and find found it. Any retention that runs on mtime (find -mtime +30 -delete, ls -t | tail -n +8) now sees a fresh file. The usual direction,
a new dump moved into place, happens to come out the same, because the file is new anyway. It’s
the archive and rotate scripts that move old files that keep them forever. Upstream:
uutils #11738, open. cp -a across the same
filesystems kept the mtime in our test, so cp -a src dst && rm src is the workaround if you can’t
pin gnumv.
timeout’s SIGKILL misses children
This one is in the timeout post, with the full test. In short, when
uutils timeout sends SIGKILL, from -s KILL or after -k, it reaches only the command it
started, not that command’s children:
timeout … sh -c 'sleep 7 & sleep 7' | GNU: exit, left running | uutils 0.10 and 0.12 |
|---|---|---|
timeout 1 | 124, nothing | 124, nothing |
timeout -k 1 1 (job ignores TERM) | 137, nothing | 137, 2 sleeps |
timeout -s KILL 1 | 137, nothing | 124, 2 sleeps |
A script that checks for 137 to tell “had to be killed” from “timed out” gets 124. The exit code
part is fixed upstream (#15075) but not in any
release Ubuntu ships. We found no upstream issue for the children. Use gnutimeout in crontabs.
date and daylight saving time
Both sides agree on date -d '7 days ago', date -d @1792000000, -I, -R, +%s, date -r
(fixed since 25.10) and every format we tried in UTC. They disagree on times the clock change
makes odd, in TZ=Europe/Berlin:
date -d … | GNU | uutils 0.10 and 0.12 |
|---|---|---|
'2026-03-29 02:30' (skipped) | error: invalid date | 03:30 CEST, exit 0 |
'2026-10-25 02:30' (happens twice) | 02:30 CET (the second) | 02:30 CEST (the first) |
GNU refuses a time that doesn’t exist; uutils moves it forward an hour and succeeds. For an
ambiguous time they pick opposite sides, an hour apart. A script that turns a local time into
+%s for comparison is off by an hour on those nights. Rejecting the skipped time is fixed upstream
in #15111, unreleased. If the job runs in UTC,
none of this applies, which is one more reason to run servers in UTC. The DST
post has what cron itself does on those nights.
date’s relative strings
GNU’s date parser has quirks that scripts have come to depend on, and uutils fixed some of them:
date -d … | GNU | uutils |
|---|---|---|
'2026-10-20 12:00 -1 day' | 2026-10-21 | 2026-10-19 |
'2026-10-20 friday' | 2026-10-20 | 2026-10-23 |
'2026-10-20 12:00 friday next week' | 2026-10-27 | 2026-10-30 |
GNU reads 12:00 -1 as 12:00 in UTC−1, then adds a day, which is why its answer is wrong. And GNU
ignores a weekday next to a full date, where uutils moves to that weekday. uutils is arguably
right on both. Both happen only when a time or a weekday follows a full date: date -d '2026-10-20 -1 day',
date -d yesterday and date -d 'last sunday' agree. To be safe on both, put the relative part
first or use @epoch arithmetic: date -d "@$(( $(date +%s) - 86400 ))" +%F.
With LC_ALL=en_US.UTF-8, date +%c, +%x and +%X ignore the locale in uutils
(10/14/26 17:46:40 against GNU’s 10/14/2026 05:46:40 PM). The upstream fix for %x and %X is
merged but not released; %c is #14679. Plain
date with no format matched.
Smaller changes a parser would notice
chown user: filesets the owner and leaves the group alone, on both releases. GNU sets the group to the user’s login group. Writechown user:group. (LP #2152867)df -mfails on 26.10 witherror: unexpected argument '-m' found. Usedf -kordf -BM. On 26.04dfis still GNU. (LP #2163678)wc < filewith no flags pads each number to seven columns (5 10 22), where GNU prints5 10 22.wc -l < filematched, so$(wc -l < f)is fine.du -b f | numfmt --to=ieckeeps the tab after the number; GNU turns it into a space.base64 -don input with garbage in it writes nothing; GNU writes the valid part, then errors. Both exit 1.cut -c1-3counts characters, GNU 9.7 and 9.10 count bytes, sožluťgivesžluagainstžl. GNU 9.11 counts characters too.- Error messages read differently on 23 commands:
mkdir: dir: File existsagainstmkdir: cannot create directory ‘dir’: File exists, and an unknown option prints a multi-lineerror: unexpected argumentblock. Exit codes matched. If a script greps stderr for GNU’s wording, it won’t match.
That’s the list we could reproduce. Ubuntu’s bug tracker has more we didn’t test, including
tail -f on a symlink never printing (LP
#2156594) and stdbuf -oL not line-buffering (LP
#2142228). For scale: uutils 0.12.0 passes 653 of the
674 GNU tests it runs.
Pinning GNU for a script
GNU coreutils is still installed. For one line, call it by name:
0 3 * * * gnutimeout -k 30s 20m /usr/local/bin/backup.sh
For a whole script, make a directory of symlinks once and put it first in PATH:
sudo mkdir -p /usr/local/lib/gnubin
for f in /usr/bin/gnu*; do sudo ln -sf "$f" "/usr/local/lib/gnubin/${f#/usr/bin/gnu}"; done
PATH=/usr/local/lib/gnubin:/usr/local/bin:/usr/bin:/bin
0 3 * * * /usr/local/bin/backup.sh
That’s how we ran the GNU side of every test. GNU’s commands work under the plain names; they
read their name from how they were called. If /usr/bin/gnudate doesn’t exist,
sudo apt install gnu-coreutils brings them back.
To switch the whole system, the 26.04 release notes give
sudo apt install coreutils-from-gnu coreutils-from-uutils- --allow-remove-essential. That’s a
supported, documented path, but you’re opting out of what Ubuntu tests by default, and the
commands differ in the other direction too. For a server running a handful of scripts, pinning
the scripts is the smaller change.
Catching the ones that exit 0
The 25.10 bug is the pattern to plan for: the job ran, did nothing, and exited 0. Exit-code alerting, which is still worth having, can’t see that. Check the result inside the job instead:
#!/bin/sh
set -eu
f=/backups/db-$(date +%F).sql.gz
pg_dump app | gzip > "$f"
# fail loudly if the dump is missing or suspiciously small
[ "$(stat -c %s "$f")" -gt 100000 ] || { echo "backup too small: $f" >&2; exit 1; }
The check uses the date in the name, not the file’s mtime, which after the mv section is the
point. An external heartbeat such as Healthchecks.io, pinged only after that check passes, also
catches the night the job never ran at all.
RunWisp (our project, so weigh accordingly) is an open-source cron replacement that ships as a
single binary. It doesn’t fix uutils; your script runs on whatever coreutils the box has. Two
things it does take off the table, in /etc/runwisp/runwisp.toml:
[tasks.backup]
cron = "0 3 * * *"
run = "/usr/local/bin/backup.sh"
timeout = "20m"
failures = ["+output:(?i)(error|too small)"]
notify = ["phone"]
timeout here isn’t timeout(1). RunWisp starts each run in its own process group, sends SIGTERM
to the whole group, and SIGKILL to the whole group after graceful_stop (5 seconds by default), so
children don’t outlive the run. And failures with an output: pattern marks a run as failed when
a line of its output matches, even if it exited 0, so the backup too small line alerts you
whether or not the script remembered to exit 1. notify sends that to a notifier you’ve named
phone (ntfy, Telegram, Slack, email and others). Every run’s output and exit code are kept.
The limits: a job that prints nothing and does nothing still looks like a success, so the check in the script is what catches it. RunWisp runs on the same box, so it can’t tell you the box is down; pair it with a heartbeat for the jobs that matter. And it’s one more service to run. The cron comparison has the rest.
Common questions
Does Ubuntu 26.04 use Rust coreutils?
How do I switch back to GNU coreutils on Ubuntu?
Why did unattended-upgrades stop working on Ubuntu 25.10?
Is mkdir still safe as a lock on Ubuntu?
How do I catch a cron job that silently stopped working?
Read next
-
Debian cron vs cronie vs BusyBox crond: CRON_TZ, /etc/cron.d, env vars and syntax compared
How cron differs on Ubuntu, Debian, Fedora, RHEL, Arch and Alpine: which crons support CRON_TZ, read /etc/cron.d, pass Docker env vars, handle a bad crontab line, and accept seconds, L, W and #. Tested on six distros.
-
When does cron's @reboot run? Not when your container restarts
@reboot runs the first time cron starts after /run was emptied. On a server that's once per boot. In Docker it's once per container, so a restart, or a host reboot with a restart policy, skips it. We tested six crons.