← all posts

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.

567commands, each run through uutils and GNU
23gave a script different output or exit code on 26.10
42%of tries, two mkdir calls both got the same lock on 26.04
0errors when mv reset a backup's age on 26.10

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
uutilsStill GNU
Ubuntu 25.100.2.2cp mv rm true
Ubuntu 26.04 LTS0.8.0 at release, 0.10.0 since Septembercp mv rm df true
Ubuntu 26.100.12.0none

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:

mkdirBoth exited 0
GNU 9.70 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 runninguutils 0.10 and 0.12
timeout 1124, nothing124, nothing
timeout -k 1 1 (job ignores TERM)137, nothing137, 2 sleeps
timeout -s KILL 1137, nothing124, 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 …GNUuutils 0.10 and 0.12
'2026-03-29 02:30' (skipped)error: invalid date03: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 …GNUuutils
'2026-10-20 12:00 -1 day'2026-10-212026-10-19
'2026-10-20 friday'2026-10-202026-10-23
'2026-10-20 12:00 friday next week'2026-10-272026-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: file sets the owner and leaves the group alone, on both releases. GNU sets the group to the user’s login group. Write chown user:group. (LP #2152867)
  • df -m fails on 26.10 with error: unexpected argument '-m' found. Use df -k or df -BM. On 26.04 df is still GNU. (LP #2163678)
  • wc < file with no flags pads each number to seven columns ( 5 10 22), where GNU prints 5 10 22. wc -l < file matched, so $(wc -l < f) is fine.
  • du -b f | numfmt --to=iec keeps the tab after the number; GNU turns it into a space.
  • base64 -d on input with garbage in it writes nothing; GNU writes the valid part, then errors. Both exit 1.
  • cut -c1-3 counts characters, GNU 9.7 and 9.10 count bytes, so žluť gives žlu against žl. GNU 9.11 counts characters too.
  • Error messages read differently on 23 commands: mkdir: dir: File exists against mkdir: cannot create directory ‘dir’: File exists, and an unknown option prints a multi-line error: unexpected argument block. 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?
Yes. Since Ubuntu 25.10, the date, timeout, sort, mkdir, touch and nearly every other coreutils command in /usr/bin comes from uutils, a Rust rewrite. Ubuntu 26.04 LTS shipped uutils 0.8.0 and has had 0.10.0 in -updates since September 2026; cp, mv, rm, df and true are still GNU there. Ubuntu 26.10 ships uutils 0.12.0 for all of them. date --version prints 'date (uutils coreutils)' if you have it.
How do I switch back to GNU coreutils on Ubuntu?
For one script or crontab, call the GNU commands Ubuntu already installs with a gnu prefix (gnudate, gnutimeout, gnumv), or put a directory of symlinks to them first in PATH. To switch the whole system, Ubuntu's 26.04 release notes give: sudo apt install coreutils-from-gnu coreutils-from-uutils- --allow-remove-essential. If /usr/bin/gnudate is missing, apt install gnu-coreutils first.
Why did unattended-upgrades stop working on Ubuntu 25.10?
uutils date 0.2.2 ignored the -r option and printed the current time instead of the file's modification time. The apt daily job compares the age of /var/lib/apt/periodic/upgrade-stamp with date -r, always got 0, and concluded it had just run, so no upgrades ran and nothing reported an error (Launchpad bug 2127970). rust-coreutils 0.2.2-0ubuntu2.1 fixed it on 2025-10-23, but because the job itself was broken, affected machines needed a manual apt update to receive the fix.
Is mkdir still safe as a lock on Ubuntu?
Not on Ubuntu 26.04 with uutils 0.10.0. When two processes ran mkdir on the same missing directory at once, both exited 0 in 125 of our 300 tries, so both believed they held the lock (Launchpad bug 2171333). GNU mkdir and uutils 0.12.0 on Ubuntu 26.10 never did. Use flock from util-linux instead, which isn't part of coreutils and wasn't replaced.
How do I catch a cron job that silently stopped working?
Exit codes only catch jobs that know they failed. The 25.10 date bug made jobs exit 0 having done nothing. Check the result instead: that the backup file exists, is newer than a day by its content or name rather than its mtime, and isn't empty, and fail loudly if not. RunWisp, an open-source cron replacement that ships as a single binary, can also mark an exit-0 run as failed when its output matches a pattern (failures = ["+output:(?i)error"]) and alert on it, and its timeout signals the whole process group without going through timeout(1). Its limits: it can't notice a job that prints nothing and does nothing, and your script still runs on whatever coreutils the box has.
  1. 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.

  2. 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.

Get started
GPL 3.0 · no accounts · free to self-host

Start in 30 seconds. Ship on your terms.

A single Go binary. If it doesn't earn the disk space, rm it. No account, no signup, no catch.

Install in 30 seconds
curl -fsSL https://get.runwisp.com | sh