You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Running the Python test suite leaks .pem files in /tmp #93353
The test suite is not marked as failed when it leaks temporary files (in /tmp)
Moreover, test_tools enters an unlimited loop and fills the $TMPDIR directory if the $TMPDIR is a sub-directory of the Python source code directory. Example:
Running test_freeze_simple_script() of test_tools copies TMP/ into TMP/TMP/ and then into TMP/TMP/TMP/, etc. Quickly, it fills TMP/ with a "loop" of files :-)
The problem comes from pip bootstrap when running the test_with_pip() test of test_venv.
It seems like pip creates a .pem file in the temporary directory (like /tmp), but only when pip is imported from a ZIP file, which is the case when I run get-pip.py: https://bootstrap.pypa.io/get-pip.py
Lib/importlib/resources/_common.py, line 84, in _tempfile (suffix='cacert.pem', fd=4, raw_path='/home/vstinner/python/main/TMP/tmpv7fwzaoocacert.pem')
If Python exits normally, the garbage collector deletes the temporary PEM file. When running ensurepip / get-pip.py, it's not deleted. I don't know why.
importlib.resources only creates a temporary file if pip is imported from a ZIP file.
OpenSSL requires a filename to a PEM file. certifi uses importlib.resources to create a temporary named file if pip._vendor.certifi is imported from a ZIP file. That's convenient.
The problem is more that the pip._vendor.certifi.core._CACERT_CTX context manager which keeps the temporary file alive until it's destroyed. Maybe sometimes _CACERT_CTX is part of a complex reference cycle, it's never destroyed, and so the temporary file is not deleted.
I used a shell script similar to #93353 (comment) and I ran manually a bisection on the test list (./python -m test --list-tests) using a script taking a random.sample() of half of tests. I repeated the operation until I found a single test file, then I looked at the code.
The leaked files was reported by more and more owners of buildbot workers.
Once the PEM issue is solved, my plan is to enhance test.regrtest to do something similar than the shell script: define TMPFILE env var. Maybe in the main process spawning test processes.
For pip/certifi, I hacked get-pip.py to add breakpoint() and I followed the code flow. It's not easy to identify which part of pip created the file. It's not just an import. It's an import + call to where() function, done in the module body that I failed to find which module. Maybe requests. It would be nice to be able to postpone when where() is called to workaround the issue: only call it when a filename to cacert.pem is needed.
certifi.where() uses importlib.resources to create a temporary file using a context manager.
logging is imported by pip
logging calls os.register_at_fork() which keeps the whole logging namespace alive
somehow, the logging module keeps the whole pip package (!) and all of its sub-modules (!) alive until Python clears os.register_at_fork(): that happens VERY LATE, like the last function call before the last GC collection.
Python clears os.register_at_fork() callbacks: the context manager finally: block is called as expected....
Oops, os.remove is None at this point, the call fails.
Fixing importlib.resources is simple: keep a reference to os.remove() in importlib.resources implementation to make sure that we can remove the file very late during Python finalization.
This problem is tricky because:
importlib.resources only creates a named temporary file if pip is imported from a ZIP file. When pip is installed on disk as a file, the bug is gone.
the code is valid.
certifi relies on the garbage collector to gently calls the context manager finalizer which removes the temporary file.
Python doesn't log any error when a bug occurs very late during Python finalization.
The commit e566ce5 relies on an environment variable to create unique temp directories per instance. The approach does not work on WASI for two reasons:
WASI runtimes do not pass environment variables to WASM programs. python.wasm process does not see the TMPDIR variable.
wasmtime uses an explicit, sandboxed mapping of host directories to runtime directories (sort of a chroot, but better). The TMPDIR outside would not be the same as the TMPDIR inside.
The initial issue (leaked PEM files) has been fixed.
regrtest now detects leaked temporary files in the main branch. I don't want to backport this change since it causes some CI issues, and I'm still fixing regrtest. I have a few more changes to enhance regrtest which will use this issue number, but IMO the issue can be closed since the initial issue has been fixed.
Activity
vstinner commentedon May 30, 2022
The problem comes from pip bootstrap when running the test_with_pip() test of test_venv.
It seems like pip creates a .pem file in the temporary directory (like /tmp), but only when pip is imported from a ZIP file, which is the case when I run get-pip.py: https://bootstrap.pypa.io/get-pip.py
vstinner commentedon May 30, 2022
I can reproduced the issue in the 3.9, 3.10, 3.11 and main branches (I didn't test older branches).
It seems to be a change between pip 20.2.3 and pip 21.1.
vstinner commentedon May 30, 2022
In get-pip.py,
from pip._internal.commands.install import InstallCommandis enough to create the.pemfile.vstinner commentedon May 30, 2022
Shell script to reproduce the issue:
Output:
vstinner commentedon May 30, 2022
The PEM file is created indirectly by importlib.resources on
import pip._vendor.requests.It's created by this code:
Debugger:
If Python exits normally, the garbage collector deletes the temporary PEM file. When running ensurepip / get-pip.py, it's not deleted. I don't know why.
importlib.resources only creates a temporary file if pip is imported from a ZIP file.
vstinner commentedon May 30, 2022
Workaround to manually delete the temporaryfile:
vstinner commentedon May 30, 2022
OpenSSL requires a filename to a PEM file. certifi uses importlib.resources to create a temporary named file if pip._vendor.certifi is imported from a ZIP file. That's convenient.
The problem is more that the
pip._vendor.certifi.core._CACERT_CTXcontext manager which keeps the temporary file alive until it's destroyed. Maybe sometimes_CACERT_CTXis part of a complex reference cycle, it's never destroyed, and so the temporary file is not deleted.serhiy-storchaka commentedon May 31, 2022
Nice. How did you find this?
vstinner commentedon May 31, 2022
I used a shell script similar to #93353 (comment) and I ran manually a bisection on the test list (./python -m test --list-tests) using a script taking a random.sample() of half of tests. I repeated the operation until I found a single test file, then I looked at the code.
The leaked files was reported by more and more owners of buildbot workers.
Once the PEM issue is solved, my plan is to enhance test.regrtest to do something similar than the shell script: define TMPFILE env var. Maybe in the main process spawning test processes.
vstinner commentedon May 31, 2022
For pip/certifi, I hacked get-pip.py to add breakpoint() and I followed the code flow. It's not easy to identify which part of pip created the file. It's not just an import. It's an import + call to where() function, done in the module body that I failed to find which module. Maybe requests. It would be nice to be able to postpone when where() is called to workaround the issue: only call it when a filename to cacert.pem is needed.
vstinner commentedon May 31, 2022
Reproducer without pip nor certifi, attached run.py and bug.py scripts:
The expected output is an empty list in run.py (parent process). bug.py never calls _tempfile() finally block.
I'm still bisecting the issue. It smells like logging prevents deleting an object somehow.
vstinner commentedon May 31, 2022
Ok, this problem is quite complicated:
finally:block is called as expected....os.removeisNoneat this point, the call fails.Fixing importlib.resources is simple: keep a reference to os.remove() in importlib.resources implementation to make sure that we can remove the file very late during Python finalization.
This problem is tricky because:
I'm working on a fix for importlib.resources.
12 remaining items
gh-93353: regrtest checks for leaked temporary files (#93776)
tiran commentedon Jun 14, 2022
PR GH-93776 broke testing on WASI:
tiran commentedon Jun 14, 2022
The commit e566ce5 relies on an environment variable to create unique temp directories per instance. The approach does not work on WASI for two reasons:
python.wasmprocess does not see theTMPDIRvariable.TMPDIRoutside would not be the same as theTMPDIRinside.vstinner commentedon Jun 14, 2022
Ah, so this feature is not supported on WASI. Can you please test #93810 on WASI?
gh-93353: Fix regrtest for -jN with N >= 2 (GH-93813)
vstinner commentedon Jun 15, 2022
The initial issue (leaked PEM files) has been fixed.
regrtest now detects leaked temporary files in the main branch. I don't want to backport this change since it causes some CI issues, and I'm still fixing regrtest. I have a few more changes to enhance regrtest which will use this issue number, but IMO the issue can be closed since the initial issue has been fixed.
gh-93353: regrtest supports checking tmp files with -j2 (#93909)
vstinner commentedon Jun 16, 2022
Follow-up: issue #93919 "On Windows, test_distutils leaks a temporary file: Microsoft".
Revert 30935 defer bpo45162 to 312 (#94285)
pythongh-93353: Fix importlib.resources._tempfile() finalizer (python…
pythongh-93353: regrtest checks for leaked temporary files (python#93776
pythongh-93353: Fix regrtest for -jN with N >= 2 (pythonGH-93813)
pythongh-93353: regrtest supports checking tmp files with -j2 (python…
[3.11] gh-108822: Backport libregrtest changes from the main branch (#…