Repository: pex-tool/pex

Issue:
Support scrubbing all PEX_* env vars
In order to scrub PEX_* env vars to keep a PEX hermetic, someone launching a PEX must either clear the environment which takes care - many things need PATH / HOME, etc. If either a PEX could be built with a PexInfo setting to ignore PEX_* env vars or the Pex runtime provided away to turn them off, or both, then locking down a PEX would be easier.

See https://github.com/pantsbuild/scie-pants/issues/3 for motivation.


Discussion:

Resolving patch:
diff --git a/pex/variables.py b/pex/variables.py
--- a/pex/variables.py
+++ b/pex/variables.py
@@ -22,7 +22,7 @@
 from pex.venv.bin_path import BinPath
 
 if TYPE_CHECKING:
-    from typing import Any, Callable, Dict, Iterator, Optional, Tuple, Type, TypeVar, Union
+    from typing import Any, Callable, Dict, Iterator, Mapping, Optional, Tuple, Type, TypeVar, Union
 
     _O = TypeVar("_O")
     _P = TypeVar("_P")
@@ -210,15 +210,40 @@ def _get_kv(cls, variable):
         if len(list(filter(None, kv))) == 2:
             return kv
 
+    @staticmethod
+    def _maybe_get_bool_var(
+        name,  # type: str
+        env,  # type: Mapping[str, Optional[str]]
+    ):
+        # type: (...) -> Optional[bool]
+        value = env.get(name, None)
+        if value is None:
+            return None
+        if value.lower() in ("0", "false"):
+            return False
+        if value.lower() in ("1", "true"):
+            return True
+        raise ValueError(
+            "Invalid bool value for {name}, must be 0/1/false/true, got {value!r}".format(
+                name=name, value=value
+            )
+        )
+
     def __init__(self, environ=None, rc=None):
-        # type: (Optional[Dict[str, str]], Optional[str]) -> None
-        self._environ = (
-            environ.copy() if environ is not None else os.environ.copy()
-        )  # type: Dict[str, str]
-        if not self.PEX_IGNORE_RCFILES:
-            rc_values = self.from_rc(rc).copy()
-            rc_values.update(self._environ)
-            self._environ = rc_values
+        # type: (Optional[Mapping[str, str]], Optional[str]) -> None
+        env = environ if environ is not None else os.environ
+        if self._maybe_get_bool_var("PEX_DISABLE_VARIABLES", env) is True:
+            self._environ = {
+                key: value
+                for key, value in env.items()
+                if key == "PEX_DISABLE_VARIABLES" or not key.startswith("PEX_")
+            }
+        else:
+            self._environ = dict(env)
+            if not self.PEX_IGNORE_RCFILES:
+                rc_values = self.from_rc(rc).copy()
+                rc_values.update(self._environ)
+                self._environ = rc_values
 
         if "PEX_ALWAYS_CACHE" in self._environ:
             pex_warnings.warn(
@@ -260,14 +285,7 @@ def _get_string(self, variable):
 
     def _maybe_get_bool(self, variable):
         # type: (str) -> Optional[bool]
-        value = self._maybe_get_string(variable)
-        if value is None:
-            return None
-        if value.lower() in ("0", "false"):
-            return False
-        if value.lower() in ("1", "true"):
-            return True
-        die("Invalid value for %s, must be 0/1/false/true, got %r" % (variable, value))
+        return self._maybe_get_bool_var(variable, self._environ)
 
     def _get_bool(self, variable):
         # type: (str) -> bool
@@ -332,16 +350,37 @@ def patch(self, **kw):
         present. The rest will be added to the environment or else updated if already present in
         the environment.
         """
+        disable_env = self._maybe_get_bool_var("PEX_DISABLE_VARIABLES", kw)
+
         old_environ = self._environ
         self._environ = self._environ.copy()
+        if disable_env:
+            for k in list(self._environ):
+                if k != "PEX_DISABLE_VARIABLES" and k.startswith("PEX_"):
+                    self._environ.pop(k)
         for k, v in kw.items():
             if v is None:
                 self._environ.pop(k, None)
+            elif disable_env and k != "PEX_DISABLE_VARIABLES" and k.startswith("PEX_"):
+                self._environ.pop(k, None)
             else:
                 self._environ[k] = v
+
         yield self._environ
         self._environ = old_environ
 
+    @defaulted_property(default=False)
+    def PEX_DISABLE_VARIABLES(self):
+        # type: () -> bool
+        """Boolean.
+
+        Disable reading of all PEX_* variables (except this one) from all sources. Both PEX_*
+        environment variables and PEX_* variables sources from pexrc files will be ignored.
+
+        This can be used to lock down the function of a PEX.
+        """
+        return self._get_bool("PEX_DISABLE_VARIABLES")
+
     @property
     def PEX(self):
         # type: () -> Optional[str]


Test patch:
diff --git a/tests/test_variables.py b/tests/test_variables.py
--- a/tests/test_variables.py
+++ b/tests/test_variables.py
@@ -56,7 +56,7 @@ def test_pex_bool_variables():
         assert Variables(environ={"HERE": value})._get_bool("HERE") is False
     for value in ("1", "TrUe", "true"):
         assert Variables(environ={"HERE": value})._get_bool("HERE") is True
-    with pytest.raises(SystemExit):
+    with pytest.raises(ValueError):
         Variables(environ={"HERE": "garbage"})._get_bool("HERE")
 
     # end to end
@@ -81,6 +81,38 @@ def test_pex_get_int():
         assert Variables(environ={"HELLO": "welp"})._get_int("HELLO")
 
 
+def test_disable():
+    v = Variables(
+        environ=dict(
+            PEX_DISABLE_VARIABLES="1",
+            PEX_VERBOSE="42",
+            PEX_PATH="other.pex:more.pex",
+            PEX_SCRIPT="other",
+        )
+    )
+    assert v.PEX_DISABLE_VARIABLES is True
+    assert 0 == v.PEX_VERBOSE
+    assert () == v.PEX_PATH
+    assert v.PEX_SCRIPT is None
+    assert v.PEX_EMIT_WARNINGS is None
+
+
+def test_disable_patch():
+    v = Variables(environ=dict(PEX_VERBOSE="42", PEX_PATH="other.pex:more.pex", PEX_SCRIPT="other"))
+    assert v.PEX_DISABLE_VARIABLES is False
+    assert 42 == v.PEX_VERBOSE
+    assert ("other.pex", "more.pex") == v.PEX_PATH
+    assert "other" == v.PEX_SCRIPT
+    assert v.PEX_EMIT_WARNINGS is None
+
+    with v.patch(PEX_DISABLE_VARIABLES="1", PEX_EMIT_WARNINGS="1"):
+        assert v.PEX_DISABLE_VARIABLES is True
+        assert 0 == v.PEX_VERBOSE
+        assert () == v.PEX_PATH
+        assert v.PEX_SCRIPT is None
+        assert v.PEX_EMIT_WARNINGS is None
+
+
 def assert_pex_vars_hermetic():
     # type: () -> None
     v = Variables()
@@ -105,6 +137,12 @@ def test_pex_vars_hermetic():
         assert_pex_vars_hermetic()
 
 
+def test_pex_vars_disabled_hermetic():
+    # type: () -> None
+    with environment_as(PEX_DISABLE_VARIABLES="True"):
+        assert_pex_vars_hermetic()
+
+
 def test_pex_get_kv():
     # type: () -> None
     v = Variables(environ={})