From a7483ccd8dc4c0a93bace88f4c5263cb68da08b7 Mon Sep 17 00:00:00 2001 From: Viswa Marepalli Date: Tue, 27 Oct 2026 21:40:07 +0000 Subject: [PATCH] Bind ssl lazily after no_ssl so boto3 imports no_ssl makes `import ssl` fail while urllib3.util.ssl_ and similar modules load, and their fallback code can leave the name `ssl` unbound. botocore does `from urllib3.util.ssl_ import ssl`, so importing boto3 failed. After no_ssl, add a module __getattr__ that imports ssl on first access. Ported from cloudflare/workers-py#292 (f3e7851). Co-Authored-By: Claude Opus 5.5 --- workers/src/_crosswind_patches/entropy.py | 40 +++++++++++++++++--- workers/tests/e2e/entropy/src/boto3_entry.py | 10 +++++ workers/tests/e2e/test_entropy_e2e.py | 22 +++++++++++ 3 files changed, 67 insertions(+), 5 deletions(-) create mode 100644 workers/tests/e2e/entropy/src/boto3_entry.py diff --git a/workers/src/_crosswind_patches/entropy.py b/workers/src/_crosswind_patches/entropy.py index df87953..cd5e8ed 100644 --- a/workers/src/_crosswind_patches/entropy.py +++ b/workers/src/_crosswind_patches/entropy.py @@ -159,13 +159,42 @@ def find_spec( return None +def add_lazy_ssl_attribute(module: ModuleType) -> None: + """ + When `import ssl` fails inside a module, its fallback code can leave the name + `ssl` unbound. Other packages may still import it from there, for example + botocore does `from urllib3.util.ssl_ import ssl`. Bind it lazily on first + access. + + TODO: Drop this when https://github.com/boto/botocore/pull/3640 is merged. + """ + if "ssl" in module.__dict__: + return + + orig_getattr = module.__dict__.get("__getattr__") + + def lazy_getattr(name: str): + if name == "ssl": + import ssl + + setattr(module, "ssl", ssl) # noqa: B010 + return ssl + if orig_getattr is not None: + return orig_getattr(name) + raise AttributeError(f"module {module.__name__!r} has no attribute {name!r}") + + # setattr, because ty only sees the declared ModuleType attributes. + setattr(module, "__getattr__", lazy_getattr) # noqa: B010 + + @contextmanager -def no_ssl(): +def no_ssl(module: ModuleType): """ Various packages will call ssl.create_default_context() at top level which uses entropy if they can import ssl. By temporarily making importing ssl raise an import error, we exercise the workaround code and so avoid the entropy calls. - After, we put the ssl module back to the normal value. + After, we put the ssl module back to the normal value and make `module.ssl` + available again in case the workaround code left it unbound. """ try: f = NoSslFinder() @@ -176,26 +205,27 @@ def no_ssl(): sys.meta_path.remove(f) if ssl: sys.modules["ssl"] = ssl + add_lazy_ssl_attribute(module) @register_exec_patch("aiohttp.connector") @contextmanager def aiohttp_connector_context(module): - with no_ssl(): + with no_ssl(module): yield @register_exec_patch("requests.adapters") @contextmanager def requests_adapters_context(module): - with no_ssl(): + with no_ssl(module): yield @register_exec_patch("urllib3.util.ssl_") @contextmanager def urllib3_util_ssl__context(module): - with no_ssl(): + with no_ssl(module): yield diff --git a/workers/tests/e2e/entropy/src/boto3_entry.py b/workers/tests/e2e/entropy/src/boto3_entry.py new file mode 100644 index 0000000..2329451 --- /dev/null +++ b/workers/tests/e2e/entropy/src/boto3_entry.py @@ -0,0 +1,10 @@ +# botocore does `from urllib3.util.ssl_ import ssl`, but crosswind imports +# urllib3.util.ssl_ without ssl to avoid entropy calls, which leaves it unbound. +import boto3 + +from workers import Response, WorkerEntrypoint + + +class Default(WorkerEntrypoint): + async def fetch(self, request): + return Response(boto3.__name__) diff --git a/workers/tests/e2e/test_entropy_e2e.py b/workers/tests/e2e/test_entropy_e2e.py index 5bf99ad..247fc1e 100644 --- a/workers/tests/e2e/test_entropy_e2e.py +++ b/workers/tests/e2e/test_entropy_e2e.py @@ -23,3 +23,25 @@ def test_numpy_and_pydantic_import_at_top_level( name, value = body.split(" ") assert name == "ok" assert 0 <= float(value) < 1 + + +@TARGETS +def test_boto3_import_at_top_level(tmp_path: Path, date: str, version: str) -> None: + # Before 3.14 the flag hands patching to crosswind's .pth hook, as in + # test_fastapi_app_answers. + flags = ["python_process_pth_files"] if version != "3.14" else [] + make_project( + tmp_path, + Path(__file__).parent / "entropy", + { + "main": "src/boto3_entry.py", + "compatibility_date": date, + "compatibility_flags": ["python_workers", *flags], + }, + ("boto3",), + ) + + with wynd_dev(tmp_path) as server: + _, body = server.get("/") + + assert body == "boto3"