Skip to content

Ubuntu 26.04: What Breaks in Bash Scripts

Published: October 10, 20267 min read

Ubuntu 26.04: What Breaks in Bash Scripts

The upgrade path from Ubuntu 24.04 LTS to 26.04 LTS opened with the 26.04.1 point release, and the servers that take it get two rewritten foundations at once: sudo-rs replaces sudo, and uutils, the Rust rewrite of coreutils, replaces most of ls, sort, date, timeout and the rest. Both aim to be drop-in replacements. Both are close. The scripts that break are the ones that relied on the gap: a sudoers rule with a wildcard, a check for exit code 137, a grep on an error message.

Everything below was reproduced on 2026-10-10 in containers on this machine, so the upgrade itself was never run: ubuntu:24.04 (24.04.5 LTS: bash 5.2.21, GNU coreutils 9.4, sudo 1.9.15p5) against ubuntu:26.04 (26.04.1 LTS: bash 5.3.9, uutils coreutils 0.10.0 with GNU coreutils 9.7 beside it, sudo-rs 0.2.13). Version numbers matter here, because uutils fixes compatibility bugs in every release; check yours with ls --version and sudo --version.

What Changed for Scripts?

Area24.04.526.04.1What to check
sudosudo 1.9.15p5sudo-rs 0.2.13 (classic sudo kept as sudo.ws)sudoers rules sudo-rs rejects; scripts matching sudo's messages
ls, sort, date, timeout, ... (99 commands)GNU coreutils 9.4uutils 0.10.0exit codes, stderr text, locale, edge cases
cp, mv, rm, df, trueGNU 9.4GNU 9.7nothing new (Ubuntu 26.10 moves cp, mv and rm to Rust too)
bash5.2.215.3.9not compared on these pages

What Breaks in sudo?

Rules with wildcards in arguments are dropped. A line like deploy ALL=(root) NOPASSWD: /usr/sbin/ipset add blacklist 203.0.113.* worked with classic sudo on 24.04. sudo-rs rejects it as wildcards are not allowed in command arguments, skips the line, prints the parse error on every sudo call, and denies the command:

text
$ sudo -n /usr/sbin/ipset add blacklist 203.0.113.7 /etc/sudoers.d/blocklist:1:29: wildcards are not allowed in command arguments deploy ALL=(root) NOPASSWD: /usr/sbin/ipset add blacklist 203.0.113.* ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ sudo: I'm sorry deploy. I'm afraid I can't do that exit=1

\* escapes, regex rules (^...$) and sha256: digests are rejected the same way. A lone * as the last argument is the one form that still works. Fix sudo-rs "wildcards are not allowed in command arguments" has the full table, a wrapper-script pattern for rules that need to check their arguments, and sudoers-rs-check.sh, which finds every affected rule on the 24.04 box before you upgrade.

The messages changed. Classic sudo answered a password is required for three different situations. sudo-rs says I'm sorry USER. I'm afraid I can't do that when no rule matches, interactive authentication is required when a rule needs a password that -n cannot give, and Authentication failed, try again. for a wrong password. The prompt changed from [sudo] password for USER: to [sudo: authenticate] Password:, which is what broke Ansible's become until Ansible shipped a fix. Fix sudo-rs "I'm afraid I can't do that" maps each message to its cause and has why-sudo-denied.sh, which says which one you hit.

What Breaks in Coreutils?

Most commands behaved identically in every test: sort with all its key options, cut, uniq, stat, date parsing and formats, seq, du, mktemp, install, split, sha256sum -c. The differences found, each run side by side in the 26.04 container:

  • timeout -s KILL exits 124, not 137. A script that checks for 137 to tell a hard kill from a soft timeout now always sees "timeout".
  • Error text changed for mkdir, ln (Already exists instead of File exists), chmod, touch, tee and timeout. Exit codes did not.
  • wc -m counts characters even in the C locale, where GNU counts bytes. café is 5 instead of 6.
  • numfmt accepts input GNU rejects and exits 0; base64 -d prints nothing before an invalid byte where GNU printed the valid part; pr lays out columns differently; printf %q quotes differently.
  • tail -n1 f >> f never stops on a file over 4 KB, reading back what it writes until the disk is full (uutils issue 15057, open at the time of writing).

Rust Coreutils vs GNU: What Breaks Scripts has the full table, what was reported but did not reproduce in 0.10.0, and coreutils-diff.sh, which runs any command line under both implementations and reports differences in exit status, output, stderr and files. Ubuntu Rust Coreutils: Check and Switch Back lists which implementation each command uses and the apt line that switches back.

How Do I Check a Server Before Upgrading?

In order, on the 24.04 box, before do-release-upgrade:

  1. sudoers: sudo ./sudoers-rs-check.sh. Every rule it lists will stop working. Fix them now; the fixed rules work on classic sudo too.
  2. Automation that drives sudo: search playbooks and scripts for password is required, password for and Sorry, try again. For Ansible, check that your version handles the sudo-rs prompt, or plan ansible_become_exe=/usr/bin/sudo.ws for upgraded hosts.
  3. Exit-code checks: grep -rn '137' /usr/local/bin /opt/*/bin and your cron scripts, looking for timeout result checks. Make them accept 124 as well.
  4. Message greps: grep -rn 'File exists\|cannot create directory\|invalid mode' in the same places. Branch on exit codes instead.
  5. Byte counts: any wc -m meant as bytes becomes wc -c, and scripts that depend on byte behaviour set LC_ALL=C explicitly.
  6. The scripts themselves: copy each into a 26.04 container and run it through coreutils-diff.sh. If a script's behaviour differs and you cannot fix it this week, call the gnu-prefixed command for that line (gnutimeout) rather than switching the whole system.

How Do I Test Scripts in a 26.04 Container First?

The default ubuntu:26.04 image already has both coreutils implementations, which is how every comparison on these pages was run:

bash
docker run --rm -it -v "$PWD:/work" -w /work ubuntu:26.04 bash # inside the container: ls --version | head -1 # ls (uutils coreutils) 0.10.0 gnuls --version | head -1 # ls (GNU coreutils) 9.7

sudo is not in the base image. apt-get update && apt-get install -y sudo installs both sudo-rs and the classic sudo.ws, with sudo-rs selected, which matches an upgraded server. Create a test user and copy your /etc/sudoers.d files in to see exactly which rules sudo-rs loads.

What If a Service Stops Starting After the Upgrade?

Read systemctl status before changing anything. Two failure states account for most "it worked yesterday" services, and neither is specific to 26.04:

To decode any other state, paste the output into the systemctl status Explainer. It reads the Loaded, Active and Main PID lines and the journal lines, and names the cause and the next command. Note that for an ordinary Type=simple service, systemctl start exits 0 even when the program fails a moment later. A post-upgrade health check has to ask systemctl is-active, not trust start.

Should I Switch Back to sudo and GNU Coreutils?

Both switches exist and both were tested in the 26.04 container:

bash
sudo update-alternatives --set sudo /usr/bin/sudo.ws # classic sudo sudo apt install coreutils-from-gnu coreutils-from-uutils- --allow-remove-essential # GNU coreutils

They are the right call when production is down and the fix is not obvious. They are the wrong place to stay: Ubuntu's own testing assumes the defaults, and the next release moves more commands to Rust, not fewer. Switch back, fix the scripts with the checks above, then return to the defaults (sudo update-alternatives --auto sudo, and the apt line with the two package names swapped).

PAID RESOURCE — $9

The Production Bash Toolkit

An operational script system + a 30-function shared library + a 52-page field guide. The production layer the free snippets don't cover.

Get the Toolkit →
curl -O bashlib-starter.sh

Get the bashlib starter

Ten functions I source into every script on my own boxes — strict-mode setup, an ERR trap that names the failing line, lock and timeout wrappers, and cleanup that runs on every exit path. One email, no sequence.