Skip to content

[3.5.0 regression] testsuite: protected-regular hangs for 300s and FAILs when unshare(2) is refused #1045

Description

@samueloph

I can reproduce the following issue on the Debian package but I asked an AI for a reproducer that doesn't require the Debian package.

I believe the reason I did not spot this earlier is because the embargo patchset didn't carry the runtests.py refactor, but have not double checked.

What happens

protected-regular hangs until the runner kills it whenever the kernel refuses a new
user namespace — most commonly because the tests are already running inside a chroot:

----- protected-regular log follows

TIMEOUT: test took over 300 seconds
----- protected-regular log ends
FAIL    protected-regular

Five minutes and a spurious failure per run. We hit it building the Debian package
under sbuild --chroot-mode=unshare.

Version: 3.5.0, also on git master (dcef974c). util-linux 2.42.2, Python 3.14.

Where it stops

The test is not root, so it probes for a usable unshare
(testsuite/protected-regular_test.py:52). Running it under python3 -X faulthandler
and sending it SIGABRT once hung:

Current thread 0x00007fa8da70f200 [python3] (most recent call first):
  File "/usr/lib/python3.14/selectors.py", line 398 in select
  File "/usr/lib/python3.14/subprocess.py", line 2153 in _communicate
  File "/usr/lib/python3.14/subprocess.py", line 1221 in communicate
  File "/usr/lib/python3.14/subprocess.py", line 557 in run
  File ".../testsuite/protected-regular_test.py", line 52 in <module>

Reproducer

Self-contained — the inner chroot is only there to make the kernel refuse the
namespace. [1] is the probe run directly, [2] is the same probe run the way the
test runs it:

unshare --user --map-root-user --mount --pid --fork --mount-proc sh -c '
mkdir -p /tmp/c; mount --make-rprivate /; mount --rbind / /tmp/c
/usr/sbin/chroot /tmp/c /bin/sh -c "
  echo \"[1] direct:\"
  t=\$(date +%s); unshare --user --map-root-user --map-users 5001:100000:1 true 2>/dev/null
  echo \"    rc=\$? after \$((\$(date +%s)-t))s\"
  echo \"[2] capturing output:\"
  t=\$(date +%s); timeout 10 python3 -c \"
import subprocess
subprocess.run([\\\"unshare\\\",\\\"--user\\\",\\\"--map-root-user\\\",\\\"--map-users\\\",\\\"5001:100000:1\\\",\\\"true\\\"], capture_output=True)\"
  echo \"    rc=\$? after \$((\$(date +%s)-t))s   (124 = timed out)\"
"'
[1] direct:
    rc=1 after 0s
[2] capturing output:
    rc=124 after 10s   (124 = timed out)

unshare(1) exits promptly either way. It is capture_output=True that hangs: a
process unshare leaves behind keeps the inherited stdout/stderr open, so the pipes
never reach EOF and communicate() blocks after the probe itself is gone. Arguably a
util-linux bug as well, and worth reporting there, but the probe only ever reads
.returncode — sending the output to /dev/null instead of capturing it makes the
hang go away and the test skips immediately:

SKIP    protected-regular (Can't chown (need root or unshare with uidmap))

Regression

Introduced by 1f689ec0c21b7c2eaa9add1958d2c7ed280aac3e (testsuite: rewrite the
shell testsuite in Python
), which turned the shell probe added in
67f1dcf604b6c5f2e860c3bbd174bbfd14d5c504 into a capture_output=True
subprocess.run(). The shell form waited only for its direct child and sent stderr to
/dev/null, so it returned immediately — case [1] above.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions