Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

/tmp/pip-build fixes #780

Merged
merged 11 commits into from
Jan 26, 2013
1 change: 1 addition & 0 deletions AUTHORS.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Clay McClure
Cody Soyland
Daniel Holth
Dave Abrahams
David (d1b)
Dmitry Gladkov
Donald Stufft
Francesco
Expand Down
3 changes: 3 additions & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ develop (unreleased)
* Added "pip list" for listing installed packages and the latest version
available. Thanks Rafael Caricio, Miguel Araujo, Dmitry Gladkov (Pull #752)

* Fixed security issues with pip's use of temp build directories.
Thanks David (d1b) and Thomas Güttler. (Pull #780)

* Improvements to sphinx docs and cli help. (Pull #773)

* Fixed issue #707, dealing with OS X temp dir handling, which was causing
Expand Down
2 changes: 1 addition & 1 deletion docs/cookbook.txt
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ pip allows you to *just* unpack archives to a build directory without installing

$ pip install --no-install SomePackage

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

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

Expand Down
2 changes: 1 addition & 1 deletion pip/commands/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ def __init__(self, *args, **kw):
default=build_prefix,
help='Directory to unpack packages into and build in. '
'The default in a virtualenv is "<venv path>/build". '
'The default for global installs is "<OS temp dir>/pip-build".')
'The default for global installs is "<OS temp dir>/pip-build-<username>".')

cmd_opts.add_option(
'-t', '--target',
Expand Down
29 changes: 28 additions & 1 deletion pip/locations.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
import site
import os
import tempfile
import getpass
from pip.backwardcompat import get_python_lib
import pip.exceptions


def running_under_virtualenv():
Expand All @@ -25,6 +27,31 @@ def virtualenv_no_global():
if running_under_virtualenv() and os.path.isfile(no_global_file):
return True

def _get_build_prefix():
""" Returns a safe build_prefix """
path = os.path.join(tempfile.gettempdir(), 'pip-build-%s' % \
getpass.getuser())
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getpass.getuser() returns the value in the LOGNAME environment variable, so this doesn't work under sudo:

$ sudo LOGNAME=foo python
Python 2.6.6 (r266:84292, Oct 12 2012, 14:23:48)
[GCC 4.4.6 20120305 (Red Hat 4.4.6-4)] on linux2
Type "help", "copyright", "credits" or "license" for more information.

import getpass
getpass.getuser()
'foo'

This should be based off of os.geteuid()

$ sudo LOGNAME=foo python
Python 2.6.6 (r266:84292, Oct 12 2012, 14:23:48)
[GCC 4.4.6 20120305 (Red Hat 4.4.6-4)] on linux2
Type "help", "copyright", "credits" or "license" for more information.

import os
import pwd
pwd.getpwuid(os.geteuid()).pw_name
'root'

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue for this: #982

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would expect LOGNAME to be 'root' under sudo.
I see that on ubuntu and centos.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@timjr yes it should have been based off os.geteuid(), but getpass.getuser() is supported on windows and linux and so I used that.

if sys.platform == 'win32':
""" on windows(tested on 7) temp dirs are isolated """
return path
try:
os.mkdir(path)
except OSError:
file_uid = None
try:
fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW)
file_uid = os.fstat(fd).st_uid
os.close(fd)
except OSError:
file_uid = None
if file_uid != os.getuid():
msg = "The temporary folder for building (%s) is not owned by your user!" \
% path
print (msg)
print("pip will not work until the temporary folder is " + \
"either deleted or owned by your user account.")
raise pip.exceptions.InstallationError(msg)
return path

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

## FIXME: keep src in cwd for now (it is not a temporary folder)
try:
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
here = os.path.abspath(os.path.dirname(__file__))

def read(*parts):
return codecs.open(os.path.join(here, *parts), 'r', 'utf8').read()
return codecs.open(os.path.join(here, *parts), 'r').read()

def find_version(*file_paths):
version_file = read(*file_paths)
Expand Down
102 changes: 102 additions & 0 deletions tests/test_locations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
"""
locations.py tests

"""
import os
import sys
import shutil
import tempfile
import getpass
from mock import Mock
from nose import SkipTest
from nose.tools import assert_raises
import pip

class TestLocations:
def setup(self):
self.tempdir = tempfile.mkdtemp()
self.st_uid = 9999
self.username = "example"
self.patch()

def tearDown(self):
self.revert_patch()
shutil.rmtree(self.tempdir, ignore_errors=True)

def patch(self):
""" first store and then patch python methods pythons """
self.tempfile_gettempdir = tempfile.gettempdir
self.old_os_fstat = os.fstat
if sys.platform != 'win32':
# os.getuid not implemented on windows
self.old_os_getuid = os.getuid
self.old_getpass_getuser = getpass.getuser

# now patch
tempfile.gettempdir = lambda : self.tempdir
getpass.getuser = lambda : self.username
os.getuid = lambda : self.st_uid
os.fstat = lambda fd : self.get_mock_fstat(fd)

def revert_patch(self):
""" revert the patches to python methods """
tempfile.gettempdir = self.tempfile_gettempdir
getpass.getuser = self.old_getpass_getuser
if sys.platform != 'win32':
# os.getuid not implemented on windows
os.getuid = self.old_os_getuid
os.fstat = self.old_os_fstat

def get_mock_fstat(self, fd):
""" returns a basic mock fstat call result.
Currently only the st_uid attribute has been set.
"""
result = Mock()
result.st_uid = self.st_uid
return result

def get_build_dir_location(self):
""" returns a string pointing to the
current build_prefix.
"""
return os.path.join(self.tempdir, 'pip-build-%s' % self.username)

def test_dir_path(self):
""" test the path name for the build_prefix
"""
from pip import locations
assert locations._get_build_prefix() == self.get_build_dir_location()

def test_dir_created(self):
""" test that the build_prefix directory is generated when
_get_build_prefix is called.
"""
#skip on windows, build dir is not created
if sys.platform == 'win32':
raise SkipTest()
assert not os.path.exists(self.get_build_dir_location() ), \
"the build_prefix directory should not exist yet!"
from pip import locations
locations._get_build_prefix()
assert os.path.exists(self.get_build_dir_location() ), \
"the build_prefix directory should now exist!"

def test_error_raised_when_owned_by_another(self):
""" test calling _get_build_prefix when there is a temporary
directory owned by another user raises an InstallationError.
"""
#skip on windows; this exception logic only runs on linux
if sys.platform == 'win32':
raise SkipTest()
from pip import locations
os.getuid = lambda : 1111
os.mkdir(self.get_build_dir_location() )
assert_raises(pip.exceptions.InstallationError, locations._get_build_prefix)

def test_no_error_raised_when_owned_by_you(self):
""" test calling _get_build_prefix when there is a temporary
directory owned by you raise no InstallationError.
"""
from pip import locations
os.mkdir(self.get_build_dir_location())
locations._get_build_prefix()