Skip to content

test_eperm_on_both_names_falls_back_too is missing @needs_unprivileged_posix, and on Windows it does not fail — it spins for ~68 hours #2392

Description

@wstczyw

Summary

tests/test_config_unreadable_rewrite.py::test_eperm_on_both_names_falls_back_too (added in #2356, f3f23d1) is the only permission-injection test in that file without the @needs_unprivileged_posix guard. Its ten siblings have it, including its immediate neighbour test_erofs_on_both_names_falls_back_rather_than_raising, which is the same shape with a different errno.

On Windows the unguarded test therefore runs, and it does not fail — it becomes CPU-bound and does not terminate. pytest on the full suite never finishes. I left one orphaned run going and it accumulated 13,527 seconds of CPU without converging.

Why it spins

The test monkeypatches os.open to raise OSError(EPERM) for every basename starting with config.json. or .config.json.. MempalaceConfig.set_backend falls back to:

fd, tmp = tempfile.mkstemp(dir=str(path.parent), prefix=f".{path.name}.", suffix=".tmp")

so every candidate name mkstemp generates matches the patch. CPython's tempfile._mkstemp_inner has a Windows-only branch that reads PermissionError as "a directory of that name already exists" and continues the loop instead of raising:

except PermissionError:
    # This exception is thrown when a directory with the chosen name
    # already exists on windows.
    if (_os.name == 'nt' and _os.path.isdir(dir) and _os.access(dir, _os.W_OK)):
        continue
    else:
        raise

The loop is for seq in range(TMP_MAX), and on this build tempfile.TMP_MAX is 2147483647.

Reproduction (no MemPalace involved)

import errno, os, sys, tempfile, threading, time

print(f"python={sys.version.split()[0]} os.name={os.name} TMP_MAX={tempfile.TMP_MAX}", flush=True)

d = tempfile.mkdtemp()
real_open = os.open
attempts = 0

def deny(path, *args, **kwargs):
    global attempts
    if os.path.basename(str(path)).startswith(".probe."):
        attempts += 1
        raise OSError(errno.EPERM, "Operation not permitted")
    return real_open(path, *args, **kwargs)

os.open = deny
done = threading.Event()

def run():
    try:
        tempfile.mkstemp(dir=d, prefix=".probe.", suffix=".tmp")
    except BaseException as exc:
        print(f"raised {type(exc).__name__}: {exc}", flush=True)
    done.set()

threading.Thread(target=run, daemon=True).start()
if not done.wait(timeout=20):
    print(f"still spinning after 20s; os.open called {attempts} times", flush=True)
os._exit(0)

Output here:

python=3.12.14 os.name=nt TMP_MAX=2147483647
still spinning after 20s; os.open called 175954 times

At ~8,800 attempts/second, 2,147,483,647 attempts is roughly 68 hours. It is not an infinite loop in the strict sense, but for a test run it may as well be.

Environment

Suggested fix

Add @needs_unprivileged_posix to test_eperm_on_both_names_falls_back_too, matching its EROFS sibling. The guard already reads os.name == "nt" or geteuid() == 0, so this only restores the intent that the rest of the file already follows.

Separately, it may be worth bounding the mkstemp fallback in config.py (the retry count is the stdlib's, not yours, but the prefix collision is what routes every attempt into it).

Workaround for anyone running the suite on Windows today

pytest --ignore=tests/test_config_unreadable_rewrite.py

With that ignore the suite completes in 6m16s here (4226 passed, 364 skipped).

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1higharea/ciCI/CD and workflowsarea/windowsWindows-specific bugs and compatibilitybugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions