Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Running requests in parallel from a zip archive can create race condition when unpacking the cacerts.pem file #5223

Closed
tru opened this issue Oct 8, 2019 · 8 comments · Fixed by #5707

Comments

@tru
Copy link

tru commented Oct 8, 2019

We ran into a really crazy case and I understand this is a edge case but it might be worth fixing.

We started to see this backtrace in our CI:

[2019-10-08T08:01:13.655Z]   File "/data/jenkins/postproc-asustor/204934530/conan/lib/conan-1.4.4-66-linux-py36.pyz/requests/api.py", line 75, in get
[2019-10-08T08:01:13.655Z]   File "/data/jenkins/postproc-asustor/204934530/conan/lib/conan-1.4.4-66-linux-py36.pyz/requests/api.py", line 60, in request
[2019-10-08T08:01:13.655Z]   File "/data/jenkins/postproc-asustor/204934530/conan/lib/conan-1.4.4-66-linux-py36.pyz/requests/sessions.py", line 533, in request
[2019-10-08T08:01:13.655Z]   File "/data/jenkins/postproc-asustor/204934530/conan/lib/conan-1.4.4-66-linux-py36.pyz/requests/sessions.py", line 646, in send
[2019-10-08T08:01:13.655Z]   File "/data/jenkins/postproc-asustor/204934530/conan/lib/conan-1.4.4-66-linux-py36.pyz/requests/adapters.py", line 416, in send
[2019-10-08T08:01:13.655Z]   File "/data/jenkins/postproc-asustor/204934530/conan/lib/conan-1.4.4-66-linux-py36.pyz/requests/adapters.py", line 224, in cert_verify
[2019-10-08T08:01:13.655Z]   File "/data/jenkins/postproc-asustor/204934530/conan/lib/conan-1.4.4-66-linux-py36.pyz/requests/utils.py", line 254, in extract_zipped_paths
[2019-10-08T08:01:13.655Z]   File "/usr/local/pyenv/versions/3.6.4/lib/python3.6/zipfile.py", line 1484, in extract
[2019-10-08T08:01:13.655Z]     return self._extract_member(member, path, pwd)
[2019-10-08T08:01:13.655Z]   File "/usr/local/pyenv/versions/3.6.4/lib/python3.6/zipfile.py", line 1547, in _extract_member
[2019-10-08T08:01:13.655Z]     os.makedirs(upperdirs)
[2019-10-08T08:01:13.655Z]   File "/usr/local/pyenv/versions/3.6.4/lib/python3.6/os.py", line 220, in makedirs
[2019-10-08T08:01:13.655Z]     mkdir(name, mode)
[2019-10-08T08:01:13.655Z] FileExistsError: [Errno 17] File exists: '/data/jenkins/postproc-asustor/204934530/_temp/certifi'

After a lot of confusion I think I understand this bug now. We distribute our python dependencies (including requests) as a pyz (created with zipapp) and the consumer in this case calls request inside a ThreadPool. requests then have logic to unpack cacerts.pem into the temp directory, but there is no race protection. So our parallel threads stepped on each other toes here when unpacking this file.

We solved this by a simple get call before starting the parallel invocation. But I think it might be worth fixing because it's very confusing.

Expected Result

Not having the cacert being overwritten :)

Actual Result

Exception above

Reproduction Steps

create a pyz with zipapp of requests and it's dependencies (certifi)

import requests                                                                                                                                                                                         from concurrent.futures import ThreadPoolExecutor

urls = ("https://github.com", "https://github.com", "https://github.com", "https://github.com")

def get(url):
    print(f"Getting {url}")
    requests.get(url)

with ThreadPoolExecutor(5) as pool:
      pool.map(get, urls)

note that since it's a race it can trigger or not trigger a lot.

System Information

$ python -m requests.help
{
  "chardet": {
    "version": "3.0.4"
  },
  "cryptography": {
    "version": "2.1.4"
  },
  "idna": {
    "version": "2.8"
  },
  "implementation": {
    "name": "CPython",
    "version": "3.6.8"
  },
  "platform": {
    "release": "4.19.72-microsoft-standard",
    "system": "Linux"
  },
  "pyOpenSSL": {
    "openssl_version": "1010100f",
    "version": "17.5.0"
  },
  "requests": {
    "version": "2.22.0"
  },
  "system_ssl": {
    "version": "1010100f"
  },
  "urllib3": {
    "version": "1.25.6"
  },
  "using_pyopenssl": true
}

This command is only available on Requests v2.16.4 and greater. Otherwise,
please provide some basic information about your system (Python version,
operating system, &c).

@gaborbernat
Copy link
Contributor

This can be reproduced with running pip in parallel too.

@sigmavirus24
Copy link
Contributor

Genuinely, it'd be best if there was a way not have to extract cacerts.pem from the zip file at all and be able to clean it up simply but that just doesn't exist. I've taken multiple approaches trying to fix this, and it's just not that simple to fix.

@sigmavirus24
Copy link
Contributor

To be more specific:

  • We could attempt to extract on first use to a unique directory, but if we end up in a multiprocessing situation or threaded situation that could mean N copies on disk for N parallel workers
  • We could extract per-request which just is awful
  • We could try doing it at import time and then cleaning up with atexit but that's horribly unreliable and could leave resources lying around
  • We could add a dependency on a lockfile library and use that to prevent any two workers from extracting to the same directory at the same time, or trying to overwrite what's being written. This last one seems most reasonable but last I checked there was no good, well-maintained lockfile library for Python that we could trust to be secure and up-to-date.

@gaborbernat
Copy link
Contributor

What about using an application data folder to extract this (no cleanup), and do the writing in parallel safe manner (mkdir with exist ok)? This is how virtualenv handles this and no one - complained yet for adding a few kb files in application data folder).

@sigmavirus24
Copy link
Contributor

We've had multiple complaints just about the fact that we write to a directory. I think previously we used a more predictable (between python interpreters) directory and that got people riled up too. In general, people don't want us creating files at all. The 100% best solution is the standard library ssl module accepting strings rather than requiring files on the system to verify certificates.

Also, can you clarify your definition of "application data folder" here? I think it's what we've done before and it opens us up to another utility in the user's space changing the file thus allowing something to corrupt the trust store (which is very bad)

@gaborbernat
Copy link
Contributor

A user write able temp folder is almost as easy to exploit as a user write able temp folder we have now. That being said I think for this issue the easiest solution is to put the extract operation in a try catch and silently swallow the exception if the file already exist. The race condition would no longer throw an exception and everything would work as expected.

gaborbernat added a commit to gaborbernat/requests that referenced this issue Dec 24, 2020
Extract also creates the folder hierarchy, however we do not need that,
the file itself being extracted to a temporary folder is good enough.
Instead we read the content of the zip and then write it. The write is
not locked but it's OK to update the same file multiple times given the
update operation will not alter the content of the file. By not creating
the folder hierarchy (default via extract) we no longer can run into the
problem of two parallel extracts both trying to create the folder
hierarchy without exists ok flag, and one must fail.

Resolves psf#5223.

Signed-off-by: Bernát Gábor <bgabor8@bloomberg.net>
@gaborbernat
Copy link
Contributor

#5707 works around the issue by keeping the current solution but without the racing condition imposed by the os makedirs.

@gaborbernat
Copy link
Contributor

@sigmavirus24 alternatively we can export this into a context manager handled temporary file on a per send call basis, what do you think? Though that can get messy with session connections.

gaborbernat added a commit to gaborbernat/requests that referenced this issue Dec 24, 2020
Extract also creates the folder hierarchy, however we do not need that,
the file itself being extracted to a temporary folder is good enough.
Instead we read the content of the zip and then write it. The write is
not locked but it's OK to update the same file multiple times given the
update operation will not alter the content of the file. By not creating
the folder hierarchy (default via extract) we no longer can run into the
problem of two parallel extracts both trying to create the folder
hierarchy without exists ok flag, and one must fail.

Resolves psf#5223.

Signed-off-by: Bernát Gábor <bgabor8@bloomberg.net>
gaborbernat added a commit to gaborbernat/requests that referenced this issue Dec 24, 2020
Extract also creates the folder hierarchy, however we do not need that,
the file itself being extracted to a temporary folder is good enough.
Instead we read the content of the zip and then write it. The write is
not locked but it's OK to update the same file multiple times given the
update operation will not alter the content of the file. By not creating
the folder hierarchy (default via extract) we no longer can run into the
problem of two parallel extracts both trying to create the folder
hierarchy without exists ok flag, and one must fail.

Resolves psf#5223.

Signed-off-by: Bernát Gábor <bgabor8@bloomberg.net>
gaborbernat added a commit to gaborbernat/requests that referenced this issue Dec 24, 2020
Extract also creates the folder hierarchy, however we do not need that,
the file itself being extracted to a temporary folder is good enough.
Instead we read the content of the zip and then write it. The write is
not locked but it's OK to update the same file multiple times given the
update operation will not alter the content of the file. By not creating
the folder hierarchy (default via extract) we no longer can run into the
problem of two parallel extracts both trying to create the folder
hierarchy without exists ok flag, and one must fail.

Resolves psf#5223.

Signed-off-by: Bernát Gábor <bgabor8@bloomberg.net>
gaborbernat added a commit to gaborbernat/requests that referenced this issue Dec 24, 2020
Extract also creates the folder hierarchy, however we do not need that,
the file itself being extracted to a temporary folder is good enough.
Instead we read the content of the zip and then write it. The write is
not locked but it's OK to update the same file multiple times given the
update operation will not alter the content of the file. By not creating
the folder hierarchy (default via extract) we no longer can run into the
problem of two parallel extracts both trying to create the folder
hierarchy without exists ok flag, and one must fail.

Resolves psf#5223.

Signed-off-by: Bernát Gábor <bgabor8@bloomberg.net>
sigmavirus24 pushed a commit that referenced this issue Jul 7, 2021
#5707)

Extract also creates the folder hierarchy, however we do not need that,
the file itself being extracted to a temporary folder is good enough.
Instead we read the content of the zip and then write it. The write is
not locked but it's OK to update the same file multiple times given the
update operation will not alter the content of the file. By not creating
the folder hierarchy (default via extract) we no longer can run into the
problem of two parallel extracts both trying to create the folder
hierarchy without exists ok flag, and one must fail.

Resolves #5223.

Signed-off-by: Bernát Gábor <bgabor8@bloomberg.net>
jonfung-scale pushed a commit to jonfung-scale/jonfung-requests that referenced this issue Jul 14, 2021
psf#5707)

Extract also creates the folder hierarchy, however we do not need that,
the file itself being extracted to a temporary folder is good enough.
Instead we read the content of the zip and then write it. The write is
not locked but it's OK to update the same file multiple times given the
update operation will not alter the content of the file. By not creating
the folder hierarchy (default via extract) we no longer can run into the
problem of two parallel extracts both trying to create the folder
hierarchy without exists ok flag, and one must fail.

Resolves psf#5223.

Signed-off-by: Bernát Gábor <bgabor8@bloomberg.net>
jonfung-scale pushed a commit to jonfung-scale/jonfung-requests that referenced this issue Jul 14, 2021
psf#5707)

Extract also creates the folder hierarchy, however we do not need that,
the file itself being extracted to a temporary folder is good enough.
Instead we read the content of the zip and then write it. The write is
not locked but it's OK to update the same file multiple times given the
update operation will not alter the content of the file. By not creating
the folder hierarchy (default via extract) we no longer can run into the
problem of two parallel extracts both trying to create the folder
hierarchy without exists ok flag, and one must fail.

Resolves psf#5223.

Signed-off-by: Bernát Gábor <bgabor8@bloomberg.net>
jonfung-scale pushed a commit to jonfung-scale/jonfung-requests that referenced this issue Jul 15, 2021
psf#5707)

Extract also creates the folder hierarchy, however we do not need that,
the file itself being extracted to a temporary folder is good enough.
Instead we read the content of the zip and then write it. The write is
not locked but it's OK to update the same file multiple times given the
update operation will not alter the content of the file. By not creating
the folder hierarchy (default via extract) we no longer can run into the
problem of two parallel extracts both trying to create the folder
hierarchy without exists ok flag, and one must fail.

Resolves psf#5223.

Signed-off-by: Bernát Gábor <bgabor8@bloomberg.net>
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Oct 5, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants