Description
When running Jython within a sandboxed JVM environment that utilizes a strict java.lang.SecurityManager (specifically, one that denies java.io.FilePermission for reading system directories like /bin/),
executing commands via the subprocess module with shell=True (or using wrappers like os.popen or os.system) crashes with a cryptic Python TypeError rather than throwing a descriptive OSError or
Java SecurityException.
Environment
- Jython Version: 2.5.x, 2.7.x (including 2.7.3 and current development branch)
- OS: Linux/Unix (where
_shell_command resolution relies on /bin/sh)
- JVM State: Running with a
SecurityManager that denies read access to standard system executable paths.
Steps to Reproduce
Run a Jython script that attempts to use os.popen() or subprocess.Popen(..., shell=True) inside a JVM where the SecurityManager denies file read access to /bin/sh.
Minimal Jython Script:
import java.lang.SecurityManager as SecurityManager
import java.lang.System as System
import java.security.AccessControlException as AccessControlException
class MySecurityManager(SecurityManager):
def checkRead(self, file, context=None):
if file == "/bin/sh" or file == "cmd.exe" or file.endswith("sh"):
raise AccessControlException("Access denied: " + file)
def checkPermission(self, perm, context=None):
pass
System.setSecurityManager(MySecurityManager())
import subprocess
# This will crash with a TypeError
subprocess.Popen(["whoami"], shell=True)
**Stack Trace:**
test_sm.py:16: RuntimeWarning: Unable to determine _shell_command for underlying os: posix
import subprocess
Traceback (most recent call last):
File "test_sm.py", line 17, in <module>
subprocess.Popen(["whoami"], shell=True)
File ".../Lib/subprocess$py.class", line 892, in __init__
File ".../Lib/subprocess$py.class", line 1352, in _execute_child
TypeError: unsupported operand type(s) for +: 'NoneType' and 'list'
Root Cause Analysis
The bug resides in Lib/subprocess.py and stems from how Jython caches the default system shell executable during module initialization.
- Initialization (
_setup_platform): When the subprocess module is loaded, it attempts to determine the system shell by iterating through known shell paths (e.g., /bin/sh). It validates these paths
using os.path.exists(executable).
- SecurityManager Intervention: Because the JVM is sandboxed,
os.path.exists("/bin/sh") triggers the SecurityManager, which throws an AccessControlException.
- Silent Failure: Jython's
os.path.exists() implementation catches this Java security exception and simply returns False.
- Invalid State: Because
os.path.exists() returns False for all candidate shells, the global variable _shell_command is never assigned a valid list (like ["/bin/sh", "-c"]) and remains
initialized to None.
- The Crash (
_execute_child and system): Later, when the user invokes subprocess.Popen(..., shell=True) or os.system(), the code attempts to prepend the shell command to the user's arguments:
if shell:
args = _shell_command + args # <--- CRASH HERE
Because `_shell_command` is `None` and `args` is a list, Python throws the `TypeError`.
Expected Behavior
Instead of a cryptic TypeError about concatenating NoneType and list, Jython should gracefully handle the scenario where a shell cannot be located (whether due to actual absence or SecurityManager
restrictions).
Description
When running Jython within a sandboxed JVM environment that utilizes a strict
java.lang.SecurityManager(specifically, one that deniesjava.io.FilePermissionfor reading system directories like/bin/),executing commands via the
subprocessmodule withshell=True(or using wrappers likeos.popenoros.system) crashes with a cryptic PythonTypeErrorrather than throwing a descriptiveOSErrororJava
SecurityException.Environment
_shell_commandresolution relies on/bin/sh)SecurityManagerthat denies read access to standard system executable paths.Steps to Reproduce
Run a Jython script that attempts to use
os.popen()orsubprocess.Popen(..., shell=True)inside a JVM where theSecurityManagerdenies file read access to/bin/sh.Minimal Jython Script:
Root Cause Analysis
The bug resides in
Lib/subprocess.pyand stems from how Jython caches the default system shell executable during module initialization._setup_platform): When thesubprocessmodule is loaded, it attempts to determine the system shell by iterating through known shell paths (e.g.,/bin/sh). It validates these pathsusing
os.path.exists(executable).os.path.exists("/bin/sh")triggers theSecurityManager, which throws anAccessControlException.os.path.exists()implementation catches this Java security exception and simply returnsFalse.os.path.exists()returnsFalsefor all candidate shells, the global variable_shell_commandis never assigned a valid list (like["/bin/sh", "-c"]) and remainsinitialized to
None._execute_childandsystem): Later, when the user invokessubprocess.Popen(..., shell=True)oros.system(), the code attempts to prepend the shell command to the user's arguments:Expected Behavior
Instead of a cryptic
TypeErrorabout concatenatingNoneTypeandlist, Jython should gracefully handle the scenario where a shell cannot be located (whether due to actual absence orSecurityManagerrestrictions).