3535import shlex
3636import signal
3737import shutil
38+ import enum
3839
3940from contextlib import suppress
4041from 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
809917def 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 )
0 commit comments