Skip to content

Commit 1ff1890

Browse files
committed
Refactor mounting code in its own function
This also adds enums to control what to do, which makes it more readable and skips the function on missing snap function right away instead of wrapping all in the if
1 parent ad2534f commit 1ff1890

File tree

2 files changed

+129
-69
lines changed

2 files changed

+129
-69
lines changed

checkbox-ng/plainbox/impl/execution.py

Lines changed: 117 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import shlex
3636
import signal
3737
import shutil
38+
import enum
3839

3940
from contextlib import suppress
4041
from subprocess import check_output, check_call, run
@@ -805,6 +806,113 @@ def dangerous_nsenter(path):
805806
run(get_plz_run(["rm", path]))
806807

807808

809+
class MountingStrategy(enum.Enum):
810+
# mount is not needed in this context
811+
DONT_MOUNT = enum.auto()
812+
# mount is needed but not permission is required
813+
MOUNT_ROOT = enum.auto()
814+
# mount is needed and permission has to be aquired via dangerous nsenter
815+
MOUNT_DANGEROUS_NSENTER = enum.auto()
816+
# mount is needed and permission has to be aquired via ambient capabilities
817+
# this is the preferred option when available and needed
818+
MOUNT_AMBIENT_CAPABILITIES = enum.auto()
819+
820+
@classmethod
821+
def from_user_core(cls, job_user, on_core, snap_base):
822+
if not on_core:
823+
return cls.DONT_MOUNT
824+
if job_user == "root":
825+
return cls.MOUNT_ROOT
826+
if snap_base == "core16":
827+
# core16 doesn't support AmbientCapabilities via dbus
828+
return cls.MOUNT_DANGEROUS_NSENTER
829+
return cls.MOUNT_AMBIENT_CAPABILITIES
830+
831+
832+
def get_snap_mount_namespace_commands(target_user, shared_location):
833+
"""
834+
Returns commands and helpers to mount the current snap namespace
835+
836+
This is necessary because when inside a core snap, some binaries may
837+
pass through a content interface. This makes their path "invalid" to any
838+
process that doesn't mount the same mount namespace. This function returns
839+
said commands (to be appended to the unit command), along with some
840+
commands to append to the script (to fix the process permissions) and a
841+
helper that will make the commands in the script work on some systems.
842+
843+
The returned values from this function are always valid, they are no-op
844+
on systems where they aren't needed
845+
"""
846+
wrapper_cmd = []
847+
cmd = []
848+
namespace_mounting_helper = suppress()
849+
on_core = on_ubuntucore()
850+
snap_base = None if not on_core else get_snap_base()
851+
852+
mounting_strategy = MountingStrategy.from_user_core(
853+
target_user, on_core, snap_base
854+
)
855+
if mounting_strategy == MountingStrategy.DONT_MOUNT:
856+
# when not on ubuntucore, there is no snap namespace to mount
857+
return wrapper_cmd, cmd, namespace_mounting_helper
858+
dangerous_nsenter_path = None
859+
snap_base = get_snap_base()
860+
861+
# mounting namespaces is not allowed as non-root, the following makes it
862+
# possible
863+
if mounting_strategy == MountingStrategy.MOUNT_DANGEROUS_NSENTER:
864+
# here we need a dangerous copy of nsenter that works as "normal"
865+
# user because the unit will be normal user and else it wont be
866+
# able to mount the snap namespace. This only on core16 because
867+
# other distros support setting AmbientCapabilities via dbus API
868+
# making that a better, more secure alternative
869+
with tempfile.NamedTemporaryFile(
870+
mode="w", delete=False, prefix="nsenter_", dir=shared_location
871+
) as f:
872+
dangerous_nsenter_path = f.name
873+
namespace_mounting_helper = dangerous_nsenter(dangerous_nsenter_path)
874+
elif mounting_strategy == MountingStrategy.MOUNT_AMBIENT_CAPABILITIES:
875+
# on core > 16 we can set AmbientCapabilities without using fs caps
876+
# These are the capabilities needed to mount the namespace
877+
# from linux/capability.h
878+
# uint64(1 << 21| 1<<18 | 1<<6 | 1<<7)
879+
CAP_SETGID = 6 # necessary for setpriv
880+
CAP_SETUID = 7 # necessary for setpriv
881+
CAP_SYS_CHROOT = 18 # necessary for nsenter
882+
CAP_SYS_ADMIN = 21 # necessary for nsenter
883+
ambient_capabilities_bitset = (
884+
1 << CAP_SETGID
885+
| 1 << CAP_SETUID
886+
| 1 << CAP_SYS_CHROOT
887+
| 1 << CAP_SYS_ADMIN
888+
)
889+
wrapper_cmd += [
890+
"-ambient-capabilities",
891+
str(ambient_capabilities_bitset),
892+
]
893+
# these binaries are not reliably shipped / may not be in path
894+
runtime_path = get_checkbox_runtime_path()
895+
runtime_nsenter = runtime_path / "usr" / "bin" / "nsenter"
896+
897+
snap_name = os.getenv("SNAP_NAME", "checkbox")
898+
cmd += [
899+
(
900+
str(runtime_nsenter)
901+
if dangerous_nsenter_path is None
902+
else str(dangerous_nsenter_path)
903+
),
904+
"-m/run/snapd/ns/{}.mnt".format(snap_name),
905+
]
906+
if mounting_strategy == MountingStrategy.MOUNT_AMBIENT_CAPABILITIES:
907+
# on non-core16 we have given ourselves AmbientCapabilities. After
908+
# using them for what we needed (mounting the namespace) we must
909+
# drop them else the "user" test will have way more priviledges
910+
# than it is supposed to
911+
runtime_setpriv = runtime_path / "usr" / "bin" / "setpriv"
912+
cmd += [str(runtime_setpriv), "--inh-caps=-all"]
913+
return wrapper_cmd, cmd, namespace_mounting_helper
914+
915+
808916
@contextlib.contextmanager
809917
def get_execution_command_systemd_unit(
810918
job, environ, session_id, nest_dir, target_user, extra_env=None
@@ -837,67 +945,19 @@ def get_execution_command_systemd_unit(
837945
if target_user != "root":
838946
wrapper_cmd += ["-pam", "system-login"]
839947
cmd = []
840-
dangerous_nsenter_path = None
841948
# this location must be accessible and in the same path for both this
842-
# process (that may be inside a snap) and the systemd unit (which is not)
843-
# fallback mechanism is for debian/source checkbox
949+
# process (that may be inside a snap) a systemd unit, debian or source
950+
# checkbox. All users must be able to read and write to it.
844951
# DONT use SNAP_COMMON (not writable by normal user)
845952
# DONT use SNAP_USER_COMMON (not writable by normal user if running as root)
846953
shared_location = "/var/tmp"
847-
core_snap = on_ubuntucore()
848954
# when in a core snap, we need the snap mount namespace to use anything
849955
# that was shared via a content interface
850-
if core_snap:
851-
snap_base = get_snap_base()
852-
if target_user != "root" and snap_base == "core16":
853-
# here we need a dangerous copy of nsenter that works as "normal"
854-
# user because the unit will be normal user and else it wont be
855-
# able to mount the snap namespace. This only on core16 because
856-
# other distros support setting AmbientCapabilities via dbus API
857-
# making that a better, more secure alternative
858-
with tempfile.NamedTemporaryFile(
859-
mode="w", delete=False, prefix="nsenter_", dir=shared_location
860-
) as f:
861-
dangerous_nsenter_path = f.name
862-
elif target_user != "root":
863-
# on core > 16 we can set AmbientCapabilities without using fs caps
864-
# These are the capabilities needed to mount the namespace
865-
# from linux/capability.h
866-
# uint64(1 << 21| 1<<18 | 1<<6 | 1<<7))}
867-
CAP_SETGID = 6 # necessary for setpriv
868-
CAP_SETUID = 7 # necessary for setpriv
869-
CAP_SYS_CHROOT = 18 # necessary for nsenter
870-
CAP_SYS_ADMIN = 21 # necessary for nsenter
871-
ambient_capabilities_bitset = (
872-
1 << CAP_SETGID
873-
| 1 << CAP_SETUID
874-
| 1 << CAP_SYS_CHROOT
875-
| 1 << CAP_SYS_ADMIN
876-
)
877-
wrapper_cmd += [
878-
"-ambient-capabilities",
879-
str(ambient_capabilities_bitset),
880-
]
881-
# these binaries are not reliably shipped / may not be in path
882-
runtime_path = get_checkbox_runtime_path()
883-
runtime_nsenter = runtime_path / "usr" / "bin" / "nsenter"
884-
runtime_setpriv = runtime_path / "usr" / "bin" / "setpriv"
885-
886-
snap_name = os.getenv("SNAP_NAME", "checkbox")
887-
cmd += [
888-
(
889-
str(runtime_nsenter)
890-
if dangerous_nsenter_path is None
891-
else dangerous_nsenter_path
892-
),
893-
"-m/run/snapd/ns/{}.mnt".format(snap_name),
894-
]
895-
if snap_base != "core16":
896-
# on non-core16 we have given ourselves AmbientCapabilities. After
897-
# using them for what we needed (mounting the namespace) we must
898-
# drop them else the "user" test will have way more priviledges
899-
# than it is supposed to
900-
cmd += [str(runtime_setpriv), "--inh-caps=-all"]
956+
extra_wrapper_cmd, extra_cmd, namespace_mounting_helper = (
957+
get_snap_mount_namespace_commands(target_user, shared_location)
958+
)
959+
wrapper_cmd += extra_wrapper_cmd
960+
cmd += extra_cmd
901961
env = get_execution_environment(job, environ, session_id, nest_dir)
902962
if extra_env:
903963
env.update(extra_env())
@@ -922,11 +982,9 @@ def get_execution_command_systemd_unit(
922982
path = f.name
923983

924984
wrapper_cmd.append(path)
925-
# dangerous_nsenter will create the dangerous version only if it is needed
926-
# it is a no-op on non-core16 snaps
927-
with dangerous_nsenter(dangerous_nsenter_path):
985+
with namespace_mounting_helper:
928986
try:
929987
yield wrapper_cmd
930988
finally:
931989
with suppress(OSError):
932-
os.remove(f.name)
990+
os.remove(path)

checkbox-ng/plainbox/impl/unit/unit.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -79,19 +79,21 @@ def get_snap_base():
7979
Get the current snap base
8080
"""
8181
snap = os.getenv("SNAP")
82-
if snap:
83-
with open(os.path.join(snap, "meta/snap.yaml")) as f:
84-
for l in f.readlines():
85-
if "base:" in l:
86-
core_version = l.replace("base:", "").strip()
87-
if core_version == "core":
88-
return "core16"
89-
return core_version
90-
return "core16" # core16 may not declare base
91-
raise ValueError("Couldn't detect snap path. Missing SNAP envvar")
82+
if not snap:
83+
raise ValueError("Couldn't detect snap path. Missing SNAP envvar")
84+
with open(os.path.join(snap, "meta/snap.yaml")) as f:
85+
for l in f.readlines():
86+
if "base:" in l:
87+
core_version = l.replace("base:", "").strip()
88+
if core_version == "core":
89+
return "core16"
90+
return core_version
91+
return "core16" # core16 may not declare base
9292

9393

9494
def get_checkbox_runtime_path():
95+
# Note: this is not the same as using SNAP as this will always be the
96+
# runtime, while SNAP on the legacy arch is the frontend
9597
snap_base_n = get_snap_base().replace("core", "")
9698
# the bases of the runtime and frontend must always match
9799
runtime_name = "checkbox" + snap_base_n

0 commit comments

Comments
 (0)