diff --git a/Makefile b/Makefile index 4afc836a..399fb7c2 100644 --- a/Makefile +++ b/Makefile @@ -25,7 +25,7 @@ install-deps-dev: install-deps install-dev: install-deps-dev install-python-requirements-dev $(PYTHON) -m pip install -e . install-travis: - python3.6 -m pip install flake8-mutable flake8-docstrings flake8-builtins flake8-mypy bandit bandit-high-entropy-string + python3.6 -m pip install -IU flake8-mutable flake8-docstrings flake8-builtins flake8-mypy bandit==1.5.1 bandit-high-entropy-string uninstall: $(PYTHON) -m pip uninstall -y ioc @if [ -f /usr/local/etc/rc.d/ioc ]; then \ diff --git a/libioc/Config/Jail/BaseConfig.py b/libioc/Config/Jail/BaseConfig.py index 0c4a6073..941daa15 100644 --- a/libioc/Config/Jail/BaseConfig.py +++ b/libioc/Config/Jail/BaseConfig.py @@ -357,6 +357,37 @@ def _get_tags(self) -> typing.List[str]: return self.__unique_list(tags) + @property + def __has_mounts_enabled(self) -> bool: + prefix = "allow_mount_" + return any((self[x] == 1) for x in self.keys() if x.startswith(prefix)) + + def _get_enforce_statfs(self) -> int: + key = "enforce_statfs" + if key in self.data.keys(): + return int(self.data[key]) + + if self.__has_mounts_enabled: + self.logger.verbose( + "setting enforce_statfs=1 to support allowed mounts" + ) + return 1 + + raise KeyError(f"{key} unconfigured") + + def _get_allow_mount(self) -> int: + key = "allow_mount" + if key in self.data.keys(): + return int(self.data[key]) + + if self.__has_mounts_enabled: + self.logger.verbose( + "inheriting allow_mount=1 from allowed mounts" + ) + return 1 + + raise KeyError(f"{key} unconfigured") + def __unique_list(self, seq: typing.List[str]) -> typing.List[str]: seen: typing.Set[str] = set() seen_add = seen.add @@ -621,7 +652,7 @@ def __getitem__(self, key: str) -> typing.Any: def unknown_config_parameters(self) -> typing.Iterator[str]: """Yield unknown config parameters already stored in the config.""" for key in self.data.keys(): - if self._is_known_property(key, explicit=True) is False: + if self.is_known_property(key, explicit=True) is False: yield key def __delitem__(self, key: str) -> None: @@ -636,7 +667,7 @@ def __setitem__( # noqa: T400 explicit: bool=True ) -> None: """Set a configuration value.""" - if self._is_known_property(key, explicit=explicit) is False: + if self.is_known_property(key, explicit=explicit) is False: if "jail" in dir(self): _jail = self.jail # noqa: T484 else: @@ -865,7 +896,7 @@ def is_user_property(key: str) -> bool: """Return whether the given key belongs to a custom user property.""" return (key == "user") or (key.startswith("user.") is True) - def _is_known_property(self, key: str, explicit: bool) -> bool: + def is_known_property(self, key: str, explicit: bool) -> bool: """Return True when the key is a known config property.""" if self._is_known_jail_param(key): return True @@ -873,6 +904,8 @@ def _is_known_property(self, key: str, explicit: bool) -> bool: return True # key is default if f"_set_{key}" in dict.__dir__(self): return True # key is setter + if f"_get_{key}" in dict.__dir__(self): + return True # key is getter if key in libioc.Config.Jail.Properties.properties: return True # key is special property if self._key_is_mac_config(key, explicit=explicit) is True: @@ -898,7 +931,7 @@ def _require_known_config_property( key: str, explicit: bool=True ) -> None: - if self._is_known_property(key, explicit=explicit) is False: + if self.is_known_property(key, explicit=explicit) is False: raise libioc.errors.UnknownConfigProperty( key=key, logger=self.logger diff --git a/libioc/Config/Jail/File/Fstab.py b/libioc/Config/Jail/File/Fstab.py index 14646d8e..0e043b9b 100644 --- a/libioc/Config/Jail/File/Fstab.py +++ b/libioc/Config/Jail/File/Fstab.py @@ -633,7 +633,7 @@ def path(self) -> str: return self.file else: path = f"{self.jail.dataset.mountpoint}/{self.file}" - self.jail._require_relative_path(path) + self.jail.require_relative_path(path) return path def add_line( @@ -692,7 +692,7 @@ def add_line( self.jail.root_path, line["destination"].strip("/") ])) - self.jail._require_relative_path(_destination) + self.jail.require_relative_path(_destination) line["destination"] = _destination libioc.helpers.require_no_symlink(str(line["destination"])) @@ -707,7 +707,7 @@ def add_line( if (auto_mount_jail and self.jail.running) is True: destination = line["destination"] - self.jail._require_relative_path(destination) + self.jail.require_relative_path(destination) self.logger.verbose( f"auto-mount {destination}" ) diff --git a/libioc/Config/Jail/File/__init__.py b/libioc/Config/Jail/File/__init__.py index 36c9a501..5b195640 100644 --- a/libioc/Config/Jail/File/__init__.py +++ b/libioc/Config/Jail/File/__init__.py @@ -210,5 +210,5 @@ def __init__( def path(self) -> str: """Absolute path to the file.""" path = f"{self.resource.root_dataset.mountpoint}/{self.file}" - self.resource._require_relative_path(path) + self.resource.require_relative_path(path) return os.path.abspath(path) diff --git a/libioc/Config/Jail/JailConfig.py b/libioc/Config/Jail/JailConfig.py index 14178b52..e90d2a4a 100644 --- a/libioc/Config/Jail/JailConfig.py +++ b/libioc/Config/Jail/JailConfig.py @@ -80,6 +80,51 @@ def _get_host_hostname(self) -> str: def _get_legacy(self) -> bool: return self.legacy + def _get_mount_fdescfs(self) -> int: + return self.__get_mount(key="mount_fdescfs") + + def _set_mount_fdescfs(self, value: typing.Union[str, int, bool]) -> None: + self.__set_mount(key="mount_fdescfs", value=value) + + def _get_mount_devfs(self) -> int: + return self.__get_mount(key="mount_devfs") + + def _set_mount_devfs(self, value: typing.Union[str, int, bool]) -> None: + self.__set_mount(key="mount_devfs", value=value) + + def __get_mount(self, key: str) -> int: + return 1 * ((int(self.data[key]) == 1) is True) + + def __set_mount( + self, + key: str, + value: typing.Union[str, int, bool] + ) -> None: + + error_reason: typing.Optional[str] = None + if isinstance(value, bool) is True: + enabled = (value is True) + else: + try: + str_value = str(int(value)) + except ValueError: + error_reason = "invalid input type (expected int, str or bool)" + + if (str_value != "0") and (str_value != "1"): + error_reason = f"Boolean input expected, but got {str_value}" + + enabled = (str_value == "1") + + if error_reason is not None: + raise libioc.errors.InvalidJailConfigValue( + jail=self.jail, + property_name="mount_fdescfs", + reason=error_reason, + logger=self.logger + ) + + self.data[key] = ("1" if enabled else "0") + def __getitem__(self, key: str) -> typing.Any: """Get the value of a configuration argument or its default.""" try: diff --git a/libioc/Jail.py b/libioc/Jail.py index 6692db8e..178d8db7 100644 --- a/libioc/Jail.py +++ b/libioc/Jail.py @@ -31,6 +31,7 @@ import libzfs import freebsd_sysctl import jail as libjail +import freebsd_sysctl.types import libioc.Types import libioc.errors @@ -55,6 +56,10 @@ import libioc.ResourceSelector import libioc.Config.Jail.File.Fstab +import ctypes.util +import errno +_dll = ctypes.CDLL(str(ctypes.util.find_library("c")), use_errno=True) + class JailResource( libioc.LaunchableResource.LaunchableResource, @@ -518,17 +523,23 @@ def _stop_failed_jail( jailAttachEvent.add_rollback_step(_stop_failed_jail) jiov = libjail.Jiov(self._launch_params) - jid = libjail.dll.jail_set(jiov.pointer, len(jiov), 1) + jid = _dll.jail_set(jiov.pointer, len(jiov), 1) if jid > 0: self.__jid = jid yield jailAttachEvent.end() else: + error_code = ctypes.get_errno() + if error_code > 0: + error_name = errno.errorcode[error_code] + error_text = f"{error_code} [{error_name}]" + else: + error_text = jiov.errmsg.value.decode("UTF-8") error = libioc.errors.JailLaunchFailed( jail=self, + reason=error_text, logger=self.logger ) - error_text = jiov.errmsg.value.decode("UTF-8") yield jailAttachEvent.fail(error_text) raise error @@ -538,6 +549,9 @@ def _stop_failed_jail( # Mount Devfs yield from self.__mount_devfs(jailStartEvent.scope) + # Mount Fdescfs + yield from self.__mount_fdescfs(jailStartEvent.scope) + # Setup Network yield from self.__start_network(jailStartEvent.scope) @@ -601,62 +615,70 @@ def __mount_devfs( self, event_scope: typing.Optional['libioc.events.Scope']=None ) -> typing.Generator['libioc.events.MountDevFS', None, None]: + yield from self.__mount_in_jail( + filesystem="devfs", + mountpoint="/dev", + event=libioc.events.MountDevFS, + event_scope=event_scope, + ruleset=self.config["devfs_ruleset"] + ) + + def __mount_fdescfs( + self, + event_scope: typing.Optional['libioc.events.Scope']=None + ) -> typing.Generator['libioc.events.MountFdescfs', None, None]: + yield from self.__mount_in_jail( + filesystem="fdescfs", + mountpoint="/dev/fd", + event=libioc.events.MountFdescfs, + event_scope=event_scope + ) - event = libioc.events.MountDevFS( + def __mount_in_jail( + self, + filesystem: str, + mountpoint: str, + event: 'libioc.events.JailEvent', + event_scope: typing.Optional['libioc.events.Scope']=None, + **iov_data: str + ) -> typing.Generator['libioc.events.MountFdescfs', None, None]: + + _event = event( jail=self, scope=event_scope ) - yield event.begin() + yield _event.begin() - if self.config["mount_devfs"] is False: - yield event.skip("disabled") + if int(self.config[f"mount_{filesystem}"]) == 0: + yield _event.skip("disabled") return - devpath = f"{self.root_path}/dev" - try: - if os.path.islink(devpath) is True: + _mountpoint = str(f"{self.root_path}{mountpoint}") + self.require_relative_path(_mountpoint) + if os.path.islink(_mountpoint) or os.path.isfile(_mountpoint): raise libioc.errors.InsecureJailPath( - path=devpath, + path=_mountpoint, logger=self.logger ) - libioc.helpers.mount( - destination=devpath, - fstype="devfs" - ) except Exception as e: - yield event.fail(e) + yield _event.fail(str(e)) raise e + if os.path.isdir(_mountpoint) is False: + os.makedirs(_mountpoint, mode=0o555) - yield event.end() - - def __unmount_devfs( - self, - event_scope: typing.Optional['libioc.events.Scope']=None - ) -> typing.Generator['libioc.events.UnmountDevFS', None, None]: - - event = libioc.events.UnmountDevFS( - jail=self, - scope=event_scope - ) - yield event.begin() - - devpath = f"{self.root_path}/dev" - - if os.path.ismount(devpath) is False: - yield event.skip() - return try: - libioc.helpers.umount( - mountpoint=devpath, + libioc.helpers.mount( + destination=_mountpoint, + fstype=filesystem, logger=self.logger, - frce=True + ruleset=4 ) except Exception as e: - yield event.fail(e) + yield _event.fail(str(e)) raise e - yield event.end() + yield _event.end() @property def _zfs_share_storage( @@ -1544,10 +1566,6 @@ def devfs_ruleset(self) -> libioc.DevfsRules.DevfsRuleset: ruleset_line_position = self.host.devfs.index(devfs_ruleset) return self.host.devfs[ruleset_line_position].number - @staticmethod - def __get_launch_command(jail_args: typing.List[str]) -> typing.List[str]: - return ["/usr/sbin/jail", "-c"] + jail_args - @property def _launch_params(self) -> libjail.Jiov: config = self.config @@ -1555,10 +1573,6 @@ def _launch_params(self) -> libjail.Jiov: value: libjail.RawIovecValue jail_params: typing.Dict[str, libioc.JailParams.JailParam] = {} for sysctl_name, sysctl in libioc.JailParams.JailParams().items(): - if sysctl.ctl_type == freebsd_sysctl.types.NODE: - # skip NODE - continue - if sysctl_name == "security.jail.param.devfs_ruleset": value = int(self.devfs_ruleset) elif sysctl_name == "security.jail.param.path": @@ -1585,17 +1599,26 @@ def _launch_params(self) -> libjail.Jiov: value = [] for _, addresses in self.config["ip6_addr"].items(): value += [x.ip for x in addresses] + elif vnet and (sysctl_name.startswith("security.jail.param.ip")): + continue else: config_property_name = sysctl.iocage_name - if self.config._is_known_property( + if self.config.is_known_property( config_property_name, explicit=False ) is True: value = config[config_property_name] + if sysctl.ctl_type in ( + freebsd_sysctl.types.NODE, + freebsd_sysctl.types.INT, + ): + sysctl_state_names = ["disable", "inherit", "new"] + if value in sysctl_state_names: + value = sysctl_state_names.index(value) else: continue - jail_params[sysctl.jail_arg_name] = value + jail_params[sysctl.jail_arg_name.rstrip(".")] = value jail_params["persist"] = None return jail_params diff --git a/libioc/JailParams.py b/libioc/JailParams.py index 37675e34..0639f0cb 100644 --- a/libioc/JailParams.py +++ b/libioc/JailParams.py @@ -47,13 +47,11 @@ def value(self) -> JailParamValueType: @value.setter def value(self, value: JailParamValueType) -> None: """Set the user defined value of this jail parameter.""" - if self.ctl_type == freebsd_sysctl.types.NODE: - raise TypeError("sysctl NODE has no value") - - if self.ctl_type in [ + if self.ctl_type in ( freebsd_sysctl.types.STRING, freebsd_sysctl.types.OPAQUE, - ]: + freebsd_sysctl.types.NODE + ): if (isinstance(value, int) or isinstance(value, str)) is False: try: value = str(value) @@ -91,7 +89,7 @@ def jail_arg_name(self) -> str: @property def iocage_name(self) -> str: """Return the name of the param formatted for iocage config.""" - return self.jail_arg_name.replace(".", "_") + return self.jail_arg_name.rstrip(".").replace(".", "_") def __str__(self) -> str: """Return the jail command argument notation of the param.""" @@ -157,14 +155,15 @@ def memoized_params(self) -> typing.Dict[str, JailParam]: def __update_sysctl_jail_params(self) -> None: prefix = "security.jail.param" jail_params = filter( - lambda x: not any(( - x.name.endswith("."), # quick filter NODE - x.name == "security.jail.allow_raw_sockets", # deprecated - )), + # security.jail.allow_raw_sockets deprecated + lambda x: x.name != "security.jail.allow_raw_sockets", self.__base_class(prefix).children ) # permanently store the queried sysctl in the singleton class - JailParams.__sysctl_params = dict([(x.name, x,) for x in jail_params]) + JailParams.__sysctl_params = dict([ + (x.name.rstrip("."), x,) + for x in jail_params + ]) class HostJailParams(JailParams): diff --git a/libioc/Resource.py b/libioc/Resource.py index 9f070dbc..81eee955 100644 --- a/libioc/Resource.py +++ b/libioc/Resource.py @@ -365,10 +365,11 @@ def save(self) -> None: "This needs to be implemented by the inheriting class" ) - def _require_relative_path( + def require_relative_path( self, filepath: str, ) -> None: + """Require the resolved filepath to be relative to the jail root.""" if self._is_path_relative(filepath) is False: raise libioc.errors.SecurityViolationConfigJailEscape( file=filepath diff --git a/libioc/ResourceUpdater.py b/libioc/ResourceUpdater.py index 92dd0113..a3d7a64a 100644 --- a/libioc/ResourceUpdater.py +++ b/libioc/ResourceUpdater.py @@ -702,7 +702,7 @@ def _post_fetch(self) -> None: def _pre_update(self) -> None: """Execute before executing the update command.""" lnk = f"{self.resource.root_path}{self._base_release_symlink_location}" - self.resource._require_relative_path(f"{lnk}/..") + self.resource.require_relative_path(f"{lnk}/..") if os.path.islink(lnk) is True: os.unlink(lnk) os.symlink("/", lnk) @@ -710,7 +710,7 @@ def _pre_update(self) -> None: def _post_update(self) -> None: """Execute after executing the update command.""" lnk = f"{self.resource.root_path}{self._base_release_symlink_location}" - self.resource._require_relative_path(f"{lnk}/..") + self.resource.require_relative_path(f"{lnk}/..") os.unlink(lnk) diff --git a/libioc/Storage/__init__.py b/libioc/Storage/__init__.py index b527eab2..51ae50e1 100644 --- a/libioc/Storage/__init__.py +++ b/libioc/Storage/__init__.py @@ -130,6 +130,13 @@ def teardown( ) yield event.begin() + try: + for mountpoint in system_mountpoints: + self.jail.require_relative_path(mountpoint) + except Exception as e: + yield event.fail(str(e)) + raise e + has_unmounted_any = False try: for mountpoint in system_mountpoints: @@ -137,7 +144,8 @@ def teardown( continue libioc.helpers.umount( mountpoint=mountpoint, - force=True + force=True, + logger=self.logger ) has_unmounted_any = True except Exception: diff --git a/libioc/decorators.py b/libioc/decorators.py index 1dfaa01e..42660cad 100644 --- a/libioc/decorators.py +++ b/libioc/decorators.py @@ -22,7 +22,7 @@ # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. -"""Collection of iocage Python decorators.""" +"""Collection of ioc Python decorators.""" import functools import time diff --git a/libioc/errors.py b/libioc/errors.py index c40ec354..8c5dcdd7 100644 --- a/libioc/errors.py +++ b/libioc/errors.py @@ -231,9 +231,12 @@ class JailLaunchFailed(JailException): def __init__( self, jail: 'libioc.Jail.JailGenerator', + reason: typing.Optional[str]=None, logger: typing.Optional['libioc.Logger.Logger']=None ) -> None: msg = f"Launching jail {jail.full_name} failed" + if reason is not None: + msg += f": {reason}" JailException.__init__(self, message=msg, jail=jail, logger=logger) diff --git a/libioc/events.py b/libioc/events.py index 1e99e2e4..de9797bd 100644 --- a/libioc/events.py +++ b/libioc/events.py @@ -397,8 +397,8 @@ class MountDevFS(DevFSEvent): pass -class UnmountDevFS(DevFSEvent): - """Unmount /dev from a jail.""" +class MountFdescfs(DevFSEvent): + """Mount /dev/fd into a jail.""" pass diff --git a/libioc/helpers.py b/libioc/helpers.py index d98fd971..03475afa 100644 --- a/libioc/helpers.py +++ b/libioc/helpers.py @@ -539,13 +539,17 @@ def mount( destination: str, source: str=None, fstype: str="nullfs", - opts: typing.List[str]=[] + opts: typing.List[str]=[], + logger: typing.Optional['libioc.Logger.Logger']=None, + **iov_data: typing.Any ) -> None: """Mount a filesystem using libc.""" data: typing.Dict[str, typing.Optional[str]] = dict( fstype=fstype, fspath=destination ) + for key, value in iov_data.items(): + data[key] = str(value) if source is not None: data["target"] = source for opt in opts: @@ -554,7 +558,8 @@ def mount( if libjail.dll.nmount(jiov.pointer, len(jiov), 0) != 0: raise libioc.errors.MountFailed( mountpoint=destination, - reason=jiov.errmsg.value + reason=jiov.errmsg.value.decode("UTF-8"), + logger=logger ) diff --git a/requirements-dev.txt b/requirements-dev.txt index 7d648de1..51c59677 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -6,7 +6,7 @@ flake8-docstrings flake8-mutable flake8-builtins flake8-mypy -bandit +bandit==1.5.1 bandit-high-entropy-string sphinx-autodoc-typehints sphinx_rtd_theme diff --git a/requirements.txt b/requirements.txt index 3f0d57c2..f853105f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ gitpython -freebsd_sysctl==0.0.5 +freebsd_sysctl==0.0.6 jail==0.0.8 diff --git a/tests/test_Jail.py b/tests/test_Jail.py index 325bbab5..09b678f3 100644 --- a/tests/test_Jail.py +++ b/tests/test_Jail.py @@ -92,6 +92,79 @@ def test_can_be_started( assert stdout.strip().count("\n") == 0 + def test_can_mount_devfs( + self, + existing_jail: 'libioc.Jail.Jail' + ) -> None: + """Test if a jail can be started.""" + existing_jail.config["mount_devfs"] = True + existing_jail.config["mount_fdescfs"] = False + existing_jail.save() + + existing_jail.start() + stdout = subprocess.check_output( + [f"/sbin/mount | grep {existing_jail.root_dataset.name}"], + shell=True + ).decode("utf-8") + assert "/dev" in stdout + assert "/dev/fd" not in stdout + + existing_jail.stop() + stdout = subprocess.check_output( + [f"/sbin/mount | grep {existing_jail.root_dataset.name}"], + shell=True + ).decode("utf-8") + assert "/dev" not in stdout + + def test_can_mount_fdescfs( + self, + existing_jail: 'libioc.Jail.Jail' + ) -> None: + """Test if a jail can be started.""" + existing_jail.config["mount_devfs"] = False + existing_jail.config["mount_fdescfs"] = True + existing_jail.save() + + existing_jail.start() + stdout = subprocess.check_output( + [f"/sbin/mount | grep {existing_jail.root_dataset.name}"], + shell=True + ).decode("utf-8") + assert "/dev/fd" in stdout + assert "/dev (" not in stdout + + existing_jail.stop() + stdout = subprocess.check_output( + [f"/sbin/mount | grep {existing_jail.root_dataset.name}"], + shell=True + ).decode("utf-8") + assert "/dev/fd" not in stdout + + def test_can_mount_devfs_and_fdescfs( + self, + existing_jail: 'libioc.Jail.Jail' + ) -> None: + """Test if a jail can be started.""" + existing_jail.config["mount_devfs"] = True + existing_jail.config["mount_fdescfs"] = True + existing_jail.save() + + existing_jail.start() + stdout = subprocess.check_output( + [f"/sbin/mount | grep {existing_jail.root_dataset.name}"], + shell=True + ).decode("utf-8") + assert "/dev (" in stdout + assert "/dev/fd" in stdout + + existing_jail.stop() + stdout = subprocess.check_output( + [f"/sbin/mount | grep {existing_jail.root_dataset.name}"], + shell=True + ).decode("utf-8") + assert "/dev (" not in stdout + assert "/dev/fd" not in stdout + def test_can_be_stopped( self, existing_jail: 'libioc.Jail.Jail'