Repository: twisted/twisted

Issue:
spawnProcess() passes incorrect environment to subprocess when env=None and posix_spawnp() is used
[Documentation on reactor.spawnProcess](https://docs.twisted.org/en/stable/api/twisted.internet.interfaces.IReactorProcess.html) says the following about env parameter:

```env is None:  On POSIX: pass os.environ```

However, twisted has [this code](https://github.com/twisted/twisted/blob/68f112f1eecb4613a3b678314a5479464c184ab4/src/twisted/internet/process.py#L881) in the code path leading to a call to posix_spawnp().

```
if environment is None:
    environment = {}
```

This leads to a subprocess being initialized with empty environment even though `os.environ` was expected.
 
**Describe how to cause this behavior**

There's a PR with automated tests added to Twisted.

**Describe the correct behavior you'd like to see**

Subprocess having parent process environment when invoked via `reactor.spawnProcess(..., env=None)`.

**Testing environment**

 - Operating System and Version; 
   - Debian 12
 - Twisted version: 23.10.0
 - Reactor: default on Linux

**Additional context**
Probably a regression since 23.8.0 when posix_spawnp was enabled.


Comments:

Patch:
diff --git a/src/twisted/internet/process.py b/src/twisted/internet/process.py
index ef3b88d9f1..ff7684e358 100644
--- a/src/twisted/internet/process.py
+++ b/src/twisted/internet/process.py
@@ -879,7 +879,7 @@ class Process(_BaseProcess):
             else:
                 fdState.append((eachFD, isCloseOnExec))
         if environment is None:
-            environment = {}
+            environment = os.environ
 
         setSigDef = [
             everySignal
diff --git a/src/twisted/newsfragments/12068.bugfix b/src/twisted/newsfragments/12068.bugfix
new file mode 100644
index 0000000000..584d3ed944
--- /dev/null
+++ b/src/twisted/newsfragments/12068.bugfix
@@ -0,0 +1,1 @@
+twisted.internet.process.Process, used by ``reactor.spawnProcess``, now copies the parent environment when the `env=None` argument is passed on Posix systems and ``os.posix_spawnp`` is used internally.


Test patch:
diff --git a/src/twisted/internet/test/test_process.py b/src/twisted/internet/test/test_process.py
index 0b8cdee750..d1d930cca3 100644
--- a/src/twisted/internet/test/test_process.py
+++ b/src/twisted/internet/test/test_process.py
@@ -29,6 +29,7 @@ from twisted.python.compat import networkString
 from twisted.python.filepath import FilePath, _asFilesystemBytes
 from twisted.python.log import err, msg
 from twisted.python.runtime import platform
+from twisted.test.test_process import Accumulator
 from twisted.trial.unittest import SynchronousTestCase, TestCase
 
 # Get the current Python executable as a bytestring.
@@ -1001,6 +1002,132 @@ class ProcessTestsBuilder(ProcessTestsBuilderBase):
             hamcrest.equal_to(["process already removed as desired"]),
         )
 
+    def checkSpawnProcessEnvironment(self, spawnKwargs, expectedEnv, usePosixSpawnp):
+        """
+        Shared code for testing the environment variables
+        present in the spawned process.
+
+        The spawned process serializes its environ to stderr or stdout (depending on usePTY)
+        which is checked against os.environ of the calling process.
+        """
+        p = Accumulator()
+        d = p.endedDeferred = Deferred()
+
+        reactor = self.buildReactor()
+        reactor._neverUseSpawn = not usePosixSpawnp
+
+        reactor.callWhenRunning(
+            reactor.spawnProcess,
+            p,
+            pyExe,
+            [
+                pyExe,
+                b"-c",
+                networkString(
+                    "import os, sys; "
+                    "env = dict(os.environ); "
+                    # LC_CTYPE is set by python, see https://peps.python.org/pep-0538/
+                    'env.pop("LC_CTYPE", None); '
+                    'env.pop("__CF_USER_TEXT_ENCODING", None); '
+                    "sys.stderr.write(str(sorted(env.items())))"
+                ),
+            ],
+            usePTY=self.usePTY,
+            **spawnKwargs,
+        )
+
+        def shutdown(ign):
+            reactor.stop()
+
+        d.addBoth(shutdown)
+
+        self.runReactor(reactor)
+
+        expectedEnv.pop("LC_CTYPE", None)
+        expectedEnv.pop("__CF_USER_TEXT_ENCODING", None)
+        self.assertEqual(
+            bytes(str(sorted(expectedEnv.items())), "utf-8"),
+            p.outF.getvalue() if self.usePTY else p.errF.getvalue(),
+        )
+
+    def checkSpawnProcessEnvironmentWithPosixSpawnp(self, spawnKwargs, expectedEnv):
+        return self.checkSpawnProcessEnvironment(
+            spawnKwargs, expectedEnv, usePosixSpawnp=True
+        )
+
+    def checkSpawnProcessEnvironmentWithFork(self, spawnKwargs, expectedEnv):
+        return self.checkSpawnProcessEnvironment(
+            spawnKwargs, expectedEnv, usePosixSpawnp=False
+        )
+
+    @onlyOnPOSIX
+    def test_environmentPosixSpawnpEnvNotSet(self):
+        """
+        An empty environment is passed to the spawned process, when the default value of the C{env}
+        is used. That is, when the C{env} argument is not explicitly set.
+
+        In this case posix_spawnp is used as the backend for spawning processes.
+        """
+        return self.checkSpawnProcessEnvironmentWithPosixSpawnp({}, {})
+
+    @onlyOnPOSIX
+    def test_environmentForkEnvNotSet(self):
+        """
+        An empty environment is passed to the spawned process, when the default value of the C{env}
+        is used. That is, when the C{env} argument is not explicitly set.
+
+        In this case fork+execvpe is used as the backend for spawning processes.
+        """
+        return self.checkSpawnProcessEnvironmentWithFork({}, {})
+
+    @onlyOnPOSIX
+    def test_environmentPosixSpawnpEnvNone(self):
+        """
+        The parent process environment is passed to the spawned process, when C{env} is set to
+        C{None}.
+
+        In this case posix_spawnp is used as the backend for spawning processes.
+        """
+        return self.checkSpawnProcessEnvironmentWithPosixSpawnp(
+            {"env": None}, os.environ
+        )
+
+    @onlyOnPOSIX
+    def test_environmentForkEnvNone(self):
+        """
+        The parent process environment is passed to the spawned process, when C{env} is set to
+        C{None}.
+
+        In this case fork+execvpe is used as the backend for spawning processes.
+        """
+        return self.checkSpawnProcessEnvironmentWithFork({"env": None}, os.environ)
+
+    @onlyOnPOSIX
+    def test_environmentPosixSpawnpEnvCustom(self):
+        """
+        The user-specified environment without extra variables from parent process is passed to the
+        spawned process, when C{env} is set to a dictionary.
+
+        In this case posix_spawnp is used as the backend for spawning processes.
+        """
+        return self.checkSpawnProcessEnvironmentWithPosixSpawnp(
+            {"env": {"MYENV1": "myvalue1"}},
+            {"MYENV1": "myvalue1"},
+        )
+
+    @onlyOnPOSIX
+    def test_environmentForkEnvCustom(self):
+        """
+        The user-specified environment without extra variables from parent process is passed to the
+        spawned process, when C{env} is set to a dictionary.
+
+        In this case fork+execvpe is used as the backend for spawning processes.
+        """
+        return self.checkSpawnProcessEnvironmentWithFork(
+            {"env": {"MYENV1": "myvalue1"}},
+            {"MYENV1": "myvalue1"},
+        )
+
 
 globals().update(ProcessTestsBuilder.makeTestCaseClasses())