Skip to content

Commit 6961e33

Browse files
committed
Merge pull request #780 from qwcode/pip_build_directory
/tmp/pip-build fixes
2 parents c96548f + 1af3f2b commit 6961e33

File tree

7 files changed

+137
-4
lines changed

7 files changed

+137
-4
lines changed

AUTHORS.txt

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ Clay McClure
1414
Cody Soyland
1515
Daniel Holth
1616
Dave Abrahams
17+
David (d1b)
1718
Dmitry Gladkov
1819
Donald Stufft
1920
Francesco

CHANGES.txt

+3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ develop (unreleased)
77
* Added "pip list" for listing installed packages and the latest version
88
available. Thanks Rafael Caricio, Miguel Araujo, Dmitry Gladkov (Pull #752)
99

10+
* Fixed security issues with pip's use of temp build directories.
11+
Thanks David (d1b) and Thomas Güttler. (Pull #780)
12+
1013
* Improvements to sphinx docs and cli help. (Pull #773)
1114

1215
* Fixed issue #707, dealing with OS X temp dir handling, which was causing

docs/cookbook.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ pip allows you to *just* unpack archives to a build directory without installing
7272

7373
$ pip install --no-install SomePackage
7474

75-
If you're in a virtualenv, the build dir is ``<virtualenv path>/build``. Otherwise, it's ``<OS temp dir>/pip-build``
75+
If you're in a virtualenv, the build dir is ``<virtualenv path>/build``. Otherwise, it's ``<OS temp dir>/pip-build-<username>``
7676

7777
Afterwards, to finish the job of installing unpacked archives, run::
7878

pip/commands/install.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ def __init__(self, *args, **kw):
7070
default=build_prefix,
7171
help='Directory to unpack packages into and build in. '
7272
'The default in a virtualenv is "<venv path>/build". '
73-
'The default for global installs is "<OS temp dir>/pip-build".')
73+
'The default for global installs is "<OS temp dir>/pip-build-<username>".')
7474

7575
cmd_opts.add_option(
7676
'-t', '--target',

pip/locations.py

+28-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
import site
55
import os
66
import tempfile
7+
import getpass
78
from pip.backwardcompat import get_python_lib
9+
import pip.exceptions
810

911

1012
def running_under_virtualenv():
@@ -25,6 +27,31 @@ def virtualenv_no_global():
2527
if running_under_virtualenv() and os.path.isfile(no_global_file):
2628
return True
2729

30+
def _get_build_prefix():
31+
""" Returns a safe build_prefix """
32+
path = os.path.join(tempfile.gettempdir(), 'pip-build-%s' % \
33+
getpass.getuser())
34+
if sys.platform == 'win32':
35+
""" on windows(tested on 7) temp dirs are isolated """
36+
return path
37+
try:
38+
os.mkdir(path)
39+
except OSError:
40+
file_uid = None
41+
try:
42+
fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW)
43+
file_uid = os.fstat(fd).st_uid
44+
os.close(fd)
45+
except OSError:
46+
file_uid = None
47+
if file_uid != os.getuid():
48+
msg = "The temporary folder for building (%s) is not owned by your user!" \
49+
% path
50+
print (msg)
51+
print("pip will not work until the temporary folder is " + \
52+
"either deleted or owned by your user account.")
53+
raise pip.exceptions.InstallationError(msg)
54+
return path
2855

2956
if running_under_virtualenv():
3057
build_prefix = os.path.join(sys.prefix, 'build')
@@ -33,7 +60,7 @@ def virtualenv_no_global():
3360
# Use tempfile to create a temporary folder for build
3461
# Note: we are NOT using mkdtemp so we can have a consistent build dir
3562
# Note: using realpath due to tmp dirs on OSX being symlinks
36-
build_prefix = os.path.realpath(os.path.join(tempfile.gettempdir(), 'pip-build'))
63+
build_prefix = os.path.realpath(_get_build_prefix())
3764

3865
## FIXME: keep src in cwd for now (it is not a temporary folder)
3966
try:

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
here = os.path.abspath(os.path.dirname(__file__))
99

1010
def read(*parts):
11-
return codecs.open(os.path.join(here, *parts), 'r', 'utf8').read()
11+
return codecs.open(os.path.join(here, *parts), 'r').read()
1212

1313
def find_version(*file_paths):
1414
version_file = read(*file_paths)

tests/test_locations.py

+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
"""
2+
locations.py tests
3+
4+
"""
5+
import os
6+
import sys
7+
import shutil
8+
import tempfile
9+
import getpass
10+
from mock import Mock
11+
from nose import SkipTest
12+
from nose.tools import assert_raises
13+
import pip
14+
15+
class TestLocations:
16+
def setup(self):
17+
self.tempdir = tempfile.mkdtemp()
18+
self.st_uid = 9999
19+
self.username = "example"
20+
self.patch()
21+
22+
def tearDown(self):
23+
self.revert_patch()
24+
shutil.rmtree(self.tempdir, ignore_errors=True)
25+
26+
def patch(self):
27+
""" first store and then patch python methods pythons """
28+
self.tempfile_gettempdir = tempfile.gettempdir
29+
self.old_os_fstat = os.fstat
30+
if sys.platform != 'win32':
31+
# os.getuid not implemented on windows
32+
self.old_os_getuid = os.getuid
33+
self.old_getpass_getuser = getpass.getuser
34+
35+
# now patch
36+
tempfile.gettempdir = lambda : self.tempdir
37+
getpass.getuser = lambda : self.username
38+
os.getuid = lambda : self.st_uid
39+
os.fstat = lambda fd : self.get_mock_fstat(fd)
40+
41+
def revert_patch(self):
42+
""" revert the patches to python methods """
43+
tempfile.gettempdir = self.tempfile_gettempdir
44+
getpass.getuser = self.old_getpass_getuser
45+
if sys.platform != 'win32':
46+
# os.getuid not implemented on windows
47+
os.getuid = self.old_os_getuid
48+
os.fstat = self.old_os_fstat
49+
50+
def get_mock_fstat(self, fd):
51+
""" returns a basic mock fstat call result.
52+
Currently only the st_uid attribute has been set.
53+
"""
54+
result = Mock()
55+
result.st_uid = self.st_uid
56+
return result
57+
58+
def get_build_dir_location(self):
59+
""" returns a string pointing to the
60+
current build_prefix.
61+
"""
62+
return os.path.join(self.tempdir, 'pip-build-%s' % self.username)
63+
64+
def test_dir_path(self):
65+
""" test the path name for the build_prefix
66+
"""
67+
from pip import locations
68+
assert locations._get_build_prefix() == self.get_build_dir_location()
69+
70+
def test_dir_created(self):
71+
""" test that the build_prefix directory is generated when
72+
_get_build_prefix is called.
73+
"""
74+
#skip on windows, build dir is not created
75+
if sys.platform == 'win32':
76+
raise SkipTest()
77+
assert not os.path.exists(self.get_build_dir_location() ), \
78+
"the build_prefix directory should not exist yet!"
79+
from pip import locations
80+
locations._get_build_prefix()
81+
assert os.path.exists(self.get_build_dir_location() ), \
82+
"the build_prefix directory should now exist!"
83+
84+
def test_error_raised_when_owned_by_another(self):
85+
""" test calling _get_build_prefix when there is a temporary
86+
directory owned by another user raises an InstallationError.
87+
"""
88+
#skip on windows; this exception logic only runs on linux
89+
if sys.platform == 'win32':
90+
raise SkipTest()
91+
from pip import locations
92+
os.getuid = lambda : 1111
93+
os.mkdir(self.get_build_dir_location() )
94+
assert_raises(pip.exceptions.InstallationError, locations._get_build_prefix)
95+
96+
def test_no_error_raised_when_owned_by_you(self):
97+
""" test calling _get_build_prefix when there is a temporary
98+
directory owned by you raise no InstallationError.
99+
"""
100+
from pip import locations
101+
os.mkdir(self.get_build_dir_location())
102+
locations._get_build_prefix()

0 commit comments

Comments
 (0)