Skip to content

Commit b1d100b

Browse files
committed
test(providers): serialize providers_v2 mutation with a suite-wide guard
The clone e2e's per-fixture lock only coordinated fixtures that acquired it; other xdist workers hit the same gateway without it and could observe the transiently-enabled providers_v2_enabled global during their own sandbox creation (CWE-362). Add an autouse readers-writer guard in conftest: every test holds a shared lock on the gateway config, and a test marked exclusive_gateway_config holds an exclusive lock. Mark the clone test exclusive so no other worker is mid-test while it enables and restores the gateway-global setting. Exact prior-value restoration is retained. Refs #1769 Signed-off-by: Russell Bryant <rbryant@redhat.com>
1 parent dc397b8 commit b1d100b

2 files changed

Lines changed: 77 additions & 50 deletions

File tree

e2e/python/conftest.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from __future__ import annotations
55

6+
import fcntl
67
import os
78
import time
89
from typing import TYPE_CHECKING
@@ -16,6 +17,38 @@
1617
from collections.abc import Callable, Iterator
1718

1819

20+
def pytest_configure(config: pytest.Config) -> None:
21+
config.addinivalue_line(
22+
"markers",
23+
"exclusive_gateway_config: hold an exclusive lock on gateway-global "
24+
"config so no other xdist worker observes a transient global setting",
25+
)
26+
27+
28+
@pytest.fixture(autouse=True)
29+
def _gateway_config_guard(
30+
request: pytest.FixtureRequest,
31+
tmp_path_factory: pytest.TempPathFactory,
32+
) -> Iterator[None]:
33+
"""Readers-writer guard over gateway-global config mutations.
34+
35+
The e2e gateway is shared across xdist workers, so a test that flips a
36+
gateway-global setting can leak that transient value into another worker's
37+
sandbox creation. Every test holds a shared lock by default; a test marked
38+
``exclusive_gateway_config`` holds an exclusive lock, so while it mutates and
39+
restores the setting no other worker is mid-test and none can observe the
40+
transient value. The lock file lives in the run's shared base temp dir
41+
(``getbasetemp().parent``), which is common to all xdist workers.
42+
"""
43+
lock_path = tmp_path_factory.getbasetemp().parent / "gateway-config.lock"
44+
exclusive = (
45+
request.node.get_closest_marker("exclusive_gateway_config") is not None
46+
)
47+
with lock_path.open("w") as lock_file:
48+
fcntl.flock(lock_file, fcntl.LOCK_EX if exclusive else fcntl.LOCK_SH)
49+
yield
50+
51+
1952
@pytest.fixture(scope="session")
2053
def cluster_name() -> str | None:
2154
return os.environ.get("OPENSHELL_GATEWAY")

e2e/python/test_sandbox_providers.py

Lines changed: 44 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111

1212
from __future__ import annotations
1313

14-
import fcntl
1514
import time
1615
from contextlib import contextmanager
1716
from typing import TYPE_CHECKING
@@ -103,69 +102,63 @@ def _delete_provider(stub: object, name: str) -> None:
103102
@pytest.fixture
104103
def providers_v2_enabled(
105104
sandbox_client: SandboxClient,
106-
tmp_path_factory: pytest.TempPathFactory,
105+
_gateway_config_guard: None,
107106
) -> Iterator[None]:
108107
"""Enable the gateway-global ``providers_v2_enabled`` opt-in for one test.
109108
110109
Composing a provider's network policy onto a sandbox is gated behind this
111110
setting, which defaults off; the built-in github profile's git-transport
112111
rules only reach the sandbox with it enabled.
113112
114-
The setting is gateway-global, so this mutates shared state. To keep it safe
115-
on an existing or shared gateway and under parallel workers:
116-
117-
- The mutation is serialized across xdist workers with an exclusive file lock
118-
on the run's shared base temp dir (``getbasetemp().parent``), held for the
119-
whole test so the read-modify-restore cannot interleave.
120-
- The exact prior value (or absence) is captured via ``GetGatewayConfig`` and
121-
restored in ``finally``, leaving the gateway as it was found.
122-
123-
``global`` is a Python keyword, so it is passed through a dict expansion.
113+
The setting is gateway-global. Exclusivity against other xdist workers is
114+
provided by the ``exclusive_gateway_config`` marker plus the autouse
115+
``_gateway_config_guard`` guard (see conftest): no concurrent worker is
116+
mid-test while this fixture mutates and restores the setting, so none can
117+
observe the transient value. Depending on the guard here also orders the
118+
exclusive lock acquisition before the mutation.
119+
120+
``GetGatewayConfig`` returns known keys even when unset, with an empty
121+
``SettingValue`` (no populated oneof), so the setting is treated as present
122+
only when its value oneof is set; otherwise restore is a delete. ``global``
123+
is a Python keyword, so it is passed through a dict expansion.
124124
"""
125125
stub = sandbox_client._stub
126126
key = "providers_v2_enabled"
127-
lock_path = tmp_path_factory.getbasetemp().parent / "providers-v2-setting.lock"
128-
with lock_path.open("w") as lock_file:
129-
fcntl.flock(lock_file, fcntl.LOCK_EX)
130-
131-
# GetGatewayConfig returns known keys even when unset, with an empty
132-
# SettingValue (no populated oneof). Only treat the setting as explicitly
133-
# present when its value oneof is actually set; otherwise restore = delete.
134-
config = stub.GetGatewayConfig(sandbox_pb2.GetGatewayConfigRequest())
135-
prior_value = sandbox_pb2.SettingValue()
136-
had_prior = (
137-
key in config.settings
138-
and config.settings[key].WhichOneof("value") is not None
127+
config = stub.GetGatewayConfig(sandbox_pb2.GetGatewayConfigRequest())
128+
prior_value = sandbox_pb2.SettingValue()
129+
had_prior = (
130+
key in config.settings
131+
and config.settings[key].WhichOneof("value") is not None
132+
)
133+
if had_prior:
134+
prior_value.CopyFrom(config.settings[key])
135+
136+
stub.UpdateConfig(
137+
openshell_pb2.UpdateConfigRequest(
138+
setting_key=key,
139+
setting_value=sandbox_pb2.SettingValue(bool_value=True),
140+
**{"global": True},
139141
)
142+
)
143+
try:
144+
yield
145+
finally:
140146
if had_prior:
141-
prior_value.CopyFrom(config.settings[key])
142-
143-
stub.UpdateConfig(
144-
openshell_pb2.UpdateConfigRequest(
145-
setting_key=key,
146-
setting_value=sandbox_pb2.SettingValue(bool_value=True),
147-
**{"global": True},
148-
)
149-
)
150-
try:
151-
yield
152-
finally:
153-
if had_prior:
154-
stub.UpdateConfig(
155-
openshell_pb2.UpdateConfigRequest(
156-
setting_key=key,
157-
setting_value=prior_value,
158-
**{"global": True},
159-
)
147+
stub.UpdateConfig(
148+
openshell_pb2.UpdateConfigRequest(
149+
setting_key=key,
150+
setting_value=prior_value,
151+
**{"global": True},
160152
)
161-
else:
162-
stub.UpdateConfig(
163-
openshell_pb2.UpdateConfigRequest(
164-
setting_key=key,
165-
delete_setting=True,
166-
**{"global": True},
167-
)
153+
)
154+
else:
155+
stub.UpdateConfig(
156+
openshell_pb2.UpdateConfigRequest(
157+
setting_key=key,
158+
delete_setting=True,
159+
**{"global": True},
168160
)
161+
)
169162

170163

171164
# ===========================================================================
@@ -560,6 +553,7 @@ def test_update_provider_rejects_type_change(
560553
# ===========================================================================
561554

562555

556+
@pytest.mark.exclusive_gateway_config
563557
@pytest.mark.usefixtures("providers_v2_enabled")
564558
def test_github_provider_allows_https_git_clone(
565559
sandbox: Callable[..., Sandbox],

0 commit comments

Comments
 (0)