def _mount_image_file( unshare: Optional[Unshare], file_path: bytes, mount_path: bytes, ) -> bytes: log.info(f'Mounting btrfs {file_path} at {mount_path}') # Explicitly set filesystem type to detect shenanigans. run_stdout_to_err(nsenter_as_root( unshare, 'mount', '-t', 'btrfs', '-o', 'loop,discard,nobarrier', file_path, mount_path, ), check=True) loop_dev = subprocess.check_output(nsenter_as_user( unshare, 'findmnt', '--noheadings', '--output', 'SOURCE', mount_path, )).rstrip(b'\n') # This increases the chances that --direct-io=on will succeed, since one # of the common failure modes is that the loopback's sector size is NOT # a multiple of the sector size of the underlying device (the devices # we've seen in production have sector sizes of 512, 1024, or 4096). if run_stdout_to_err([ 'sudo', 'losetup', '--sector-size=4096', loop_dev, ]).returncode != 0: log.error( f'Failed to set --sector-size=4096 for {loop_dev}, setting ' 'direct IO is more likely to fail.' ) # This helps perf and avoids doubling our usage of buffer cache. # Also, when the image is on tmpfs, setting direct IO fails. if run_stdout_to_err([ 'sudo', 'losetup', '--direct-io=on', loop_dev, ]).returncode != 0: log.error( f'Could not enable --direct-io for {loop_dev}, expect worse ' 'performance.' ) return loop_dev
def _minimize_image_size( *, unshare: Optional[Unshare], cur_size: int, image_path: bytes, mount_path: bytes, loop_dev: bytes, ) -> int: 'Returns the new filesystem size.' min_size_out = subprocess.check_output(nsenter_as_root( unshare, 'btrfs', 'inspect-internal', 'min-dev-size', mount_path, )).split(b' ') assert min_size_out[1] == b'bytes' min_size = _fix_up_fs_size(int(min_size_out[0]), MIN_SHRINK_BYTES) if min_size >= cur_size: log.info( f'Nothing to do: the minimum resize limit {min_size} is no less ' f'than the current filesystem size of {cur_size} bytes.' ) return log.info(f'Shrinking {image_path} to the btrfs minimum, {min_size} bytes') run_stdout_to_err(nsenter_as_root( unshare, 'btrfs', 'filesystem', 'resize', str(min_size), mount_path, ), check=True) fs_bytes = int(subprocess.check_output(nsenter_as_user( unshare, 'findmnt', '--bytes', '--noheadings', '--output', 'SIZE', mount_path, ))) # Log an error on size rounding since this is not expected to need it. _create_or_resize_image_file(image_path, fs_bytes, log_level=logging.ERROR) run_stdout_to_err([ 'sudo', 'losetup', '--set-capacity', loop_dev, ], check=True) return min_size
def check_mnt_dest(mnt_dest: str): cypa = os.path.join(mnt_dest, 'cypa') # The outer NS cannot see the mount self.assertFalse(os.path.exists(cypa)) # But we can read it from inside the namespace self.assertEqual( b'kvoh', subprocess.check_output( nsenter_as_user(unshare, 'cat', cypa), ))
def test_pid_namespace(self): with Unshare([Namespace.PID]) as unshare: proc, _ = self._popen_sleep_forever(unshare) # Check that "as user" works. for arg, expected in (('-u', os.geteuid()), ('-g', os.getegid())): actual = int( subprocess.check_output(nsenter_as_user( unshare, 'id', arg))) self.assertEqual(expected, actual) time.sleep(2) # Leave some time for `sleep` to exit erroneously self.assertEqual(None, proc.poll()) # Sleeps forever self._check_ns_diff(unshare, {'pid'}) self.assertEqual(-signal.SIGKILL, proc.poll()) # Reaped by PID NS
def _send_to_loopback_if_fits(self, output_path, fs_size_bytes) -> bool: ''' Creates a loopback of the specified size, and sends the current subvolume to it. Returns True if the subvolume fits in that space. ''' open(output_path, 'wb').close() with pipe() as (r_send, w_send), \ Unshare([Namespace.MOUNT, Namespace.PID]) as ns, \ LoopbackVolume(ns, output_path, fs_size_bytes) as loop_vol, \ self.mark_readonly_and_write_sendstream_to_file(w_send): w_send.close() # This end is now fully owned by `btrfs send`. with r_send: recv_ret = run_stdout_to_err(nsenter_as_root( ns, 'btrfs', 'receive', loop_vol.dir(), ), stdin=r_send, stderr=subprocess.PIPE) if recv_ret.returncode != 0: if recv_ret.stderr.endswith(self._OUT_OF_SPACE_SUFFIX): return False # It's pretty lame to rely on `btrfs receive` continuing # to be unlocalized, and emitting that particular error # message, so we fall back to checking available bytes. size_ret = subprocess.run(nsenter_as_user( ns, 'findmnt', '--noheadings', '--bytes', '--output', 'AVAIL', loop_vol.dir(), ), stdout=subprocess.PIPE) # If the `findmnt` fails, don't mask the original error. if size_ret.returncode == 0 and int(size_ret.stdout) == 0: return False # Covering this is hard, so the test plan is "inspection". log.error( # pragma: no cover 'Unhandled receive stderr:\n\n' + recv_ret.stderr.decode(errors='surrogateescape'), ) recv_ret.check_returncode() loop_vol.minimize_size() return True
def test_pid_namespace_dead_keepalive(self): with Unshare([Namespace.PID]) as unshare: self._check_ns_diff(unshare, {'pid'}) good_echo = nsenter_as_user(unshare, 'echo') subprocess.check_call(good_echo) # Will fail once the NS is dead proc, _ = self._popen_sleep_forever(unshare) time.sleep(2) # Leave some time for `sleep` to exit erroneously self.assertEqual(None, proc.poll()) # Sleeps forever self._kill_keepalive(unshare) self.assertEqual(-signal.SIGKILL, proc.wait()) # The NS is dead # The `echo` command that worked above no longer works. with self.assertRaises(subprocess.CalledProcessError): subprocess.check_call(good_echo)
def _popen_sleep_forever(self, unshare: Unshare): # We need the ready signal to know when we've actually executed the # payload -- otherwise, we might try to interact with it while we're # still at `nsenter`. proc = subprocess.Popen(nsenter_as_user( unshare, 'bash', '-uec', 'echo ready $$ ; exec sleep infinity', ), stdout=subprocess.PIPE) # Wait for the child to start ready_and_pid = proc.stdout.readline().split(b' ') self.assertEqual(b'ready', ready_and_pid[0]) proc.stdout.close() # `sudo` keeps stdout open, but will not write. # Returning the PID lets us clean up the `sleep infinity` when it is # not inside a PID namespace. return proc, int(ready_and_pid[1])
def test_nsenter_wrappers(self): self.assertEqual(('a', 'b'), nsenter_as_user(None, 'a', 'b')) self.assertEqual(('sudo', 'c', 'd'), nsenter_as_root(None, 'c', 'd'))