| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480 |
- """
- Environments are a way to activate different Python versions or Virtualenvs for
- static analysis. The Python binary in that environment is going to be executed.
- """
- import os
- import sys
- import hashlib
- import filecmp
- from collections import namedtuple
- from shutil import which
- from typing import TYPE_CHECKING
- from jedi.cache import memoize_method, time_cache
- from jedi.inference.compiled.subprocess import CompiledSubprocess, \
- InferenceStateSameProcess, InferenceStateSubprocess
- import parso
- if TYPE_CHECKING:
- from jedi.inference import InferenceState
- _VersionInfo = namedtuple('VersionInfo', 'major minor micro') # type: ignore[name-match]
- _SUPPORTED_PYTHONS = ['3.13', '3.12', '3.11', '3.10', '3.9', '3.8', '3.7', '3.6']
- _SAFE_PATHS = ['/usr/bin', '/usr/local/bin']
- _CONDA_VAR = 'CONDA_PREFIX'
- _CURRENT_VERSION = '%s.%s' % (sys.version_info.major, sys.version_info.minor)
- class InvalidPythonEnvironment(Exception):
- """
- If you see this exception, the Python executable or Virtualenv you have
- been trying to use is probably not a correct Python version.
- """
- class _BaseEnvironment:
- @memoize_method
- def get_grammar(self):
- version_string = '%s.%s' % (self.version_info.major, self.version_info.minor)
- return parso.load_grammar(version=version_string)
- @property
- def _sha256(self):
- try:
- return self._hash
- except AttributeError:
- self._hash = _calculate_sha256_for_file(self.executable)
- return self._hash
- def _get_info():
- return (
- sys.executable,
- sys.prefix,
- sys.version_info[:3],
- )
- class Environment(_BaseEnvironment):
- """
- This class is supposed to be created by internal Jedi architecture. You
- should not create it directly. Please use create_environment or the other
- functions instead. It is then returned by that function.
- """
- _subprocess = None
- def __init__(self, executable, env_vars=None):
- self._start_executable = executable
- self._env_vars = env_vars
- # Initialize the environment
- self._get_subprocess()
- def _get_subprocess(self):
- if self._subprocess is not None and not self._subprocess.is_crashed:
- return self._subprocess
- try:
- self._subprocess = CompiledSubprocess(self._start_executable,
- env_vars=self._env_vars)
- info = self._subprocess._send(None, _get_info)
- except Exception as exc:
- raise InvalidPythonEnvironment(
- "Could not get version information for %r: %r" % (
- self._start_executable,
- exc))
- # Since it could change and might not be the same(?) as the one given,
- # set it here.
- self.executable = info[0]
- """
- The Python executable, matches ``sys.executable``.
- """
- self.path = info[1]
- """
- The path to an environment, matches ``sys.prefix``.
- """
- self.version_info = _VersionInfo(*info[2])
- """
- Like :data:`sys.version_info`: a tuple to show the current
- Environment's Python version.
- """
- return self._subprocess
- def __repr__(self):
- version = '.'.join(str(i) for i in self.version_info)
- return '<%s: %s in %s>' % (self.__class__.__name__, version, self.path)
- def get_inference_state_subprocess(
- self,
- inference_state: 'InferenceState',
- ) -> InferenceStateSubprocess:
- return InferenceStateSubprocess(inference_state, self._get_subprocess())
- @memoize_method
- def get_sys_path(self):
- """
- The sys path for this environment. Does not include potential
- modifications from e.g. appending to :data:`sys.path`.
- :returns: list of str
- """
- # It's pretty much impossible to generate the sys path without actually
- # executing Python. The sys path (when starting with -S) itself depends
- # on how the Python version was compiled (ENV variables).
- # If you omit -S when starting Python (normal case), additionally
- # site.py gets executed.
- return self._get_subprocess().get_sys_path()
- class _SameEnvironmentMixin:
- def __init__(self):
- self._start_executable = self.executable = sys.executable
- self.path = sys.prefix
- self.version_info = _VersionInfo(*sys.version_info[:3])
- self._env_vars = None
- class SameEnvironment(_SameEnvironmentMixin, Environment):
- pass
- class InterpreterEnvironment(_SameEnvironmentMixin, _BaseEnvironment):
- def get_inference_state_subprocess(
- self,
- inference_state: 'InferenceState',
- ) -> InferenceStateSameProcess:
- return InferenceStateSameProcess(inference_state)
- def get_sys_path(self):
- return sys.path
- def _get_virtual_env_from_var(env_var='VIRTUAL_ENV'):
- """Get virtualenv environment from VIRTUAL_ENV environment variable.
- It uses `safe=False` with ``create_environment``, because the environment
- variable is considered to be safe / controlled by the user solely.
- """
- var = os.environ.get(env_var)
- if var:
- # Under macOS in some cases - notably when using Pipenv - the
- # sys.prefix of the virtualenv is /path/to/env/bin/.. instead of
- # /path/to/env so we need to fully resolve the paths in order to
- # compare them.
- if os.path.realpath(var) == os.path.realpath(sys.prefix):
- return _try_get_same_env()
- try:
- return create_environment(var, safe=False)
- except InvalidPythonEnvironment:
- pass
- def _calculate_sha256_for_file(path):
- sha256 = hashlib.sha256()
- with open(path, 'rb') as f:
- for block in iter(lambda: f.read(filecmp.BUFSIZE), b''):
- sha256.update(block)
- return sha256.hexdigest()
- def get_default_environment():
- """
- Tries to return an active Virtualenv or conda environment.
- If there is no VIRTUAL_ENV variable or no CONDA_PREFIX variable set
- set it will return the latest Python version installed on the system. This
- makes it possible to use as many new Python features as possible when using
- autocompletion and other functionality.
- :returns: :class:`.Environment`
- """
- virtual_env = _get_virtual_env_from_var()
- if virtual_env is not None:
- return virtual_env
- conda_env = _get_virtual_env_from_var(_CONDA_VAR)
- if conda_env is not None:
- return conda_env
- return _try_get_same_env()
- def _try_get_same_env():
- env = SameEnvironment()
- if not os.path.basename(env.executable).lower().startswith('python'):
- # This tries to counter issues with embedding. In some cases (e.g.
- # VIM's Python Mac/Windows, sys.executable is /foo/bar/vim. This
- # happens, because for Mac a function called `_NSGetExecutablePath` is
- # used and for Windows `GetModuleFileNameW`. These are both platform
- # specific functions. For all other systems sys.executable should be
- # alright. However here we try to generalize:
- #
- # 1. Check if the executable looks like python (heuristic)
- # 2. In case it's not try to find the executable
- # 3. In case we don't find it use an interpreter environment.
- #
- # The last option will always work, but leads to potential crashes of
- # Jedi - which is ok, because it happens very rarely and even less,
- # because the code below should work for most cases.
- if os.name == 'nt':
- # The first case would be a virtualenv and the second a normal
- # Python installation.
- checks = (r'Scripts\python.exe', 'python.exe')
- else:
- # For unix it looks like Python is always in a bin folder.
- checks = (
- 'bin/python%s.%s' % (sys.version_info[0], sys.version[1]),
- 'bin/python%s' % (sys.version_info[0]),
- 'bin/python',
- )
- for check in checks:
- guess = os.path.join(sys.exec_prefix, check)
- if os.path.isfile(guess):
- # Bingo - We think we have our Python.
- return Environment(guess)
- # It looks like there is no reasonable Python to be found.
- return InterpreterEnvironment()
- # If no virtualenv is found, use the environment we're already
- # using.
- return env
- def get_cached_default_environment():
- var = os.environ.get('VIRTUAL_ENV') or os.environ.get(_CONDA_VAR)
- environment = _get_cached_default_environment()
- # Under macOS in some cases - notably when using Pipenv - the
- # sys.prefix of the virtualenv is /path/to/env/bin/.. instead of
- # /path/to/env so we need to fully resolve the paths in order to
- # compare them.
- if var and os.path.realpath(var) != os.path.realpath(environment.path):
- _get_cached_default_environment.clear_cache()
- return _get_cached_default_environment()
- return environment
- @time_cache(seconds=10 * 60) # 10 Minutes
- def _get_cached_default_environment():
- try:
- return get_default_environment()
- except InvalidPythonEnvironment:
- # It's possible that `sys.executable` is wrong. Typically happens
- # when Jedi is used in an executable that embeds Python. For further
- # information, have a look at:
- # https://github.com/davidhalter/jedi/issues/1531
- return InterpreterEnvironment()
- def find_virtualenvs(paths=None, *, safe=True, use_environment_vars=True):
- """
- :param paths: A list of paths in your file system to be scanned for
- Virtualenvs. It will search in these paths and potentially execute the
- Python binaries.
- :param safe: Default True. In case this is False, it will allow this
- function to execute potential `python` environments. An attacker might
- be able to drop an executable in a path this function is searching by
- default. If the executable has not been installed by root, it will not
- be executed.
- :param use_environment_vars: Default True. If True, the VIRTUAL_ENV
- variable will be checked if it contains a valid VirtualEnv.
- CONDA_PREFIX will be checked to see if it contains a valid conda
- environment.
- :yields: :class:`.Environment`
- """
- if paths is None:
- paths = []
- _used_paths = set()
- if use_environment_vars:
- # Using this variable should be safe, because attackers might be
- # able to drop files (via git) but not environment variables.
- virtual_env = _get_virtual_env_from_var()
- if virtual_env is not None:
- yield virtual_env
- _used_paths.add(virtual_env.path)
- conda_env = _get_virtual_env_from_var(_CONDA_VAR)
- if conda_env is not None:
- yield conda_env
- _used_paths.add(conda_env.path)
- for directory in paths:
- if not os.path.isdir(directory):
- continue
- directory = os.path.abspath(directory)
- for path in os.listdir(directory):
- path = os.path.join(directory, path)
- if path in _used_paths:
- # A path shouldn't be inferred twice.
- continue
- _used_paths.add(path)
- try:
- executable = _get_executable_path(path, safe=safe)
- yield Environment(executable)
- except InvalidPythonEnvironment:
- pass
- def find_system_environments(*, env_vars=None):
- """
- Ignores virtualenvs and returns the Python versions that were installed on
- your system. This might return nothing, if you're running Python e.g. from
- a portable version.
- The environments are sorted from latest to oldest Python version.
- :yields: :class:`.Environment`
- """
- for version_string in _SUPPORTED_PYTHONS:
- try:
- yield get_system_environment(version_string, env_vars=env_vars)
- except InvalidPythonEnvironment:
- pass
- # TODO: this function should probably return a list of environments since
- # multiple Python installations can be found on a system for the same version.
- def get_system_environment(version, *, env_vars=None):
- """
- Return the first Python environment found for a string of the form 'X.Y'
- where X and Y are the major and minor versions of Python.
- :raises: :exc:`.InvalidPythonEnvironment`
- :returns: :class:`.Environment`
- """
- exe = which('python' + version)
- if exe:
- if exe == sys.executable:
- return SameEnvironment()
- return Environment(exe)
- if os.name == 'nt':
- for exe in _get_executables_from_windows_registry(version):
- try:
- return Environment(exe, env_vars=env_vars)
- except InvalidPythonEnvironment:
- pass
- raise InvalidPythonEnvironment("Cannot find executable python%s." % version)
- def create_environment(path, *, safe=True, env_vars=None):
- """
- Make it possible to manually create an Environment object by specifying a
- Virtualenv path or an executable path and optional environment variables.
- :raises: :exc:`.InvalidPythonEnvironment`
- :returns: :class:`.Environment`
- """
- if os.path.isfile(path):
- _assert_safe(path, safe)
- return Environment(path, env_vars=env_vars)
- return Environment(_get_executable_path(path, safe=safe), env_vars=env_vars)
- def _get_executable_path(path, safe=True):
- """
- Returns None if it's not actually a virtual env.
- """
- if os.name == 'nt':
- pythons = [os.path.join(path, 'Scripts', 'python.exe'), os.path.join(path, 'python.exe')]
- else:
- pythons = [os.path.join(path, 'bin', 'python')]
- for python in pythons:
- if os.path.exists(python):
- break
- else:
- raise InvalidPythonEnvironment("%s seems to be missing." % python)
- _assert_safe(python, safe)
- return python
- def _get_executables_from_windows_registry(version):
- import winreg
- # TODO: support Python Anaconda.
- sub_keys = [
- r'SOFTWARE\Python\PythonCore\{version}\InstallPath',
- r'SOFTWARE\Wow6432Node\Python\PythonCore\{version}\InstallPath',
- r'SOFTWARE\Python\PythonCore\{version}-32\InstallPath',
- r'SOFTWARE\Wow6432Node\Python\PythonCore\{version}-32\InstallPath'
- ]
- for root_key in [winreg.HKEY_CURRENT_USER, winreg.HKEY_LOCAL_MACHINE]:
- for sub_key in sub_keys:
- sub_key = sub_key.format(version=version)
- try:
- with winreg.OpenKey(root_key, sub_key) as key:
- prefix = winreg.QueryValueEx(key, '')[0]
- exe = os.path.join(prefix, 'python.exe')
- if os.path.isfile(exe):
- yield exe
- except WindowsError:
- pass
- def _assert_safe(executable_path, safe):
- if safe and not _is_safe(executable_path):
- raise InvalidPythonEnvironment(
- "The python binary is potentially unsafe.")
- def _is_safe(executable_path):
- # Resolve sym links. A venv typically is a symlink to a known Python
- # binary. Only virtualenvs copy symlinks around.
- real_path = os.path.realpath(executable_path)
- if _is_unix_safe_simple(real_path):
- return True
- # Just check the list of known Python versions. If it's not in there,
- # it's likely an attacker or some Python that was not properly
- # installed in the system.
- for environment in find_system_environments():
- if environment.executable == real_path:
- return True
- # If the versions don't match, just compare the binary files. If we
- # don't do that, only venvs will be working and not virtualenvs.
- # venvs are symlinks while virtualenvs are actual copies of the
- # Python files.
- # This still means that if the system Python is updated and the
- # virtualenv's Python is not (which is probably never going to get
- # upgraded), it will not work with Jedi. IMO that's fine, because
- # people should just be using venv. ~ dave
- if environment._sha256 == _calculate_sha256_for_file(real_path):
- return True
- return False
- def _is_unix_safe_simple(real_path):
- if _is_unix_admin():
- # In case we are root, just be conservative and
- # only execute known paths.
- return any(real_path.startswith(p) for p in _SAFE_PATHS)
- uid = os.stat(real_path).st_uid
- # The interpreter needs to be owned by root. This means that it wasn't
- # written by a user and therefore attacking Jedi is not as simple.
- # The attack could look like the following:
- # 1. A user clones a repository.
- # 2. The repository has an innocent looking folder called foobar. jedi
- # searches for the folder and executes foobar/bin/python --version if
- # there's also a foobar/bin/activate.
- # 3. The attacker has gained code execution, since he controls
- # foobar/bin/python.
- return uid == 0
- def _is_unix_admin():
- try:
- return os.getuid() == 0
- except AttributeError:
- return False # Windows
|