├── .gitignore ├── MANIFEST.in ├── README.md ├── kvm.config ├── setup.py ├── uml.config ├── vido └── virt-stub /.gitignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /dist/ 3 | /MANIFEST 4 | /*.egg-info/ 5 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include kvm.config uml.config 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # vido 3 | 4 | `vido` is a kernel launcher. It is used much like sudo, by putting 5 | `vido --` in front of a command. 6 | Commands run inside a new kernel, with passthrough access 7 | to the filesystem, whitelisted devices, and (if enabled) the network. 8 | 9 | The main uses are: 10 | 11 | - **Privilege virtualisation.** `vido` starts out entirely unprivileged, 12 | and creates an environment where commands run as root without affecting 13 | the rest of the system. This is a more powerful alternative to `fakeroot`; 14 | it allows full access to a possibly customised kernel. 15 | - **Regression testing.** Run the same command against multiple kernels. 16 | - **Kernel debugging.** The `--gdb` flag runs the virtual 17 | kernel inside a debugger. If you have an application that 18 | triggers kernel bugs, you can wrap it in `vido --gdb`, usually 19 | without changes. 20 | - **Kernel hacking.** Experiment with small changes to the kernel 21 | and test them immediately. 22 | 23 | Get overlay access to privileged directories with `--clear-dirs` 24 | and `--rw-dirs`. This requires Linux 3.18. 25 | 26 | Pass disk images or block devices with `--disk`. 27 | They are exposed as `$VIDO_DISK0`… variables. 28 | 29 | Aside from the default pass-throughs, commands run in a fairly 30 | bare environment. If more services are needed, pass a script 31 | that will launch them. For example, launching udev/eudev gives 32 | udev support. 33 | 34 | With network passthrough (`--net`), commands can do unprivileged 35 | networking (a SLIRP stack, with IPv4 NAT). The `ping` command won't work 36 | unless [patched](http://openwall.info/wiki/people/segoon/ping#Userspace-support) 37 | to use [ICMP sockets](https://lwn.net/Articles/420799/). 38 | 39 | # Usage 40 | 41 | The default command is a shell: 42 | 43 | vido 44 | 45 | Always put two dashes before the command: 46 | 47 | vido -- cat /proc/uptime 48 | vido -- sh -c 'dmesg |tail' 49 | 50 | Most flags should be self-documenting: 51 | 52 | vido --help 53 | 54 | # Requirements 55 | 56 | You need Python 3.3 57 | 58 | There are two main implementations, UML and KVM. 59 | In both cases you need a suitable kernel for the guest. 60 | 61 | ## UML 62 | 63 | On Ubuntu and Debian, 64 | 65 | sudo apt-get install user-mode-linux 66 | 67 | installs a UML kernel which you can run with: 68 | 69 | vido --uml 70 | 71 | You can also download UML kernels from 72 | , or build your own: 73 | 74 | vido --uml --kernel path/to/linux 75 | 76 | ## Qemu / KVM 77 | 78 | You may be able to use your current kernel: 79 | 80 | sudo chmod a+r /boot/vmlinuz-* 81 | vido --kvm --qemu-9p-workaround --watchdog 82 | 83 | This is designed to work with distribution kernels that don't 84 | have 9p modules built-in. 85 | `--qemu-9p-workaround` is required if Qemu is older than 1.6. 86 | 87 | If the distribution kernel isn't suitable, build a minimal kernel with: 88 | 89 | CONFIG_NET_9P=y 90 | CONFIG_NET_9P_VIRTIO=y 91 | CONFIG_9P_FS=y 92 | CONFIG_DEVTMPFS=y 93 | CONFIG_SERIAL_8250_CONSOLE=y 94 | 95 | Note that 9p can't be built as a loadable module, it has to be built in. 96 | Your kernel should also have: 97 | 98 | CONFIG_DEVTMPFS_MOUNT=y 99 | CONFIG_9P_FSCACHE=y 100 | CONFIG_OVERLAY_FS=y 101 | # networking 102 | CONFIG_E1000=y 103 | CONFIG_PACKET=y 104 | # watchdog 105 | CONFIG_IB700_WDT=y 106 | 107 | Usage: 108 | 109 | vido --kvm --kernel path/to/arch/x86/boot/bzImage 110 | 111 | ## User namespaces 112 | 113 | As an alternative to UML and KVM, `vido` can also use user namespaces. 114 | This is a recent kernel feature, less powerful than kernel 115 | virtualisation (you become root, but without the ability to take 116 | over the kernel and without many unvirtualised kernel features) but 117 | powerful enough to allow some control over mountpoints. 118 | 119 | If `CONFIG_USER_NS` is not supported by your host kernel, you may need 120 | to upgrade or rebuild it. Note that `CONFIG_USER_NS` clashes with XFS 121 | in pre-3.12 kernels. 122 | 123 | -------------------------------------------------------------------------------- /kvm.config: -------------------------------------------------------------------------------- 1 | # Applies on top of kvm_guest.config 2 | # Run the following from a Linux kernel checkout: 3 | # scripts/kconfig/merge_config.sh -n kernel/configs/kvm_guest.config ~/src/github.com/g2p/vido/kvm.config 4 | 5 | CONFIG_64BIT=y 6 | CONFIG_PRINTK=y 7 | CONFIG_BINFMT_SCRIPT=y 8 | CONFIG_PROC_FS=y 9 | CONFIG_SYSFS=y 10 | CONFIG_SHMEM=y 11 | CONFIG_TMPFS=y 12 | # For poweroff 13 | CONFIG_ACPI=y 14 | 15 | CONFIG_VIRTIO_PCI_LEGACY=y 16 | CONFIG_NET_9P=y 17 | CONFIG_NET_9P_VIRTIO=y 18 | CONFIG_9P_FS=y 19 | CONFIG_DEVTMPFS=y 20 | CONFIG_SERIAL_8250_CONSOLE=y 21 | CONFIG_DEVTMPFS_MOUNT=y 22 | CONFIG_FSCACHE=y 23 | CONFIG_9P_FSCACHE=y 24 | CONFIG_OVERLAY_FS=y 25 | CONFIG_ETHERNET=y 26 | CONFIG_NET_VENDOR_INTEL=y 27 | CONFIG_E1000=y 28 | CONFIG_PACKET=y 29 | CONFIG_WATCHDOG=y 30 | CONFIG_IB700_WDT=y 31 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # encoding: utf-8 3 | 4 | from distutils.core import setup 5 | 6 | setup( 7 | name='vido', 8 | version='0.3.2', 9 | author='Gabriel de Perthuis', 10 | author_email='g2p.code+vido@gmail.com', 11 | url='https://github.com/g2p/vido', 12 | license='GNU GPL', 13 | keywords= 14 | 'kvm uml debugging testing ci gdb virtualisation ' 15 | 'sudo unshare fakeroot wrapper', 16 | description='Wrap commands in throwaway virtual machines', 17 | scripts=['vido', 'virt-stub'], 18 | classifiers=''' 19 | Programming Language :: Python :: 3 20 | Programming Language :: Python :: 3.3 21 | License :: OSI Approved :: GNU General Public License (GPL) 22 | Operating System :: POSIX :: Linux 23 | Topic :: Utilities 24 | Environment :: Console 25 | '''.strip().splitlines(), 26 | long_description=''' 27 | vido is a sudo-like command wrapper. Commands run inside a new 28 | kernel, with passthrough access to the filesystem, whitelisted 29 | devices, and (if enabled) the network. 30 | 31 | The main uses are: 32 | 33 | * Experimentation. Make small changes to the kernel and test them 34 | immediately. 35 | * Privilege elevation. Commands run as root even if you don't have 36 | root privileges in the first place. 37 | * Regression testing. Run the same command against multiple kernels. 38 | * Kernel debugging. The --gdb flag will run the virtual kernel 39 | inside a debugger. 40 | 41 | See `github.com/g2p/vido `_ 42 | for installation and usage instructions.''') 43 | 44 | -------------------------------------------------------------------------------- /uml.config: -------------------------------------------------------------------------------- 1 | # From a Linux kernel checkout: 2 | # ARCH=um scripts/kconfig/merge_config.sh -n arch/um/configs/x86_64_defconfig ~/src/github.com/g2p/vido/uml.config 3 | CONFIG_64BIT=y 4 | CONFIG_BINFMT_SCRIPT=y 5 | CONFIG_BINFMT_ELF=y 6 | CONFIG_TMPFS=y 7 | CONFIG_VIRTIO=y 8 | CONFIG_VIRTIO_UML=y 9 | CONFIG_NETDEVICES=y 10 | CONFIG_NET_9P=y 11 | CONFIG_NET_9P_VIRTIO=y 12 | CONFIG_NETWORK_FILESYSTEMS=y 13 | CONFIG_FSCACHE=y 14 | CONFIG_9P_FS=y 15 | CONFIG_9P_FSCACHE=y 16 | CONFIG_DEVTMPFS=y 17 | CONFIG_DEVTMPFS_MOUNT=y 18 | CONFIG_OVERLAY_FS=y 19 | -------------------------------------------------------------------------------- /vido: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 -S 2 | import argparse 3 | import ctypes 4 | import errno 5 | import json 6 | import os 7 | import pwd 8 | import shutil 9 | import subprocess 10 | import sys 11 | import tempfile 12 | import types 13 | import urllib.parse 14 | 15 | 16 | def path_resolve(cmd): 17 | cmd0 = cmd 18 | 19 | if '/' not in cmd: 20 | cmd = shutil.which(cmd) 21 | assert cmd, cmd0 22 | 23 | cmd = os.path.abspath(cmd) 24 | assert os.path.exists(cmd), cmd0 25 | return cmd 26 | 27 | 28 | def fatal(msg): 29 | print(msg, file=sys.stderr) 30 | sys.exit(2) 31 | 32 | 33 | try: 34 | assert False 35 | except AssertionError: 36 | pass 37 | else: 38 | fatal('Assertions must be enabled') 39 | 40 | script = path_resolve(sys.argv[0]) 41 | 42 | assert script.endswith('/vido') 43 | runner = os.path.dirname(script) + '/virt-stub' 44 | assert os.path.exists(runner) 45 | 46 | 47 | def kopt_safe(st): 48 | # The kernel cmdline doesn't support any kind of quoting 49 | # Mustn't contain any whitespace (see string.whitespace, ' \t\n\r\v\f') 50 | return len(st.split()) == 1 51 | 52 | assert kopt_safe(runner) 53 | 54 | # We only really need to quote whitespace and escape unicode 55 | # maybe str.replace would suffice 56 | KCMD_SAFE = ":/?#[]@" + "!$&'()*+,;=" + "{}\"\\" 57 | assert kopt_safe(KCMD_SAFE) 58 | conf = types.SimpleNamespace() 59 | 60 | 61 | def mkinitramfs(irf, rootflags, runner, modbase): 62 | mods = [] 63 | builtin_mods = set( 64 | li.rstrip() for li in open(modbase + 'modules.builtin').readlines()) 65 | def add_mod(path): 66 | base = os.path.basename(path) 67 | if os.path.exists(modbase + path): 68 | shutil.copyfile(modbase + path, tdir + base) 69 | mods.append(base) 70 | elif os.path.exists(modbase + path + '.gz'): 71 | with open(tdir + base, 'xb') as out: 72 | with gzip.open(modbase + path + '.gz') as fin: 73 | shutil.copyfileobj(fin, out) 74 | elif path in builtin_mods: 75 | pass 76 | else: 77 | fatal('Module {} not found and not built-in'.format(path)) 78 | with tempfile.TemporaryDirectory(suffix='.initramfs') as tdir: 79 | tdir += '/' 80 | add_mod('kernel/drivers/virtio/virtio_ring.ko') 81 | add_mod('kernel/drivers/virtio/virtio.ko') 82 | add_mod('kernel/drivers/virtio/virtio_pci.ko') 83 | add_mod('kernel/net/9p/9pnet.ko') 84 | add_mod('kernel/net/9p/9pnet_virtio.ko') 85 | add_mod('kernel/fs/fscache/fscache.ko') 86 | add_mod('kernel/fs/9p/9p.ko') 87 | add_mod('kernel/drivers/net/virtio_net.ko') 88 | add_mod('kernel/fs/overlayfs/overlay.ko') 89 | with open(tdir + '/init', 'x') as initf: 90 | initf.write('''#!/busybox sh 91 | /busybox mkdir /bin 92 | /busybox --install /bin 93 | {insmod} 94 | mount -n -o {rootflags} -t 9p /dev/root /root 95 | exec switch_root /root {runner} "$@" 96 | '''.format(rootflags=rootflags, runner=runner, 97 | insmod='\n'.join( 98 | 'insmod /' + mod for mod in mods))) 99 | os.chmod(initf.fileno(), int('555', 8)) 100 | 101 | # static 102 | try: 103 | shutil.copy('/bin/busybox', tdir) 104 | except FileNotFoundError: 105 | shutil.copy('/usr/sbin/busybox', tdir) 106 | 107 | with subprocess.Popen( 108 | 'cpio --quiet -o -H newc'.split(), 109 | stdin=subprocess.PIPE, stdout=irf, cwd=tdir 110 | ) as proc: 111 | proc.stdin.write( 112 | '\n'.join('busybox init'.split() + mods).encode()) 113 | assert proc.returncode == 0 114 | 115 | 116 | def drop_privs(user): 117 | # https://www.securecoding.cert.org/confluence/display/seccode/POS36-C.+Observe+correct+revocation+order+while+relinquishing+privileges 118 | # https://www.securecoding.cert.org/confluence/display/seccode/POS37-C.+Ensure+that+privilege+relinquishment+is+successful 119 | pw = pwd.getpwnam(user) 120 | os.setgroups([]) 121 | os.setgid(pw.pw_gid) 122 | os.setuid(pw.pw_uid) 123 | 124 | # See CERT above, and Qemu does it too 125 | # AIUI this is useful with ancient kernels 126 | # or when starting with mixed credentials 127 | try: 128 | os.setuid(0) 129 | except PermissionError: 130 | pass 131 | else: 132 | raise RuntimeError('Failed to drop privileges') 133 | 134 | 135 | def quote_config(conf): 136 | return urllib.parse.quote( 137 | json.dumps(vars(conf), separators=(',', ':')), safe=KCMD_SAFE) 138 | 139 | # HOME and TERM seem to be set by the kernel (as well as root and the kernel 140 | # cmdline), the rest would be set by a shell 141 | ALWAYS_PASS_ENV = 'HOME TERM PATH SHELL'.split() 142 | 143 | parser = argparse.ArgumentParser() 144 | sp_virt = parser.add_argument_group('Choice of virtualisation') 145 | sp_virt.add_argument( 146 | '--uml', action='store_const', dest='virt', const='uml', 147 | help='Run a UML kernel (current default)') 148 | sp_virt.add_argument( 149 | '--kvm', action='store_const', dest='virt', const='kvm', 150 | help='Run a standard kernel with QEMU and KVM') 151 | sp_virt.add_argument( 152 | '--userns', action='store_const', dest='virt', const='userns', 153 | help='Don\'t enter a new kernel, just a new user namespace' 154 | ' (needs CONFIG_USER_NS)') 155 | sp_virt.set_defaults(virt='uml') 156 | 157 | # no virt-specific handling required 158 | sp_common = parser.add_argument_group('Common options') 159 | sp_common.add_argument( 160 | '--pass-env', dest='pass_env', 161 | nargs='+', default=[], metavar='NAME', 162 | help='Environment variables to preserve') 163 | sp_common.add_argument( 164 | '--clear-files', dest='clear_files', default=[], nargs='+', metavar='FILE', 165 | help='Files that will become empty and writable') 166 | sp_common.add_argument( 167 | '--clear-dirs', dest='clear_dirs', default=[], nargs='+', metavar='DIR', 168 | help='Directories that will become empty and writable') 169 | sp_common.add_argument( 170 | '--rw-dirs', dest='rw_dirs', default=[], nargs='+', metavar='DIR', 171 | help='Directories that will pass reads through and redirect writes' 172 | ' to a temporary overlay') 173 | sp_common.add_argument( 174 | '--allow-sudo', dest='allow_sudo', action='store_true', 175 | help='Enable permissive sudo') 176 | 177 | # need kernel virt 178 | sp_kernel = parser.add_argument_group('Kernel options') 179 | sp_kernel.add_argument( 180 | '--kernel', metavar='KERNEL', help='Kernel executable') 181 | sp_kernel.add_argument( 182 | '--kernel-version', metavar='VERSION', help='Installed kernel version') 183 | sp_kernel.add_argument( 184 | '--mem', help='Memory limit (use KMG suffixes)') 185 | sp_kernel.add_argument( 186 | '--gdb', action='store_true', help='Run the kernel in a debugger') 187 | sp_kernel.add_argument( 188 | '--kopts', dest='kopts', default=[], nargs='+', metavar='KOPT', 189 | help='Append to the kernel command line') 190 | sp_kernel.add_argument( 191 | '--disks', dest='disks', default=[], nargs='+', metavar='DISK', 192 | help='Expose block devices and disk images through' 193 | ' $VIDO_DISK0 onwards') 194 | sp_kernel.add_argument( 195 | '--run-as', dest='run_as', 196 | help='Drop user privileges before entering the kernel' 197 | ' (sudo vido --run-as $USER is useful to open block devices)') 198 | 199 | # need kvm 200 | sp_kvm = parser.add_argument_group('KVM-only options') 201 | # slirp is unmaintained outside of qemu 202 | sp_kvm.add_argument( 203 | '--net', action='store_true', 204 | help='Configure the network (unnecessary with userns)') 205 | # qemu needs this to catch crashes (unnecessary with uml) 206 | sp_kvm.add_argument( 207 | '--watchdog', action='store_true', 208 | help='Catch crashes with a watchdog timer') 209 | sp_kvm.add_argument( 210 | '--qemu-runner', help='Choose a custom qemu-system-* command') 211 | sp_kvm.add_argument( 212 | '--qemu-9p-workaround', action='store_true', 213 | help='Work around a bug with the 9p server in Qemu < 1.6') 214 | 215 | sp_cmd = parser.add_argument_group( 216 | 'Command', 'Use two dashes (--) to separate the command' 217 | ' from prior flags') 218 | sp_cmd.add_argument( 219 | 'cmd', nargs='*', metavar='CMD', 220 | help='Command and arguments to run; defaults to a shell') 221 | 222 | args = parser.parse_args() 223 | 224 | maybe_pass_env = args.pass_env + ['PWD'] 225 | pass_env = ALWAYS_PASS_ENV + [ 226 | var for var in maybe_pass_env if var in os.environ] 227 | 228 | cmd = args.cmd or [os.environ['SHELL']] 229 | cmd[0] = path_resolve(cmd[0]) 230 | 231 | kcmd = ['rw', 'quiet', 'init=' + runner] 232 | 233 | conf.env = {var: os.environ[var] for var in pass_env} 234 | conf.cmd = cmd 235 | conf.cwd = os.getcwd() 236 | conf.allow_sudo = args.allow_sudo 237 | 238 | for dn in args.rw_dirs: 239 | assert os.path.exists(dn), dn 240 | conf.rw_dirs = args.rw_dirs 241 | if '/' in conf.rw_dirs: 242 | conf.rw_dirs.remove('/') 243 | conf.pivot_root = True 244 | else: 245 | conf.pivot_root = False 246 | 247 | for dn in args.clear_files: 248 | assert os.path.exists(dn), dn 249 | conf.clear_files = args.clear_files 250 | 251 | for dn in args.clear_dirs: 252 | assert os.path.exists(dn), dn 253 | conf.clear_dirs = args.clear_dirs 254 | 255 | assert all(map(kopt_safe, args.kopts)) 256 | kcmd.extend(args.kopts) 257 | 258 | 259 | def pass_disks(conf, disk_prefix): 260 | assert len(args.disks) <= 26 261 | conf.disks = [ 262 | '/dev/' + disk_prefix + chr(ord('a') + i) 263 | for i in range(len(args.disks))] 264 | 265 | 266 | if args.qemu_runner and args.virt != 'kvm': 267 | fatal('--qemu-runner is not supported with {}'.format(args.virt)) 268 | if args.watchdog and args.virt != 'kvm': 269 | fatal('--watchdog is not supported with {}'.format(args.virt)) 270 | if args.qemu_9p_workaround and args.virt != 'kvm': 271 | fatal('--qemu-9p-workaround is not supported with {}'.format(args.virt)) 272 | if args.net and args.virt != 'kvm': 273 | fatal('--net is not supported with {}'.format(args.virt)) 274 | if args.run_as and args.virt not in 'kvm uml'.split(): 275 | fatal('--run-as is not supported with {}'.format(args.virt)) 276 | if args.gdb and args.virt not in 'kvm uml'.split(): 277 | fatal('--gdb is not supported with {}'.format(args.virt)) 278 | if args.kopts and args.virt not in 'kvm uml'.split(): 279 | fatal('--kopts is not supported with {}'.format(args.virt)) 280 | if args.disks and args.virt not in 'kvm uml'.split(): 281 | fatal('--disks is not supported with {}'.format(args.virt)) 282 | if args.mem is not None: 283 | if args.virt not in 'kvm uml'.split(): 284 | fatal('--mem is not supported with {}'.format(args.virt)) 285 | else: 286 | args.mem = '128M' 287 | if args.kernel and args.virt not in 'kvm uml'.split(): 288 | fatal('--kernel is not supported with {}'.format(args.virt)) 289 | 290 | for dname in args.disks: 291 | if not os.path.exists(dname): 292 | fatal('Disk {!r} doesn\'t exist'.format(dname)) 293 | 294 | conf.watchdog = args.watchdog 295 | 296 | # Kept for GC purposes 297 | ipc_dir = None 298 | def open_ipc(): 299 | global ipc_dir 300 | ipc_dir = tempfile.TemporaryDirectory(prefix='vido-', suffix='.ipc') 301 | conf.ipc = ipc_dir.name 302 | 303 | conf.virt = args.virt 304 | 305 | if args.virt == 'kvm': 306 | if args.qemu_runner is None: 307 | qemu = 'qemu-system-' + os.uname().machine 308 | else: 309 | qemu = args.qemu_runner 310 | 311 | qcmd = [ 312 | qemu, 313 | '-m', args.mem, '-enable-kvm', '-nodefaults', 314 | '-serial', 'stdio', '-nographic', 315 | '-fsdev', 'local,id=root,path=/,security_model=none', 316 | '-device', 'virtio-9p-pci,fsdev=root,mount_tag=/dev/root'] 317 | 318 | pass_fds = [] 319 | for disk in args.disks: 320 | # O_EXCL on a block device does mandatory locking 321 | # against kernel mounts (as well as advisory locking 322 | # against other vido instances) 323 | fd = os.open(disk, os.O_RDWR|os.O_EXCL) 324 | pass_fds.append(fd) 325 | qcmd += [ 326 | '-add-fd', 'fd={},set={}'.format(fd, fd), 327 | '-drive', 'file=/dev/fdset/{},if=virtio,media=disk'.format(fd)] 328 | pass_disks(conf, 'vd') 329 | 330 | if args.run_as: 331 | #qcmd += ['-runas', args.run_as] 332 | drop_privs(args.run_as) 333 | open_ipc() 334 | 335 | if args.kernel is None: 336 | if args.kernel_version: 337 | kernel_ver = args.kernel_version 338 | else: 339 | kernel_ver = os.uname().release 340 | 341 | # Those may not work out of the box, need 9p+virtio built-in 342 | # Building a custom initramfs would work around that 343 | qcmd += ['-kernel', '/boot/vmlinuz-' + kernel_ver] 344 | add_9p_mods = True 345 | modbase = '/lib/modules/' + kernel_ver + '/' 346 | else: 347 | qcmd += ['-kernel', args.kernel] 348 | add_9p_mods = False 349 | 350 | if args.net: 351 | # Can't get virtio to work (btw, I get a sit0 not eth0) 352 | #qcmd += ['-netdev', 'user', '-net', 'nic,model=virtio'] 353 | qcmd += ['-net', 'user,net=10.0.2.0/24,host=10.0.2.2,dns=10.0.2.3', 354 | '-net', 'nic,model=virtio'] 355 | # Not passing dns, sharing the host's resolv.conf 356 | # Suggest a port-forward for the local resolver case 357 | conf.net = dict(host='10.0.2.15/24', router='10.0.2.2') 358 | 359 | if args.watchdog: 360 | qcmd += ['-watchdog', 'ib700', '-watchdog-action', 'poweroff'] 361 | # Shutdown within a second 362 | kcmd += ['ib700wdt.timeout=1', 'ib700wdt.nowayout=1'] 363 | 364 | if args.gdb: 365 | qcmd += ['-s'] 366 | # We can't use the -S flag to wait for gdb, any breakpoints set 367 | # at early startup would be unusable: 368 | # http://thread.gmane.org/gmane.comp.emulators.qemu/80327 369 | print('Please run: gdb -ex "target remote :1234" ./vmlinux') 370 | 371 | rootflags = 'trans=virtio,cache=fscache' 372 | if args.qemu_9p_workaround: 373 | rootflags += ',version=9p2000.u' 374 | 375 | kcmd += ['rootfstype=9p', 'rootflags=' + rootflags, 'console=ttyS0'] 376 | 377 | kcmd.append('VIDO_CONFIG=' + quote_config(conf)) 378 | qcmd += ['-append', ' '.join(kcmd)] 379 | if add_9p_mods: 380 | with tempfile.NamedTemporaryFile(suffix='.initramfs') as irf: 381 | mkinitramfs(irf, rootflags=rootflags, runner=runner, modbase=modbase) 382 | qcmd += ['-initrd', irf.name] 383 | subprocess.check_call(qcmd, pass_fds=pass_fds) 384 | else: 385 | subprocess.check_call(qcmd, pass_fds=pass_fds) 386 | elif args.virt == 'uml': 387 | if args.kernel is None: 388 | args.kernel = '/usr/bin/linux.uml' 389 | ucmd = [args.kernel] 390 | 391 | pass_fds = [] 392 | for pos, disk in enumerate(args.disks): 393 | # O_EXCL: see above 394 | fd = os.open(disk, os.O_RDWR|os.O_EXCL) 395 | pass_fds.append(fd) 396 | kcmd.append('ubd{}=/dev/fd/{}'.format(pos, fd)) 397 | pass_disks(conf, 'ubd') 398 | if args.run_as: 399 | drop_privs(args.run_as) 400 | open_ipc() 401 | 402 | kcmd += ['rootfstype=hostfs', 'mem=' + args.mem] 403 | kcmd.append('VIDO_CONFIG=' + quote_config(conf)) 404 | if args.gdb: 405 | subprocess.check_call([ 406 | 'gdb', 407 | '-ex', 'handle SIGSEGV pass nostop noprint', 408 | '-ex', 'handle SIGUSR1 nopass stop print', 409 | '--args'] + ucmd + kcmd, pass_fds=pass_fds) 410 | else: 411 | out2 = os.dup(1) 412 | kcmd.append('con0=fd:0,fd:{}'.format(out2)) 413 | subprocess.check_call( 414 | ucmd + kcmd, pass_fds=[out2] + pass_fds, 415 | stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) 416 | elif args.virt == 'userns': 417 | open_ipc() 418 | conf.disks = [] 419 | def do_unshare(): 420 | libc = ctypes.CDLL('libc.so.6', use_errno=True) 421 | CLONE_NEWUSER = 0x10000000 422 | CLONE_NEWNS = 0x00020000 423 | orig_uid = os.getuid() 424 | orig_gid = os.getgid() 425 | if libc.unshare(CLONE_NEWUSER | CLONE_NEWNS) != 0: 426 | eno = ctypes.get_errno() 427 | if eno == errno.EINVAL: 428 | fatal('unshare: EINVAL. ' 429 | 'Make sure your kernel has CONFIG_USER_NS.') 430 | raise OSError(eno) 431 | try: 432 | with open('/proc/self/setgroups', 'w') as mf: 433 | mf.write('deny\n') 434 | except FileNotFoundError: 435 | can_setgroups = True 436 | else: 437 | can_setgroups = False 438 | with open('/proc/self/uid_map', 'w') as mf: 439 | mf.write('0 {} 1\n'.format(orig_uid)) 440 | with open('/proc/self/gid_map', 'w') as mf: 441 | mf.write('0 {} 1\n'.format(orig_gid)) 442 | os.setuid(0) 443 | os.setgid(0) 444 | if can_setgroups: 445 | os.setgroups([]) 446 | subprocess.check_call( 447 | [runner], env=dict(VIDO_CONFIG=quote_config(conf)), 448 | preexec_fn=do_unshare) 449 | else: 450 | assert False, args.virt 451 | 452 | try: 453 | exitf = open(conf.ipc + '/exit-status') 454 | except FileNotFoundError: 455 | fatal('VM crash') 456 | else: 457 | with exitf: 458 | exitstr = exitf.read() 459 | if not exitstr: 460 | fatal('virt-stub crashed') 461 | sys.exit(int(exitstr)) 462 | 463 | -------------------------------------------------------------------------------- /virt-stub: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 -S 2 | import fcntl 3 | import json 4 | import os 5 | import subprocess 6 | import urllib.parse 7 | import tempfile 8 | import termios 9 | import time 10 | import threading 11 | import types 12 | 13 | conf = json.loads( 14 | urllib.parse.unquote(os.environ['VIDO_CONFIG']), 15 | object_hook=lambda x: types.SimpleNamespace(**x)) 16 | 17 | # Open this early so that clear_dirs/rw_dirs won't interfere 18 | exit_status_file = open(conf.ipc + '/exit-status', 'w') 19 | 20 | if os.stat('/').st_dev == os.stat('/proc').st_dev: 21 | # Do this before scanning mountinfo 22 | # Skip in userns (already mounted, and already namespaced) 23 | subprocess.check_call('mount -nt proc proc /proc'.split()) 24 | assert os.stat('/').st_dev != os.stat('/proc').st_dev 25 | 26 | mounted = [] 27 | def read_mounts(): 28 | with open('/proc/self/mountinfo') as mounts: 29 | for line in mounts: 30 | items = line.split() 31 | idx = items.index('-') 32 | fstype = items[idx + 1] 33 | opts1 = items[5].split(',') 34 | opts2 = items[idx + 3].split(',') 35 | readonly = 'ro' in opts1 + opts2 36 | intpath = items[3] 37 | mpoint = items[4] 38 | dev = items[idx + 2] 39 | mounted.append((fstype, mpoint)) 40 | 41 | if conf.pivot_root and conf.virt == 'userns': 42 | # mount --move is disallowed (we may reveal a directory that was 43 | # meant to be hidden), use bind mounts instead 44 | slash0 = tempfile.mkdtemp(prefix='vido-', suffix='.new-slash') 45 | delta0 = tempfile.mkdtemp(prefix='vido-', suffix='.delta-slash') 46 | subprocess.check_call('mount -nt tmpfs tmpfs --'.split() + [delta0]) 47 | read_mounts() 48 | subprocess.check_call( 49 | 'mount -nt overlay overlay --make-unbindable'.split() 50 | + ['-olowerdir={},upperdir={}'.format('/', delta0), slash0]) 51 | bound = set() 52 | for (fstype, mpoint) in mounted: 53 | if mpoint == '/': 54 | continue 55 | if mpoint in bound: 56 | continue 57 | if any(mpoint.startswith(mp + '/') for mp in bound): 58 | continue 59 | bound.add(mpoint) 60 | #print('Binding', mpoint) 61 | subprocess.check_call( 62 | 'mount -n --rbind --'.split() + [mpoint, slash0 + mpoint]) 63 | slash1 = tempfile.mkdtemp( 64 | prefix='vido-', suffix='.old-slash', dir=slash0) 65 | os.chdir(slash0) 66 | subprocess.check_call(['/sbin/pivot_root', '.', slash1]) 67 | os.chroot('.') 68 | slash1 = '/' + os.path.relpath(slash1, start=slash0) 69 | elif conf.pivot_root: 70 | slash0 = tempfile.mkdtemp(prefix='vido-', suffix='.new-slash') 71 | delta0 = tempfile.mkdtemp(prefix='vido-', suffix='.delta-slash') 72 | subprocess.check_call('mount -nt tmpfs tmpfs --'.split() + [delta0]) 73 | read_mounts() 74 | subprocess.check_call( 75 | 'mount -nt overlay overlay'.split() 76 | + ['-olowerdir={},upperdir={}'.format('/', delta0), slash0]) 77 | 78 | slash1 = tempfile.mkdtemp( 79 | prefix='vido-', suffix='.old-slash', dir=slash0) 80 | os.chdir(slash0) 81 | subprocess.check_call(['/sbin/pivot_root', '.', slash1]) 82 | os.chroot('.') 83 | slash1 = '/' + os.path.relpath(slash1, start=slash0) 84 | moved = set() 85 | for (fstype, mpoint) in mounted: 86 | if mpoint == '/': 87 | continue 88 | if mpoint in moved: 89 | continue 90 | if any(mpoint.startswith(mp + '/') for mp in moved): 91 | continue 92 | moved.add(mpoint) 93 | #print('Moving', mpoint) 94 | subprocess.check_call( 95 | 'mount -n --move --'.split() + [slash1 + mpoint, mpoint]) 96 | else: 97 | read_mounts() 98 | 99 | 100 | mounted = set(mounted) 101 | def ensure_mount(fstype, mpoint): 102 | if (fstype, mpoint) in mounted: 103 | return 104 | subprocess.check_call('mount -nt'.split() + [fstype, '--', fstype, mpoint]) 105 | 106 | def ensure_dir(path): 107 | try: 108 | os.mkdir(path) 109 | except FileExistsError: 110 | pass 111 | 112 | # Or set CONFIG_DEVTMPFS_MOUNT 113 | try: 114 | ensure_mount('devtmpfs', '/dev') 115 | except subprocess.CalledProcessError: 116 | # XXX Debian/Ubuntu ship uml kernels without devtmpfs 117 | if conf.virt != 'uml': 118 | raise 119 | 120 | if conf.watchdog: 121 | if not os.path.exists('/dev/watchdog'): 122 | subprocess.check_call('/sbin/modprobe -v ib700wdt'.split(), stdout=subprocess.DEVNULL) 123 | assert os.path.exists('/dev/watchdog') 124 | wd = open('/dev/watchdog', 'wb', buffering=0) 125 | 126 | class Watchdog(threading.Thread): 127 | daemon = True 128 | 129 | def run(self): 130 | while True: 131 | wd.write(b'.') 132 | time.sleep(.5) 133 | 134 | Watchdog().start() 135 | 136 | if not os.path.lexists('/dev/fd'): 137 | subprocess.check_call('ln -sT /proc/self/fd /dev/fd'.split()) 138 | 139 | if conf.virt != 'userns': 140 | ensure_mount('sysfs', '/sys') 141 | ensure_mount('tmpfs', '/run') 142 | ensure_dir('/run/lock') 143 | ensure_dir('/run/shm') 144 | 145 | # Set controlling tty 146 | os.setsid() 147 | fcntl.ioctl(0, termios.TIOCSCTTY, 0) 148 | 149 | # Reset windows size 150 | # If this is resized after boot, call `resize` manually. 151 | # The guest won't get SIGWINCH. 152 | try: 153 | subprocess.call(['resize'], stdout=subprocess.DEVNULL) 154 | except FileNotFoundError: 155 | # Not installed 156 | pass 157 | 158 | # At least the Debian and Ubuntu UML package uses this 159 | if conf.virt == 'uml': 160 | subprocess.call( 161 | 'mount -nt hostfs -o /usr/lib/uml/modules hostfs /lib/modules'.split(), 162 | stderr=subprocess.DEVNULL) 163 | 164 | env = vars(conf.env) 165 | 166 | 167 | for dn in conf.rw_dirs: 168 | tdn = tempfile.mkdtemp(prefix='vido-', suffix='.delta') 169 | subprocess.check_call('mount -nt tmpfs tmpfs --'.split() + [tdn]) 170 | subprocess.check_call( 171 | 'mount -nt overlay overlay'.split() 172 | + ['-olowerdir={},upperdir={}'.format(dn, tdn), dn]) 173 | 174 | for dn in conf.clear_dirs: 175 | subprocess.check_call('mount -nt tmpfs tmpfs --'.split() + [dn]) 176 | 177 | for fn in conf.clear_files: 178 | tfd, tfn = tempfile.mkstemp(prefix='vido-', suffix='.clear-file') 179 | subprocess.check_call('mount -nB'.split() + [tfn, fn]) 180 | 181 | if conf.allow_sudo: 182 | tfd, tfn = tempfile.mkstemp(prefix='vido-', suffix='.sudoers') 183 | with os.fdopen(tfd, 'w') as tf: 184 | # user host=(newuser:newgrp) flags:cmd 185 | tf.write('ALL ALL=(ALL:ALL) NOPASSWD:ALL\n') 186 | subprocess.check_call('mount -nB'.split() + [tfn, '/etc/sudoers']) 187 | 188 | if hasattr(conf, 'net'): 189 | subprocess.check_call( 190 | 'ip addr add dev eth0'.split() + [conf.net.host]) 191 | subprocess.check_call('ip link set eth0 up'.split()) 192 | subprocess.check_call( 193 | 'ip route add default via'.split() + [conf.net.router] 194 | + 'dev eth0'.split()) 195 | 196 | for i, disk in enumerate(conf.disks): 197 | env['VIDO_DISK{}'.format(i)] = disk 198 | 199 | rcode = subprocess.call(conf.cmd, cwd=conf.cwd, env=env) 200 | exit_status_file.write('%d' % rcode) 201 | exit_status_file.close() 202 | if conf.virt != 'userns': 203 | subprocess.check_call('/sbin/poweroff -f'.split()) 204 | 205 | --------------------------------------------------------------------------------