Skip to content

path: Force-remove files in unlink() - #130

Open
kvmajo wants to merge 1 commit into
Rahix:masterfrom
kvmajo:fix-unlink-force
Open

path: Force-remove files in unlink()#130
kvmajo wants to merge 1 commit into
Rahix:masterfrom
kvmajo:fix-unlink-force

Conversation

@kvmajo

@kvmajo kvmajo commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Fixes #128.

Path.unlink() removes a file with a plain rm:

self.host.exec0("rm", self)

If the file is write-protected (e.g. -r--r--r--) but the containing
directory still permits deletion, GNU rm asks an interactive
confirmation question before removing it:

rm: remove write-protected regular file '...'?

Since tbot's channel has no way to answer that question, exec0()
never sees a shell prompt again and hangs indefinitely instead of
raising an error.

unlink() already resolves existence/missing_ok itself before
running rm, so nothing is lost by making the actual removal
non-interactive as well, via -f.

Testing

  • pre-commit's pinned black, flake8, and mypy all pass on the
    changed file.
  • selftest/tests/test_path.py has the same pass/fail counts before
    and after this change (two pre-existing, unrelated failures in
    test_rglob_error reproduce identically on unmodified master).
  • Reproduced the actual bug: unlinking a chmod 400 file hangs
    (confirmed via timeout) on unmodified master, and completes cleanly
    on this branch.

Path.unlink() removes a file with a plain `rm`.  If the file is
write-protected (e.g. -r--r--r--) but the containing directory still
permits deletion, GNU rm asks an interactive confirmation question
before removing it.  Since tbot's channel has no way to answer that
question, exec0() never sees a shell prompt again and hangs
indefinitely instead of raising an error.

unlink() already resolves existence/missing_ok itself before running
rm, so nothing is lost by making the actual removal non-interactive
as well, via -f.

Fixes: Rahix#128
Signed-off-by: Martin Jocic <martin.jocic@kvaser.com>
@Rahix

Rahix commented Aug 20, 2026

Copy link
Copy Markdown
Owner

Hi, thanks for the contribution!

I just confirmed, Python pathlib's .unlink() also removes a 0400 file without complaining so we should do the same. I think your change is fine, but please add a new testcase that creates such an 0400 file and then unlinks it.

The -f makes me a bit anxious, I think we should add some more safeguards here. This would have been a good idea regardless, I wonder why I didn't do it back then already.

I just checked, busybox rm also supports -- to signal end of flags so maybe we should just add a -- before the self argument to ensure you can't pass filenames that will be interpreted as flags? Alternatively we could add a ./ to any non-absolute paths which may be a bit more portable.

@Rahix

Rahix commented Aug 20, 2026

Copy link
Copy Markdown
Owner

two pre-existing, unrelated failures in test_rglob_error reproduce identically on unmodified master

What were those about? I cannot reproduce them here and CI also seems happy... Can you open a new issue for this topic, please?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Path.unlink() should force-remove to avoid hanging on an interactive "remove write-protected file?" prompt

2 participants