"fail-clean", "force-mirror", "force-prefix", "getbinpkg",
"installsources", "keeptemp", "keepwork", "fixlafiles", "lmirror",
"merge-sync",
- "metadata-transfer", "mirror", "multilib-strict", "news",
+ "metadata-transfer", "mirror", "multilib-strict",
+ "network-sandbox", "news",
"noauto", "noclean", "nodoc", "noinfo", "noman",
"nostrip", "notitles", "parallel-fetch", "parallel-install",
"prelink-checksums", "preserve-libs",
from itertools import chain
import logging
import os as _os
+import platform
import pwd
import re
import signal
"prerm", "setup"
])
+# phases in which networking access is allowed
+_networked_phases = frozenset([
+ # for VCS fetching
+ "unpack",
+ # for IPC
+ "setup", "pretend",
+ "preinst", "postinst", "prerm", "postrm",
+])
+
_phase_func_map = {
"config": "pkg_config",
"setup": "pkg_setup",
if phase in _unsandboxed_phases:
kwargs['free'] = True
+ if phase in _networked_phases:
+ kwargs['networked'] = True
if phase == 'depend':
kwargs['droppriv'] = 'userpriv' in settings.features
# XXX This would be to replace getstatusoutput completely.
# XXX Issue: cannot block execution. Deadlock condition.
-def spawn(mystring, mysettings, debug=0, free=0, droppriv=0, sesandbox=0, fakeroot=0, **keywords):
+def spawn(mystring, mysettings, debug=0, free=0, droppriv=0, sesandbox=0, fakeroot=0, networked=0, **keywords):
"""
Spawn a subprocess with extra portage-specific options.
Optiosn include:
@type sesandbox: Boolean
@param fakeroot: Run this command with faked root privileges
@type fakeroot: Boolean
+ @param networked: Run this command with networking access enabled
+ @type networked: Boolean
@param keywords: Extra options encoded as a dict, to be passed to spawn
@type keywords: Dictionary
@rtype: Integer
break
features = mysettings.features
+
+ # Unshare network namespace to keep ebuilds sanitized
+ if not networked and uid == 0 and platform.system() == 'Linux' and "network-sandbox" in features:
+ keywords['unshare_net'] = True
+
# TODO: Enable fakeroot to be used together with droppriv. The
# fake ownership/permissions will have to be converted to real
# permissions in the merge phase.
import atexit
import errno
+import fcntl
import platform
import signal
+import socket
+import struct
import sys
import traceback
import os as _os
from portage.const import BASH_BINARY, SANDBOX_BINARY, FAKEROOT_BINARY
from portage.exception import CommandNotFound
+from portage.util._ctypes import find_library, LoadLibrary, ctypes
try:
import resource
def spawn(mycommand, env={}, opt_name=None, fd_pipes=None, returnpid=False,
uid=None, gid=None, groups=None, umask=None, logfile=None,
- path_lookup=True, pre_exec=None, close_fds=True):
+ path_lookup=True, pre_exec=None, close_fds=True, unshare_net=False):
"""
Spawns a given command.
@param close_fds: If True, then close all file descriptors except those
referenced by fd_pipes (default is True).
@type close_fds: Boolean
-
+ @param unshare_net: If True, networking will be unshared from the spawned process
+ @type unshare_net: Boolean
+
logfile requires stdout and stderr to be assigned to this process (ie not pointed
somewhere else.)
fd_pipes[1] = pw
fd_pipes[2] = pw
+ # This caches the libc library lookup in the current
+ # process, so that it's only done once rather than
+ # for each child process.
+ if unshare_net:
+ find_library("c")
+
parent_pid = os.getpid()
pid = None
try:
if pid == 0:
try:
_exec(binary, mycommand, opt_name, fd_pipes,
- env, gid, groups, uid, umask, pre_exec, close_fds)
+ env, gid, groups, uid, umask, pre_exec, close_fds,
+ unshare_net)
except SystemExit:
raise
except Exception as e:
return 0
def _exec(binary, mycommand, opt_name, fd_pipes, env, gid, groups, uid, umask,
- pre_exec, close_fds):
+ pre_exec, close_fds, unshare_net):
"""
Execute a given binary with options
@type umask: Integer
@param pre_exec: A function to be called with no arguments just prior to the exec call.
@type pre_exec: callable
+ @param unshare_net: If True, networking will be unshared from the spawned process
+ @type unshare_net: Boolean
@rtype: None
@return: Never returns (calls os.execve)
"""
-
+
# If the process we're creating hasn't been given a name
# assign it the name of the executable.
if not opt_name:
_setup_pipes(fd_pipes, close_fds=close_fds)
+ # Unshare network (while still uid==0)
+ if unshare_net:
+ filename = find_library("c")
+ if filename is not None:
+ libc = LoadLibrary(filename)
+ if libc is not None:
+ CLONE_NEWNET = 0x40000000
+ try:
+ if libc.unshare(CLONE_NEWNET) != 0:
+ writemsg("Unable to unshare network: %s\n" % (
+ errno.errorcode.get(ctypes.get_errno(), '?')),
+ noiselevel=-1)
+ else:
+ # 'up' the loopback
+ IFF_UP = 0x1
+ ifreq = struct.pack('16sh', b'lo', IFF_UP)
+ SIOCSIFFLAGS = 0x8914
+
+ sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, 0)
+ try:
+ fcntl.ioctl(sock, SIOCSIFFLAGS, ifreq)
+ except IOError as e:
+ writemsg("Unable to enable loopback interface: %s\n" % (
+ errno.errorcode.get(e.errno, '?')),
+ noiselevel=-1)
+ except AttributeError:
+ # unshare() not supported by libc
+ pass
+
# Set requested process permissions.
if gid:
# Cast proxies to int, in case it matters.