diff --git a/DCO-1.1.txt b/DCO-1.1.txt new file mode 100644 index 0000000..2da8a59 --- /dev/null +++ b/DCO-1.1.txt @@ -0,0 +1,29 @@ +The text 'Signed-off-by:' in a commit message indicates that the signer +agrees to the Developer's Certificate of Origin 1.1, reproduced below. + +Developer's Certificate of Origin 1.1 + +By making a contribution to this project, I certify that: + +(a) The contribution was created in whole or in part by me and I + have the right to submit it under the open source license + indicated in the file; or + +(b) The contribution is based upon previous work that, to the best + of my knowledge, is covered under an appropriate open source + license and I have the right under that license to submit that + work with modifications, whether created in whole or in part + by me, under the same open source license (unless I am + permitted to submit under a different license), as indicated + in the file; or + +(c) The contribution was provided directly to me by some other + person who certified (a), (b) or (c) and I have not modified + it. + +(d) I understand and agree that this project and the contribution + are public and that a record of the contribution (including all + personal information I submit with it, including my sign-off) is + maintained indefinitely and may be redistributed consistent with + this project or the open source license(s) involved. + diff --git a/README.md b/README.md index 006b190..7efaaa9 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,9 @@ Kernels produced with KernelCraft are lacking lots of features, in order to reduce the build time to the minimum and still provide you a usable kernel capable of running your tests and experiments. +KernelCraft is based on virtme, written by Andy Lutomirski +([web][korg-web] | [git][korg-git]). + Quick start =========== @@ -60,18 +63,30 @@ repository): Requirements ============ -KernelCraft requires a customized version of virtme, available here -[web][arighi-virtme]. + * You need Python 3.8 or higher -You may also need to install `crash` to use the memory dump inspection feature -(see example below). + * QEMU 1.6 or higher is recommended (QEMU 1.4 and 1.5 are partially supported + using a rather ugly kludge) + * You will have a much better experience if KVM is enabled. That means that + you should be on bare metal with hardware virtualization (VT-x or SVM) + enabled or in a VM that supports nested virtualization. On some Linux + distributions, you may need to be a member of the "kvm" group. Using + VirtualBox or most VPS providers will fall back to emulation. -If you are using Ubuntu you can install all the required packages and dependencies from this ppa: -[web][kernelcraft-ppa]. + * Depending on the options you use, you may need a statically linked `busybox` + binary somewhere in your path. + + * You may need to install `crash` to use the memory dump inspection feature + (see example below). Examples ======== + - Build and run a kernel from a local git repository: +``` + $ kc +``` + - Build and run v6.1-rc3 from the public mainline git repository: ``` $ kc -r mainline -c v6.1-rc3 @@ -192,16 +207,16 @@ virtme-configkernel. It is possible to specify a set of custom configs (.config chunk) in ~/.kc.config, these user-specific settings will override the default settings -of virtme-configkernel (except for the mandatory configs that are required to -boot and test the kernel inside qemu, using virtme). +of virtme-configkernel (except for the mandatory configs that are required to boot +and test the kernel inside qemu, using virtme-run). Then the kernel is compiled either locally or on an external build host (if the `--build-host` option is used); once the build is done only the required files needed to test the kernel are copied from the remote host if an external build host is used. -Then the kernel is executed using virtme. This allows to test the kernel using -a safe copy-on-write snapshot of the entire host filesystem. +Then the kernel is executed using the virtme module. This allows to test the +kernel using a safe copy-on-write snapshot of the entire host filesystem. All the kernels compiled with KernelCraft have a `-rc` suffix to their kernel version, this allows to easily determine if you're inside a KernelCraft kernel @@ -281,6 +296,11 @@ Troubleshooting $ kc --clean --build-host HOSTNAME ``` +Contributing +============ + +Please see DCO-1.1.txt. + Credits ======= @@ -291,5 +311,5 @@ KernelCraft is based on virtme, written by Andy Lutomirski [korg-web]: https://git.kernel.org/cgit/utils/kernel/virtme/virtme.git "virtme on kernel.org" [korg-git]: git://git.kernel.org/pub/scm/utils/kernel/virtme/virtme.git "git address" -[arighi-virtme]: https://github.com/arighi/virtme "arighi virtme" +[virtme]: https://github.com/amluto/virtme "virtme" [kernelcraft-ppa]: https://launchpad.net/~arighi/+archive/ubuntu/kernelcraft "kernelcraft ppa" diff --git a/kc b/bin/kc similarity index 82% rename from kc rename to bin/kc index 3c2b3bf..69c857a 100755 --- a/kc +++ b/bin/kc @@ -10,8 +10,8 @@ # source distribution. import sys -if sys.version_info < (3, 3): - sys.stderr.write('ERROR: kernelcraft requires Python 3.3 or higher\n') +if sys.version_info < (3,8): + print('KernelCraft requires Python 3.8 or higher') sys.exit(1) from kernelcraft import run diff --git a/bin/virtme-configkernel b/bin/virtme-configkernel new file mode 100755 index 0000000..2eabad0 --- /dev/null +++ b/bin/virtme-configkernel @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# -*- mode: python -*- +# virtme-configkernel: Configure a kernel for virtme +# Copyright © 2014 Andy Lutomirski +# Licensed under the GPLv2, which is available in the virtme distribution +# as a file called LICENSE with SHA-256 hash: +# 8177f97513213526df2cf6184d8ff986c675afb514d4e68a404010521b880643 + +# This file is not installed; it's just used to run virtme from inside +# a source distribution. + +import sys +if sys.version_info < (3,8): + print('KernelCraft requires Python 3.8 or higher') + sys.exit(1) + +from virtme.commands import configkernel +exit(configkernel.main()) diff --git a/bin/virtme-mkinitramfs b/bin/virtme-mkinitramfs new file mode 100755 index 0000000..87323ae --- /dev/null +++ b/bin/virtme-mkinitramfs @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 +# -*- mode: python -*- +# virtme-mkinitramfs: Generate an initramfs image for virtme +# Copyright © 2019 Marcos Paulo de Souza +# Licensed under the GPLv2, which is available in the virtme distribution +# as a file called LICENSE with SHA-256 hash: +# 8177f97513213526df2cf6184d8ff986c675afb514d4e68a404010521b880643 + +import sys +if sys.version_info < (3,8): + print('KernelCraft requires Python 3.8 or higher') + sys.exit(1) + +from virtme.commands import mkinitramfs +exit(mkinitramfs.main()) diff --git a/bin/virtme-prep-kdir-mods b/bin/virtme-prep-kdir-mods new file mode 100755 index 0000000..9da7b7a --- /dev/null +++ b/bin/virtme-prep-kdir-mods @@ -0,0 +1,68 @@ +#!/bin/sh + +# This is still a bit of an experiment. +FAKEVER=0.0.0 +MODDIR=".virtme_mods/lib/modules/$FAKEVER" + +# Some distro don't have /sbin or /usr/sbin in user's default path. Make sure +# to setup the right path to find all the commands needed to setup the modules +# (depmod, etc.). +PATH=$PATH:/sbin:/usr/sbin + +COPY_MODULES=${COPY_MODULES:-"false"} + +print_help() { + script_name=$(basename "$0") + echo "usage: ${script_name} [-h | --help] [-c | --copy-modules]" + echo "" + echo "optional arguments:" + echo " -h, --help show this help message and exit" + echo " -c, --copy-modules copy kernel instead of linking" +} + +while ":"; do + case "$1" in + -h | --help) + print_help + exit 0 + ;; + -c | --copy-modules) + COPY_MODULES="true" + shift + ;; + *) + break + esac +done + +if ! [ -f "modules.order" ]; then + echo 'kc-prep-kdir-mods must be run from a kernel build directory' >&2 + echo "modules.order is missing. Your kernel may be too old or you didn't make modules." >&2 + exit 1 +fi + +# Set up .virtme_mods/lib/modules/0.0.0 as a module directory for this kernel, +# but fill it with symlinks instead of actual modules. + +mkdir -p "$MODDIR/kernel" +ln -srfT . "$MODDIR/build" + +# Remove all preexisting symlinks and add symlinks to all modules that belong +# to the build kenrnel. +find "$MODDIR/kernel" -type l -print0 |xargs -0 rm -f -- +# from v6.2, modules.order lists .o files, we need the .ko ones +sed 's:\.o$:.ko:' modules.order | while read -r i; do + [ ! -e "$i" ] && i=$(echo "$i" | sed s:^kernel/::) + mkdir -p "$MODDIR/kernel/$(dirname "$i")" + if [ "$COPY_MODULES" = "true" ]; then + cp "$i" "$MODDIR/kernel/$i" + else + ln -sr "$i" "$MODDIR/kernel/$i" + fi +done + +# Link in the files that make modules_install would copy +ln -srf modules.builtin modules.builtin.modinfo modules.order "$MODDIR/" + +# Now run depmod to collect dependencies +depmod -ae -F System.map -b .virtme_mods "$FAKEVER" diff --git a/bin/virtme-run b/bin/virtme-run new file mode 100755 index 0000000..3ef608b --- /dev/null +++ b/bin/virtme-run @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# -*- mode: python -*- +# virtme-run: The main command-line virtme frontend +# Copyright © 2014 Andy Lutomirski +# Licensed under the GPLv2, which is available in the virtme distribution +# as a file called LICENSE with SHA-256 hash: +# 8177f97513213526df2cf6184d8ff986c675afb514d4e68a404010521b880643 + +# This file is not installed; it's just use to run virtme from inside +# a source distribution. + +import sys +if sys.version_info < (3,8): + print('KernelCraft requires Python 3.8 or higher') + sys.exit(1) + +from virtme.commands import run +exit(run.main()) diff --git a/debian/control b/debian/control index 404b5bb..4bad7de 100644 --- a/debian/control +++ b/debian/control @@ -13,7 +13,6 @@ Depends: ${shlibs:Depends}, ${misc:Depends}, ${python3:Depends}, - virtme, crash, gcc-12, autoconf, diff --git a/kernelcraft/run.py b/kernelcraft/run.py index eee79b0..53bcc6b 100644 --- a/kernelcraft/run.py +++ b/kernelcraft/run.py @@ -150,6 +150,8 @@ def arg_fail(message, show_usage=True): def create_root(destdir, arch): if os.path.exists(destdir): return + # Use Ubuntu's cloud images to create a rootfs, these images are fairly + # small and they provide a nice environment to test kernels. release = check_output('lsb_release -s -c', shell=True).decode(sys.stdout.encoding).rstrip() url = f'https://cloud-images.ubuntu.com/{release}/current/{release}-server-cloudimg-{arch}-root.tar.xz' prevdir = os.getcwd() @@ -347,7 +349,7 @@ def run(self, arch=None, root=None, cpus=None, memory=None, network=None, disk=N opts = ' '.join(opts) else: opts = '' - # Start VM using virtme + # Start VM using virtme-run rw_dirs = ' '.join(f'--overlay-rwdir {d}' for d in ('/boot', '/etc', '/home', '/opt', '/srv', '/usr', '/var')) cmd = f'virtme-run {arch} --name {hostname} --kdir ./ {mods} {rw_dirs} {pwd} {username} {root} {execute} {network} {disk} {opts} --qemu-opts -m {memory} -smp {cpus} -s -qmp tcp:localhost:3636,server,nowait' check_call(cmd, shell=True) diff --git a/setup.py b/setup.py index c4c555d..3e64537 100755 --- a/setup.py +++ b/setup.py @@ -5,8 +5,8 @@ from setuptools import setup from kernelcraft.utils import VERSION, CONF_PATH -if sys.version_info < (3,3): - print('kernelcraft requires Python 3.3 or higher') +if sys.version_info < (3,8): + print('KernelCraft requires Python 3.8 or higher') sys.exit(1) setup( @@ -14,23 +14,35 @@ version=VERSION, author='Andrea Righi', author_email='andrea.righi@canonical.com', - description='', + description='Build and run a kernel inside a virtualized snapshot of your live system', url='https://git.launchpad.net/~arighi/+git/kernelcraft', license='GPLv2', long_description=open(os.path.join(os.path.dirname(__file__), 'README.md'), 'r').read(), long_description_content_type="text/markdown", - packages=['kernelcraft'], - install_requires=['argcomplete', 'virtme'], - include_package_data=True, + packages=['kernelcraft', 'virtme', 'virtme.commands', 'virtme.guest'], + install_requires=['argcomplete'], entry_points = { 'console_scripts': [ 'kc = kernelcraft.run:main', + 'virtme-run = virtme.commands.run:main', + 'virtme-configkernel = virtme.commands.configkernel:main', + 'virtme-mkinitramfs = virtme.commands.mkinitramfs:main', ] }, data_files = [ (str(CONF_PATH), ['cfg/kernelcraft.conf']), ], + scripts = [ + 'bin/virtme-prep-kdir-mods', + ], + package_data = { + 'virtme.guest': [ + 'virtme-init', + 'virtme-udhcpc-script', + ], + }, + include_package_data=True, classifiers=['Environment :: Console', 'Intended Audience :: Developers', 'Intended Audience :: System Administrators', diff --git a/virtme/__init__.py b/virtme/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/virtme/architectures.py b/virtme/architectures.py new file mode 100644 index 0000000..fa90cc0 --- /dev/null +++ b/virtme/architectures.py @@ -0,0 +1,364 @@ +# -*- mode: python -*- +# qemu_helpers: Helpers to find QEMU and handle its quirks +# Copyright © 2014 Andy Lutomirski +# Licensed under the GPLv2, which is available in the virtme distribution +# as a file called LICENSE with SHA-256 hash: +# 8177f97513213526df2cf6184d8ff986c675afb514d4e68a404010521b880643 + +import os +from typing import List, Optional + +class Arch(object): + def __init__(self, name) -> None: + self.virtmename = name + self.qemuname = name + self.linuxname = name + self.gccname = name + + defconfig_target = 'defconfig' + + @staticmethod + def serial_dev_name(index) -> str: + return 'ttyS%d' % index + + @staticmethod + def qemuargs(is_native) -> List[str]: + return [] + + @staticmethod + def virtio_dev_type(virtiotype) -> str: + # Return a full name for a virtio device. It would be + # nice if QEMU abstracted this away, but it doesn't. + return 'virtio-%s-pci' % virtiotype + + @staticmethod + def earlyconsole_args() -> List[str]: + return [] + + @staticmethod + def serial_console_args() -> List[str]: + return [] + + @staticmethod + def qemu_nodisplay_args() -> List[str]: + return ['-vga', 'none', '-display', 'none'] + + @staticmethod + def qemu_serial_console_args() -> List[str]: + # We should be using the new-style -device serialdev,chardev=xyz, + # but many architecture-specific serial devices don't support that. + return ['-serial', 'chardev:console'] + + @staticmethod + def config_base() -> List[str]: + return [] + + def kimg_path(self) -> str: + return 'arch/%s/boot/bzImage' % self.linuxname + + @staticmethod + def dtb_path() -> Optional[str]: + return None + +class Arch_unknown(Arch): + @staticmethod + def qemuargs(is_native): + return Arch.qemuargs(is_native) + +class Arch_x86(Arch): + def __init__(self, name): + Arch.__init__(self, name) + + self.linuxname = 'x86' + self.defconfig_target = '%s_defconfig' % name + + @staticmethod + def qemuargs(is_native): + ret = Arch.qemuargs(is_native) + + # Add a watchdog. This is useful for testing. + ret.extend(['-device', 'i6300esb,id=watchdog0']) + + if is_native and os.access('/dev/kvm', os.R_OK): + # If we're likely to use KVM, request a full-featured CPU. + # (NB: if KVM fails, this will cause problems. We should probe.) + ret.extend(['-cpu', 'host']) # We can't migrate regardless. + + return ret + + @staticmethod + def earlyconsole_args(): + return ['earlyprintk=serial,ttyS0,115200'] + + @staticmethod + def serial_console_args(): + return ['console=ttyS0'] + + @staticmethod + def config_base(): + return ['CONFIG_SERIO=y', + 'CONFIG_PCI=y', + 'CONFIG_INPUT=y', + 'CONFIG_INPUT_KEYBOARD=y', + 'CONFIG_KEYBOARD_ATKBD=y', + 'CONFIG_SERIAL_8250=y', + 'CONFIG_SERIAL_8250_CONSOLE=y', + 'CONFIG_X86_VERBOSE_BOOTUP=y', + 'CONFIG_VGA_CONSOLE=y', + 'CONFIG_FB=y', + 'CONFIG_FB_VESA=y', + 'CONFIG_FRAMEBUFFER_CONSOLE=y', + 'CONFIG_RTC_CLASS=y', + 'CONFIG_RTC_HCTOSYS=y', + 'CONFIG_RTC_DRV_CMOS=y', + 'CONFIG_HYPERVISOR_GUEST=y', + 'CONFIG_PARAVIRT=y', + 'CONFIG_KVM_GUEST=y', + + # Depending on the host kernel, virtme can nest! + 'CONFIG_KVM=y', + 'CONFIG_KVM_INTEL=y', + 'CONFIG_KVM_AMD=y', + ] + +class Arch_arm(Arch): + def __init__(self): + Arch.__init__(self, 'arm') + + self.defconfig_target = 'vexpress_defconfig' + + @staticmethod + def qemuargs(is_native): + ret = Arch.qemuargs(is_native) + + # Emulate a vexpress-a15. + ret.extend(['-M', 'vexpress-a15']) + + # TODO: consider adding a PCI bus (and figuring out how) + + # TODO: This won't boot unless -dtb is set, but we need to figure + # out how to find the dtb file. + + return ret + + @staticmethod + def virtio_dev_type(virtiotype): + return 'virtio-%s-device' % virtiotype + + @staticmethod + def earlyconsole_args(): + return ['earlyprintk=serial,ttyAMA0,115200'] + + @staticmethod + def serial_console_args(): + return ['console=ttyAMA0'] + + def kimg_path(self): + return 'arch/arm/boot/zImage' + + def dtb_path(self): + return 'arch/arm/boot/dts/vexpress-v2p-ca15-tc1.dtb' + +class Arch_aarch64(Arch): + def __init__(self): + Arch.__init__(self, 'aarch64') + + self.qemuname = 'aarch64' + self.linuxname = 'arm64' + self.gccname = 'aarch64' + + @staticmethod + def qemuargs(is_native): + ret = Arch.qemuargs(is_native) + + if is_native: + ret.extend(['-M', 'virt,gic-version=host']) + ret.extend(['-cpu', 'host']) + else: + # Emulate a fully virtual system. + ret.extend(['-M', 'virt']) + + # Despite being called qemu-system-aarch64, QEMU defaults to + # emulating a 32-bit CPU. Override it. + ret.extend(['-cpu', 'cortex-a57']) + + + return ret + + @staticmethod + def virtio_dev_type(virtiotype): + return 'virtio-%s-device' % virtiotype + + @staticmethod + def earlyconsole_args(): + return ['earlyprintk=serial,ttyAMA0,115200'] + + @staticmethod + def serial_console_args(): + return ['console=ttyAMA0'] + + def kimg_path(self): + return 'arch/arm64/boot/Image' + +class Arch_ppc(Arch): + def __init__(self, name): + Arch.__init__(self, name) + + self.defconfig_target = 'ppc64e_defconfig' + self.qemuname = 'ppc64' + self.linuxname = 'powerpc' + self.gccname = 'powerpc64le' + + def qemuargs(self, is_native): + ret = Arch.qemuargs(is_native) + + ret.extend(['-M', 'pseries']) + + return ret + + @staticmethod + def config_base(): + return ['CONFIG_PPC64=y', + 'CONFIG_PPC_BOOK3S_64=y', + 'CONFIG_GENERIC_CPU=y', + 'CONFIG_PPC_BOOK3S=y', + 'CONFIG_PPC_FPU_REGS=y', + 'CONFIG_PPC_FPU=y', + 'CONFIG_ALTIVEC=y', + 'CONFIG_VSX=y', + 'CONFIG_PPC_64S_HASH_MMU=y', + 'CONFIG_PPC_RADIX_MMU=y', + 'CONFIG_PPC_RADIX_MMU_DEFAULT=y', + 'CONFIG_PPC_KUEP=y', + 'CONFIG_PPC_KUAP=y', + 'CONFIG_PPC_HAVE_PMU_SUPPORT=y', + 'CONFIG_PPC_PERF_CTRS=y', + 'CONFIG_FORCE_SMP=y', + 'CONFIG_SMP=y', + 'CONFIG_PPC_DOORBELL=y', + 'CONFIG_VDSO32=y', + 'CONFIG_CPU_LITTLE_ENDIAN=y', + 'CONFIG_PPC64_ELF_ABI_V2=y', + 'CONFIG_PPC64_BOOT_WRAPPER=y', + 'CONFIG_64BIT=y', + 'CONFIG_PPC_64K_PAGES=y', + 'CONFIG_PPC_SMLPAR=y', + 'CONFIG_PPC_SUBPAGE_PROT=y', + 'CONFIG_PPC_SVM=y', + 'CONFIG_PPC_TRANSACTIONAL_MEM=y', + 'CONFIG_PPC_UV=y', + 'CONFIG_PPC_WATCHDOG=y', + 'CONFIG_PPC_MEMTRACE=y', + 'CONFIG_PPC_UV=y', + 'CONFIG_PPC_WATCHDOG=y', + 'CONFIG_MEMORY_HOTPLUG=y', + 'CONFIG_VIRTUALIZATION=y', + 'CONFIG_KVM_BOOK3S_64=y', + 'CONFIG_KVM_BOOK3S_64_HV=y', + 'CONFIG_MEMORY_HOTREMOVE=y', + 'CONFIG_ZONE_DEVICE=y', + 'CONFIG_DEVICE_PRIVATE=y', + 'CONFIG_HARDLOCKUP_DETECTOR=y', + 'CONFIG_CRYPTO_MD5_PPC=m', + 'CONFIG_CRYPTO_SHA1_PPC=m', + 'CONFIG_HVC_CONSOLE=y', + ] + + def kimg_path(self): + # Apparently SLOF (QEMU's bundled firmware?) can't boot a zImage. + return 'vmlinux' + +class Arch_riscv64(Arch): + def __init__(self): + Arch.__init__(self, 'riscv64') + + self.defconfig_target = 'defconfig' + self.qemuname = 'riscv64' + self.linuxname = 'riscv' + self.gccname = 'riscv64' + + def qemuargs(self, is_native): + ret = Arch.qemuargs(is_native) + + ret.extend(['-machine', 'virt']) + ret.extend(['-bios', 'default']) + + return ret + + @staticmethod + def serial_console_args(): + return ['console=ttyS0'] + + def kimg_path(self): + return 'arch/riscv/boot/Image' + +class Arch_sparc64(Arch): + def __init__(self): + Arch.__init__(self, 'sparc64') + + self.defconfig_target = 'sparc64_defconfig' + self.qemuname = 'sparc64' + self.linuxname = 'sparc' + self.gccname = 'sparc64' + + def qemuargs(self, is_native): + ret = Arch.qemuargs(is_native) + + return ret + + def kimg_path(self): + return 'arch/sparc/boot/image' + + def qemu_nodisplay_args(self): + # qemu-system-sparc fails to boot if -display none is set. + return ['-nographic', '-vga', 'none'] + +class Arch_s390x(Arch): + def __init__(self): + Arch.__init__(self, 's390x') + + self.qemuname = 's390x' + self.linuxname = 's390' + self.gccname = 's390x' + + @staticmethod + def virtio_dev_type(virtiotype): + return 'virtio-%s-ccw' % virtiotype + + def qemuargs(self, is_native): + ret = Arch.qemuargs(is_native) + + # Ask for the latest version of s390-ccw + ret.extend(['-M', 's390-ccw-virtio']) + + # To be able to configure a console, we need to get rid of the + # default console + ret.extend(['-nodefaults']) + + return ret + + @staticmethod + def config_base(): + return ['CONFIG_MARCH_Z900=y'] + + @staticmethod + def qemu_serial_console_args(): + return ['-device', 'sclpconsole,chardev=console'] + +ARCHES = {arch.virtmename: arch for arch in [ + Arch_x86('x86_64'), + Arch_x86('i386'), + Arch_arm(), + Arch_aarch64(), + Arch_ppc('ppc64'), + Arch_ppc('ppc64le'), + Arch_riscv64(), + Arch_sparc64(), + Arch_s390x(), +]} + +def get(arch: str) -> Arch: + if arch in ARCHES: + return ARCHES[arch] + else: + return Arch_unknown(arch) diff --git a/virtme/commands/__init__.py b/virtme/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/virtme/commands/configkernel.py b/virtme/commands/configkernel.py new file mode 100644 index 0000000..dff934f --- /dev/null +++ b/virtme/commands/configkernel.py @@ -0,0 +1,235 @@ +# -*- mode: python -*- +# virtme-configkernel: Configure a kernel for virtme +# Copyright © 2014 Andy Lutomirski +# Licensed under the GPLv2, which is available in the virtme distribution +# as a file called LICENSE with SHA-256 hash: +# 8177f97513213526df2cf6184d8ff986c675afb514d4e68a404010521b880643 + +from typing import Optional + +import argparse +import os +import shlex +import shutil +import subprocess +import multiprocessing +from .. import architectures + +uname = os.uname() + +def make_parser(): + parser = argparse.ArgumentParser( + description='Configure a kernel for virtme', + ) + + parser.add_argument('--arch', action='store', metavar='ARCHITECTURE', + default=uname.machine, + help='Target architecture') + + parser.add_argument('--custom', action='append', metavar='CUSTOM', + help='Use a custom config snippet file to override specific config options') + + g = parser.add_argument_group(title='Mode').add_mutually_exclusive_group() + + g.add_argument('--allnoconfig', action='store_true', + help='Overwrite configuration with a virtme-suitable allnoconfig (unlikely to work)') + + g.add_argument('--defconfig', action='store_true', + help='Overwrite configuration with a virtme-suitable defconfig') + + g.add_argument('--update', action='store_true', + help='Update existing config for virtme') + + parser.add_argument('envs', metavar='envs', type=str, nargs='*', + help='Additional Makefile variables') + + return parser + +_ARGPARSER = make_parser() + +def arg_fail(message): + print(message) + _ARGPARSER.print_usage() + exit(1) + +_GENERIC_CONFIG = [ + '# Generic', + 'CONFIG_UEVENT_HELPER=n', # Obsolete and slow + 'CONFIG_VIRTIO=y', + 'CONFIG_VIRTIO_PCI=y', + 'CONFIG_VIRTIO_MMIO=y', + 'CONFIG_NET=y', + 'CONFIG_NET_CORE=y', + 'CONFIG_NETDEVICES=y', + 'CONFIG_NETWORK_FILESYSTEMS=y', + 'CONFIG_INET=y', + 'CONFIG_NET_9P=y', + 'CONFIG_NET_9P_VIRTIO=y', + 'CONFIG_9P_FS=y', + 'CONFIG_VIRTIO_NET=y', + 'CONFIG_CMDLINE_OVERRIDE=n', + 'CONFIG_BINFMT_SCRIPT=y', + 'CONFIG_SHMEM=y', + 'CONFIG_TMPFS=y', + 'CONFIG_UNIX=y', + 'CONFIG_MODULE_SIG_FORCE=n', + 'CONFIG_DEVTMPFS=y', + 'CONFIG_TTY=y', + 'CONFIG_VT=y', + 'CONFIG_UNIX98_PTYS=y', + 'CONFIG_EARLY_PRINTK=y', + 'CONFIG_INOTIFY_USER=y', + '', + '# virtio-scsi support', + 'CONFIG_BLOCK=y', + 'CONFIG_SCSI_LOWLEVEL=y', + 'CONFIG_SCSI=y', + 'CONFIG_SCSI_VIRTIO=y', + 'CONFIG_BLK_DEV_SD=y', + '', + '# virt-serial support', + 'CONFIG_VIRTIO_CONSOLE=y', + '', + '# watchdog (useful for test scripts)', + 'CONFIG_WATCHDOG=y', + 'CONFIG_WATCHDOG_CORE=y', + 'CONFIG_I6300ESB_WDT=y', + + '# Make sure debuginfo are available', + 'CONFIG_DEBUG_INFO_DWARF_TOOLCHAIN_DEFAULT=y', + + '# Enable overlayfs', + 'CONFIG_OVERLAY_FS=y', +] + +_GENERIC_CONFIG_OPTIONAL = [ + '# initramfs support', + 'BLK_DEV_INITRD=y', + + '# Provide USB support as module', + 'CONFIG_USB=m' + + '# BPF stuff', + 'CONFIG_BPF=y', + 'CONFIG_BPF_SYSCALL=y', + 'CONFIG_BPF_JIT=y', + 'CONFIG_HAVE_EBPF_JIT=y', + 'CONFIG_BPF_EVENTS=y', + 'CONFIG_FTRACE_SYSCALLS=y', + 'CONFIG_FUNCTION_TRACER=y', + 'CONFIG_HAVE_DYNAMIC_FTRACE=y', + 'CONFIG_DYNAMIC_FTRACE=y', + 'CONFIG_HAVE_KPROBES=y', + 'CONFIG_KPROBES=y', + 'CONFIG_KPROBE_EVENTS=y', + 'CONFIG_ARCH_SUPPORTS_UPROBES=y', + 'CONFIG_UPROBES=y', + 'CONFIG_UPROBE_EVENTS=y', + 'CONFIG_DEBUG_FS=y', + 'CONFIG_DEBUG_INFO_BTF=y', + + '# Useful debugging features', + 'CONFIG_PROVE_LOCKING=y', + + '# Unnecessary configs', + '# CONFIG_LOCALVERSION_AUTO is not set', + '# CONFIG_DRM is not set', + '# CONFIG_SOUND is not set', + '# CONFIG_I2C is not set', + '# CONFIG_INPUT_MOUSE is not set', + '# CONFIG_MOUSE_PS2 is not set', + '# CONFIG_USB_HID is not set', + '# CONFIG_HID is not set', + '# CONFIG_MLX4_EN is not set', + '# CONFIG_MLX5_CORE is not set', + '# CONFIG_NFS_FS is not set', + '# CONFIG_IPV6 is not set', + '# CONFIG_AUDIT is not set', + '# CONFIG_SECURITY is not set', + '# CONFIG_WIRELESS is not set', + '# CONFIG_WLAN is not set', + '# CONFIG_SCHED_MC is not set', + '# CONFIG_CPU_FREQ is not set', + '# CONFIG_INFINIBAND is not set', + '# CONFIG_PPP is not set', + '# CONFIG_PPPOE is not set', + '# CONFIG_EXT2_FS is not set', + '# CONFIG_REISERFS_FS not set', + '# CONFIG_JFS_FS is not set', + '# CONFIG_XFS_FS is not set', + '# CONFIG_BTRFS_FS is not set', + '# CONFIG_HFS_FS is not set', + '# CONFIG_HFSPLUS_FS is not set', + +] + +def main(): + args = _ARGPARSER.parse_args() + + if not os.path.isfile('scripts/kconfig/merge_config.sh') and \ + not os.path.isfile('source/scripts/kconfig/merge_config.sh'): + print('virtme-configkernel must be run in a kernel source/build directory') + return 1 + + arch = architectures.get(args.arch) + + custom_conf = [] + if args.custom: + for conf_chunk in args.custom: + with open(conf_chunk) as fd: + custom_conf += fd.readlines() + + conf = (_GENERIC_CONFIG_OPTIONAL + + ['# Arch-specific options'] + + arch.config_base() + + custom_conf + + _GENERIC_CONFIG) + + archargs = ['ARCH=%s' % shlex.quote(arch.linuxname)] + + if shutil.which('%s-linux-gnu-gcc' % arch.gccname): + archargs.append('CROSS_COMPILE=%s' % shlex.quote("%s-linux-gnu-" % arch.gccname)) + + maketarget: Optional[str] + + if args.allnoconfig: + maketarget = 'allnoconfig' + updatetarget = 'syncconfig' + elif args.defconfig: + maketarget = arch.defconfig_target + updatetarget = 'olddefconfig' + elif args.update: + maketarget = None + updatetarget = 'olddefconfig' + else: + arg_fail('No mode selected') + + # TODO: Get rid of most of the noise and check the result. + + # Set up an initial config + if maketarget: + subprocess.check_call(['make'] + archargs + [maketarget]) + + config = '.config' + + # Check if KBUILD_OUTPUT is defined and if it's a directory + config_dir = os.environ.get('KBUILD_OUTPUT', '') + if config_dir and os.path.isdir(config_dir): + config = os.path.join(config_dir, config) + + with open(config, 'ab') as conffile: + conffile.write('\n'.join(conf).encode('utf-8')) + + # Propagate additional Makefile variables + for var in args.envs: + archargs.append(shlex.quote(var)) + + subprocess.check_call(['make'] + archargs + [updatetarget]) + + print("Configured. Build with 'make %s -j%d'" % + (' '.join(archargs), multiprocessing.cpu_count())) + + return 0 + +if __name__ == '__main__': + exit(main()) diff --git a/virtme/commands/mkinitramfs.py b/virtme/commands/mkinitramfs.py new file mode 100755 index 0000000..563c98b --- /dev/null +++ b/virtme/commands/mkinitramfs.py @@ -0,0 +1,56 @@ +# -*- mode: python -*- +# virtme-mkinitramfs: Generate an initramfs image for virtme +# Copyright © 2014 Andy Lutomirski +# Licensed under the GPLv2, which is available in the virtme distribution +# as a file called LICENSE with SHA-256 hash: +# 8177f97513213526df2cf6184d8ff986c675afb514d4e68a404010521b880643 + +import argparse +from .. import modfinder +from .. import virtmods +from .. import mkinitramfs + +def make_parser(): + parser = argparse.ArgumentParser( + description='Generate an initramfs image for virtme', + ) + + parser.add_argument('--mod-kversion', action='store', default=None, + help='Find kernel modules related to kernel version set') + + parser.add_argument('--rw', action='store_true', default=False, + help='Mount initramfs as rw. Default is ro') + + parser.add_argument('--outfile', action='store', default=None, + help='Filename of the resulting initramfs file. Default: send initramfs to stdout') + + return parser + +def main(): + import sys + + args = make_parser().parse_args() + + config = mkinitramfs.Config() + + if args.mod_kversion is not None: + config.modfiles = modfinder.find_modules_from_install( + virtmods.MODALIASES, kver=args.mod_kversion) + + # search for busybox in the root filesystem + config.busybox = mkinitramfs.find_busybox(root = '/', is_native = True) + + if args.rw: + config.access = 'rw' + + if args.outfile is None: + buf = sys.stdout.buffer + else: + buf = open(args.outfile, 'w+b') + + mkinitramfs.mkinitramfs(buf, config) + + return 0 + +if __name__ == '__main__': + exit(main()) diff --git a/virtme/commands/run.py b/virtme/commands/run.py new file mode 100644 index 0000000..b05c4ef --- /dev/null +++ b/virtme/commands/run.py @@ -0,0 +1,662 @@ +# -*- mode: python -*- +# virtme-run: The main command-line virtme frontend +# Copyright © 2014 Andy Lutomirski +# Licensed under the GPLv2, which is available in the virtme distribution +# as a file called LICENSE with SHA-256 hash: +# 8177f97513213526df2cf6184d8ff986c675afb514d4e68a404010521b880643 + +from typing import Any, Optional, List, NoReturn, Dict, Tuple + +import argparse +import tempfile +import os +import errno +import fcntl +import sys +import shlex +import re +import itertools +import subprocess +from .. import virtmods +from .. import modfinder +from .. import mkinitramfs +from .. import qemu_helpers +from .. import architectures +from .. import resources + +uname = os.uname() + +class SilentError(Exception): + pass + +def make_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + description='Virtualize your system (or another) under a kernel image', + ) + + g: Any + + g = parser.add_argument_group(title='Selection of kernel and modules').add_mutually_exclusive_group() + g.add_argument('--installed-kernel', action='store', nargs='?', + const=uname.release, default=None, metavar='VERSION', + help='Use an installed kernel and its associated modules. If no version is specified, the running kernel will be used.') + + g.add_argument('--kimg', action='store', + help='Use specified kernel image with no modules.') + + g.add_argument('--kdir', action='store', metavar='KDIR', + help='Use a compiled kernel source directory') + + g = parser.add_argument_group(title='Kernel options') + g.add_argument('--mods', action='store', choices=['none', 'use', 'auto'], default='use', + help='Setup loadable kernel modules inside a compiled kernel source directory (used in conjunction with --kdir); none: ignore kernel modules, use: asks user to refresh virtme\'s kernel modules directory, auto: automatically refreshes virtme\'s kernel modules directory') + + g.add_argument('-a', '--kopt', action='append', default=[], + help='Add a kernel option. You can specify this more than once.') + + g.add_argument('--xen', action='store', + help='Boot Xen using the specified uncompressed hypervisor.') + + g = parser.add_argument_group(title='Common guest options') + g.add_argument('--root', action='store', default='/', + help='Local path to use as guest root') + g.add_argument('--rw', action='store_true', + help='Give the guest read-write access to its root filesystem') + g.add_argument('--graphics', action='store_true', + help='Show graphical output instead of using a console.') + g.add_argument('--net', action='store', const='user', nargs='?', + choices=['user', 'bridge'], + help='Enable basic network access.') + g.add_argument('--balloon', action='store_true', + help='Allow the host to ask the guest to release memory.') + g.add_argument('--disk', action='append', default=[], metavar='NAME=PATH', + help='Add a read/write virtio-scsi disk. The device node will be /dev/disk/by-id/scsi-0virtme_disk_NAME.') + g.add_argument('--blk-disk', action='append', default=[], metavar='NAME=PATH', + help='Add a read/write virtio-blk disk. The device nodes will be /dev/disk/by-id/virtio-virtme_disk_blk_NAME.') + g.add_argument('--memory', action='store', default=None, + help='Set guest memory and qemu -m flag.') + g.add_argument('--cpus', action='store', default=None, + help='Set guest cpu and qemu -smp flag.') + g.add_argument('--name', action='store', default=None, + help='Set guest hostname and qemu -name flag.') + g.add_argument('--user', action='store', + help='Change guest user') + + g = parser.add_argument_group( + title='Scripting', + description="Using any of the scripting options will run a script in the guest. The script's stdin will be attached to virtme-run's stdin and the script's stdout and stderr will both be attached to virtme-run's stdout. Kernel logs will go to stderr. This behaves oddly if stdin is a terminal; try using 'cat |virtme-run' if you have trouble with script mode.") + g.add_argument('--script-sh', action='store', metavar='SHELL_COMMAND', + help='Run a one-line shell script in the guest.') + g.add_argument('--script-exec', action='store', metavar='BINARY', + help='Run the specified binary in the guest.') + + g = parser.add_argument_group( + title='Architecture', + description="Options related to architecture selection") + g.add_argument('--arch', action='store', metavar='ARCHITECTURE', + default=uname.machine, + help='Guest architecture') + g.add_argument('--busybox', action='store', metavar='PATH_TO_BUSYBOX', + help='Use the specified busybox binary.') + + g = parser.add_argument_group(title='Virtualizer settings') + g.add_argument('--qemu-bin', action='store', default=None, + help="Use specified QEMU binary.") + g.add_argument('-q', '--qemu-opt', action='append', default=[], + help="Add a single QEMU argument. Use this when --qemu-opts's greedy behavior is problematic.'") + g.add_argument('--qemu-opts', action='store', nargs=argparse.REMAINDER, + metavar='OPTS...', help='Additional arguments for QEMU. This will consume all remaining arguments, so it must be specified last. Avoid using -append; use --kopt instead.') + + g = parser.add_argument_group(title='Debugging/testing') + g.add_argument('--force-initramfs', action='store_true', + help='Use an initramfs even if unnecessary') + g.add_argument('--dry-run', action='store_true', + help="Initialize everything but don't run the guest") + g.add_argument('--show-command', action='store_true', + help='Show the VM command line') + g.add_argument('--save-initramfs', action='store', + help='Save the generated initramfs to the specified path') + g.add_argument('--show-boot-console', action='store_true', + help='Show the boot console when running scripts') + + g = parser.add_argument_group(title='Guest userspace configuration').add_mutually_exclusive_group() + g.add_argument('--pwd', action='store_true', + help='Propagate current working directory to the guest') + g.add_argument('--cwd', action='store', + help='Change guest working directory') + + g = parser.add_argument_group(title='Sharing resources with guest') + g.add_argument('--rwdir', action='append', default=[], + help="Supply a read/write directory to the guest. Use --rwdir=path or --rwdir=guestpath=hostpath.") + g.add_argument('--rodir', action='append', default=[], + help="Supply a read-only directory to the guest. Use --rodir=path or --rodir=guestpath=hostpath.") + + g.add_argument('--overlay-rwdir', action='append', default=[], + help="Supply a directory that is r/w to the guest but read-only in the host. Use --overlay-rwdir=path.") + + return parser + +_ARGPARSER = make_parser() + +def arg_fail(message, show_usage=True) -> NoReturn: + print(message) + if show_usage: + _ARGPARSER.print_usage() + sys.exit(1) + +def is_file_more_recent(a, b) -> bool: + return os.stat(a).st_mtime > os.stat(b).st_mtime + +class Kernel: + __slots__ = ['kimg', 'dtb', 'modfiles', 'moddir', 'use_root_mods', 'config'] + + kimg: str + dtb: Optional[str] + modfiles: List[str] + moddir: Optional[str] + use_root_mods: bool + config: Optional[Dict[str, str]] + + def load_config(self, kdir: str) -> None: + cfgfile = os.path.join(kdir, '.config') + if os.path.isfile(cfgfile): + self.config = {} + regex = re.compile('^(CONFIG_[A-Z0-9_]+)=([ymn])$') + for line in open(cfgfile, 'r'): + m = regex.match(line.strip()) + if m: + self.config[m.group(1)] = m.group(2) + +def find_kernel_and_mods(arch, args) -> Kernel: + kernel = Kernel() + + kernel.use_root_mods = False + + if args.installed_kernel is not None: + kver = args.installed_kernel + if args.mods != 'none': + kernel.modfiles = modfinder.find_modules_from_install( + virtmods.MODALIASES, kver=kver) + kernel.moddir = os.path.join('/lib/modules', kver) + else: + kernel.modfiles = [] + kernel.moddir = None + kernel.kimg = '/usr/lib/modules/%s/vmlinuz' % kver + if not os.path.exists(kernel.kimg): + kernel.kimg = '/boot/vmlinuz-%s' % kver + kernel.dtb = None # For now + kernel.use_root_mods = True + elif args.kdir is not None: + kernel.kimg = os.path.join(args.kdir, arch.kimg_path()) + virtme_mods = os.path.join(args.kdir, '.virtme_mods') + mod_file = os.path.join(args.kdir, 'modules.order') + virtme_mod_file = os.path.join(virtme_mods, 'lib/modules/0.0.0/modules.dep') + kernel.load_config(args.kdir) + + # Kernel modules support + kver = None + kernel.moddir = None + kernel.modfiles = [] + + modmode = args.mods + if kernel.config is not None and kernel.config.get('CONFIG_MODULES', 'n') != 'y': + modmode = 'none' + + if modmode == 'none': + pass + elif modmode == 'use' or modmode == 'auto': + # Check if modules.order exists, otherwise it's not possible to use + # this option + if not os.path.exists(mod_file): + arg_fail('%s not found: kernel modules not enabled or kernel not compiled properly' % mod_file, show_usage=False) + # Check if virtme's kernel modules directory needs to be updated + if not os.path.exists(virtme_mods) or \ + is_file_more_recent(mod_file, virtme_mod_file): + if modmode == 'use': + # Inform user to manually refresh virtme's kernel modules + # directory + arg_fail("please run virtme-prep-kdir-mods to update virtme's kernel modules directory or use --mods=auto", show_usage=False) + else: + # Auto-refresh virtme's kernel modules directory + try: + resources.run_script('virtme-prep-kdir-mods', + cwd=args.kdir) + except subprocess.CalledProcessError: + raise SilentError() + kernel.moddir = os.path.join(virtme_mods, 'lib/modules', '0.0.0') + kernel.modfiles = modfinder.find_modules_from_install( + virtmods.MODALIASES, root=virtme_mods, kver='0.0.0') + else: + arg_fail("invalid argument '%s', please use --mods=none|use|auto" % args.mods) + + dtb_path = arch.dtb_path() + if dtb_path is None: + kernel.dtb = None + else: + kernel.dtb = os.path.join(args.kdir, dtb_path) + elif args.kimg is not None: + kernel.kimg = args.kimg + kernel.modfiles = [] + kernel.moddir = None + kernel.dtb = None # TODO: fix this + if args.mods != 'use': + arg_fail("--mods is not currently supported properly with --kimg") + else: + arg_fail('You must specify a kernel to use.') + + return kernel + +def export_virtfs(qemu: qemu_helpers.Qemu, arch: architectures.Arch, + qemuargs: List[str], path: str, + mount_tag: str, security_model='none', readonly=True) -> None: + # NB: We can't use -virtfs for this, because it can't handle a mount_tag + # that isn't a valid QEMU identifier. + fsid = 'virtfs%d' % len(qemuargs) + qemuargs.extend(['-fsdev', 'local,id=%s,path=%s,security_model=%s%s%s' % + (fsid, qemu.quote_optarg(path), + security_model, ',readonly=on' if readonly else '', + ',multidevs=remap' if qemu.has_multidevs else '')]) + qemuargs.extend(['-device', '%s,fsdev=%s,mount_tag=%s' % (arch.virtio_dev_type('9p'), fsid, qemu.quote_optarg(mount_tag))]) + +def quote_karg(arg: str) -> str: + if '"' in arg: + raise ValueError("cannot quote '\"' in kernel args") + + if ' ' in arg: + return '"%s"' % arg + else: + return arg + +# Validate name=path arguments from --disk and --blk-disk +def sanitize_disk_args(func: str, arg: str) -> Tuple[str, str]: + namefile = arg.split('=', 1) + if len(namefile) != 2: + arg_fail('invalid argument to %s' % func) + name, fn = namefile + if '=' in fn or ',' in fn: + arg_fail("%s filenames cannot contain '=' or ','" % (func)) + if '=' in name or ',' in name: + arg_fail("%s device names cannot contain '=' or ','" % (func)) + + return name, fn + +# Allowed characters in mount paths. We can extend this over time if needed. +_SAFE_PATH_PATTERN = '[a-zA-Z0-9_+ /.-]+' +_RWDIR_RE = re.compile('^(%s)(?:=(%s))?$' % + (_SAFE_PATH_PATTERN, _SAFE_PATH_PATTERN)) + +def do_it() -> int: + args = _ARGPARSER.parse_args() + + arch = architectures.get(args.arch) + is_native = (args.arch == uname.machine) + + qemu = qemu_helpers.Qemu(args.qemu_bin, arch.qemuname) + qemu.probe() + + need_initramfs = args.force_initramfs or qemu.cannot_overmount_virtfs + + config = mkinitramfs.Config() + + if len(args.overlay_rwdir) > 0: + virtmods.MODALIASES.append('overlay') + + kernel = find_kernel_and_mods(arch, args) + config.modfiles = kernel.modfiles + if config.modfiles: + need_initramfs = True + + qemuargs: List[str] = [qemu.qemubin] + kernelargs = [] + xenargs = [] + + # Put the '-name' flag first so it's easily visible in ps, top, etc. + if args.name: + qemuargs.extend(['-name', args.name]) + kernelargs.append('virtme_hostname=%s' % args.name) + + # Set up virtfs + export_virtfs(qemu, arch, qemuargs, args.root, '/dev/root', readonly=(not args.rw)) + + guest_tools_path = resources.find_guest_tools() + if guest_tools_path is None: + raise ValueError("couldn't find guest tools -- virtme is installed incorrectly") + + export_virtfs(qemu, arch, qemuargs, guest_tools_path, + 'virtme.guesttools') + + initcmds = ['mkdir -p /run/virtme/guesttools', + '/bin/mount -n -t 9p -o ro,version=9p2000.L,trans=virtio,access=any virtme.guesttools /run/virtme/guesttools', + 'exec /run/virtme/guesttools/virtme-init'] + + # Arrange for modules to end up in the right place + if kernel.moddir is not None: + if kernel.use_root_mods: + # Tell virtme-init to use the root /lib/modules + kernelargs.append("virtme_root_mods=1") + else: + # We're grabbing modules from somewhere other than /lib/modules. + # Rather than mounting it separately, symlink it in the guest. + # This allows symlinks within the module directory to resolve + # correctly in the guest. + kernelargs.append("virtme_link_mods=/%s" % qemu.quote_optarg(os.path.relpath(kernel.moddir, args.root))) + else: + # No modules are available. virtme-init will hide /lib/modules/KVER + pass + + # Set up mounts + mount_index = 0 + for dirtype, dirarg in itertools.chain((('rwdir', i) for i in args.rwdir), + (('rodir', i) for i in args.rodir)): + m = _RWDIR_RE.match(dirarg) + if not m: + arg_fail('invalid --%s parameter %r' % (dirtype, dirarg)) + if m.group(2) is not None: + guestpath = m.group(1) + hostpath = m.group(2) + else: + hostpath = m.group(1) + guestpath = os.path.relpath(hostpath, args.root) + if guestpath.startswith('..'): + arg_fail('%r is not inside the root' % hostpath) + + idx = mount_index + mount_index += 1 + tag = 'virtme.initmount%d' % idx + export_virtfs(qemu, arch, qemuargs, hostpath, tag, readonly=(dirtype != 'rwdir')) + kernelargs.append('virtme_initmount%d=%s' % (idx, guestpath)) + + for i, d in enumerate(args.overlay_rwdir): + kernelargs.append('virtme_rw_overlay%d=%s' % (i, d)) + + # Turn on KVM if available + if is_native: + qemuargs.extend(['-machine', 'accel=kvm:tcg']) + + # Add architecture-specific options + qemuargs.extend(arch.qemuargs(is_native)) + + # Set up / override baseline devices + qemuargs.extend(['-parallel', 'none']) + qemuargs.extend(['-net', 'none']) + + if not args.graphics and not args.script_sh and not args.script_exec: + # It would be nice to use virtconsole, but it's terminally broken + # in current kernels. Nonetheless, I'm configuring the console + # manually to make it easier to tweak in the future. + qemuargs.extend(['-echr', '1']) + qemuargs.extend(['-serial', 'none']) + qemuargs.extend(['-chardev', 'stdio,id=console,signal=off,mux=on']) + + qemuargs.extend(arch.qemu_serial_console_args()) + + qemuargs.extend(['-mon', 'chardev=console']) + + kernelargs.extend(arch.earlyconsole_args()) + qemuargs.extend(arch.qemu_nodisplay_args()) + + if not args.xen: + kernelargs.extend(arch.serial_console_args()) + else: + # Horrible special case + xenargs.extend(['console=com1']) + kernelargs.extend(['xencons=hvc', 'console=hvc0']) + + # PS/2 probing is slow; give the kernel a hint to speed it up. + kernelargs.extend(['psmouse.proto=exps']) + + # Fix the terminal defaults (and set iutf8 because that's a better + # default nowadays). I don't know of any way to keep this up to date + # after startup, though. + try: + terminal_size = os.get_terminal_size() + kernelargs.extend(['virtme_stty_con=rows %d cols %d iutf8' % + (terminal_size.lines, terminal_size.columns)]) + except OSError as e: + # don't die if running with a non-TTY stdout + if e.errno != errno.ENOTTY: + raise + + # Propagate the terminal type + if 'TERM' in os.environ: + kernelargs.extend(['TERM=%s' % os.environ['TERM']]) + + if args.balloon: + qemuargs.extend(['-balloon', 'virtio']) + + if args.memory: + qemuargs.extend(['-m', args.memory]) + + if args.cpus: + qemuargs.extend(['-smp', args.cpus]) + + if args.blk_disk: + for i,d in enumerate(args.blk_disk): + driveid = 'blk-disk%d' % i + name, fn = sanitize_disk_args('--blk-disk', d) + qemuargs.extend(['-drive', 'if=none,id=%s,file=%s' % (driveid, fn), + '-device', 'virtio-blk-pci,drive=%s,serial=%s' % (driveid, name)]) + + if args.disk: + qemuargs.extend(['-device', '%s,id=scsi' % arch.virtio_dev_type('scsi')]) + + for i,d in enumerate(args.disk): + driveid = 'disk%d' % i + name, fn = sanitize_disk_args('--disk', d) + qemuargs.extend(['-drive', 'if=none,id=%s,file=%s' % (driveid, fn), + '-device', 'scsi-hd,drive=%s,vendor=virtme,product=disk,serial=%s' % (driveid, name)]) + + has_script = False + + def do_script(shellcmd: str, use_exec=False, show_boot_console=False) -> None: + if args.graphics: + arg_fail('scripts and --graphics are mutually exclusive') + + nonlocal has_script + nonlocal need_initramfs + if has_script: + arg_fail('conflicting script options') + has_script = True + need_initramfs = True # TODO: Fix this + + # Turn off default I/O + qemuargs.extend(arch.qemu_nodisplay_args()) + + # Send kernel logs to stderr + qemuargs.extend(['-serial', 'none']) + qemuargs.extend(['-chardev', 'file,id=console,path=/proc/self/fd/2']) + + # We should be using the new-style -device serialdev,chardev=xyz, + # but many architecture-specific serial devices don't support that. + qemuargs.extend(['-serial', 'chardev:console']) + + if show_boot_console: + serdev = qemu.quote_optarg(arch.serial_dev_name(0)) + kernelargs.extend(['console=%s' % serdev, + 'earlyprintk=serial,%s,115200' % serdev]) + + # Set up a virtserialport for script I/O + qemuargs.extend(['-chardev', 'stdio,id=stdin,signal=on,mux=off']) + qemuargs.extend(['-device', arch.virtio_dev_type('serial')]) + qemuargs.extend(['-device', 'virtserialport,name=virtme.stdin,chardev=stdin']) + + qemuargs.extend(['-chardev', 'file,id=stdout,path=/proc/self/fd/1']) + qemuargs.extend(['-device', arch.virtio_dev_type('serial')]) + qemuargs.extend(['-device', 'virtserialport,name=virtme.stdout,chardev=stdout']) + + # Scripts shouldn't reboot + qemuargs.extend(['-no-reboot']) + + # Ask virtme-init to run the script + config.virtme_data[b'script'] = """#!/bin/sh + + {prefix}{shellcmd} + """.format(shellcmd=shellcmd, prefix="exec " if use_exec else "").encode('ascii') + + # Nasty issue: QEMU will set O_NONBLOCK on fds 0, 1, and 2. + # This isn't inherently bad, but it can cause a problem if + # another process is reading from 1 or writing to 0, which is + # exactly what happens if you're using a terminal and you + # redirect some, but not all, of the tty fds. Work around it + # by giving QEMU private copies of the open object if either + # of them is a terminal. + for oldfd,mode in ((0,os.O_RDONLY), (1,os.O_WRONLY), (2,os.O_WRONLY)): + if os.isatty(oldfd): + try: + newfd = os.open('/proc/self/fd/%d' % oldfd, mode) + except OSError: + pass + else: + os.dup2(newfd, oldfd) + os.close(newfd) + + if args.script_sh is not None: + do_script(args.script_sh, show_boot_console=args.show_boot_console) + + if args.script_exec is not None: + do_script(shlex.quote(args.script_exec), use_exec=True, show_boot_console=args.show_boot_console) + + if args.net: + qemuargs.extend(['-device', 'virtio-net-pci,netdev=n0']) + if args.net == 'user': + qemuargs.extend(['-netdev', 'user,id=n0']) + elif args.net == 'bridge': + # This is highly experimental. At least on Fedora 30 on + # a wireless network, it appears to successfully start but + # not have any network access. Patches or guidance welcome. + # (I assume it's mostly a lost cause on a wireless network + # due to a lack of widespread or automatic WDS support.) + qemuargs.extend(['-netdev', 'bridge,id=n0,br=virbr0']) + else: + assert False + kernelargs.extend(['virtme.dhcp']) + + if args.pwd: + rel_pwd = os.path.relpath(os.getcwd(), args.root) + if rel_pwd.startswith('..'): + print('current working directory is not contained in the root') + return 1 + kernelargs.append('virtme_chdir=%s' % rel_pwd) + + if args.cwd is not None: + if args.pwd: + arg_fail('--pwd and --cwd are mutually exclusive') + rel_cwd = os.path.relpath(args.cwd, args.root) + if rel_cwd.startswith('..'): + print('specified working directory is not contained in the root') + return 1 + kernelargs.append('virtme_chdir=%s' % rel_cwd) + + if args.user: + kernelargs.append('virtme_user=%s' % args.user) + + initrdpath: Optional[str] + + if need_initramfs: + if args.busybox is not None: + config.busybox = args.busybox + else: + busybox = mkinitramfs.find_busybox(args.root, is_native) + if busybox is None: + print('virtme-run: initramfs is needed, and no busybox was found', + file=sys.stderr) + return 1 + config.busybox = busybox + + if args.rw: + config.access = 'rw' + + # Set up the initramfs (warning: hack ahead) + if args.save_initramfs is not None: + initramfsfile = open(args.save_initramfs, 'xb') + initramfsfd = initramfsfile.fileno() + else: + initramfsfd,tmpname = tempfile.mkstemp('irfs') + os.unlink(tmpname) + initramfsfile = os.fdopen(initramfsfd, 'r+b') + mkinitramfs.mkinitramfs(initramfsfile, config) + initramfsfile.flush() + if args.save_initramfs is not None: + initrdpath = args.save_initramfs + else: + fcntl.fcntl(initramfsfd, fcntl.F_SETFD, 0) + initrdpath = '/proc/self/fd/%d' % initramfsfd + else: + if args.save_initramfs is not None: + print('--save_initramfs specified but initramfs is not used', + file=sys.stderr) + return 1 + + # No initramfs! Warning: this is slower than using an initramfs + # because the kernel will wait for device probing to finish. + # Sigh. + kernelargs.extend([ + 'rootfstype=9p', + 'rootflags=version=9p2000.L,trans=virtio,access=any', + 'raid=noautodetect', + 'rw' if args.rw else 'ro', + ]) + initrdpath = None + initcmds.insert(0, 'mount -t tmpfs run /run') + + # Now that we're done setting up kernelargs, append user-specified args + # and then initargs + kernelargs.extend(args.kopt) + + # Unknown options get turned into arguments to init, which is annoying + # because we're explicitly passing '--' to set the arguments directly. + # Fortunately, 'init=' will clear any arguments parsed so far, so make + # sure that 'init=' appears directly before '--'. + kernelargs.append('init=/bin/sh') + kernelargs.append('--') + kernelargs.extend(['-c', ';'.join(initcmds)]) + + if args.xen is None: + # Load a normal kernel + qemuargs.extend(['-kernel', kernel.kimg]) + if kernelargs: + qemuargs.extend(['-append', + ' '.join(quote_karg(a) for a in kernelargs)]) + if initrdpath is not None: + qemuargs.extend(['-initrd', initrdpath]) + if kernel.dtb is not None: + qemuargs.extend(['-dtb', kernel.dtb]) + + if xenargs: + raise ValueError("Can't pass Xen any arguments if we're not using Xen") + else: + # Use multiboot syntax to load Xen + qemuargs.extend(['-kernel', args.xen]) + if xenargs: + qemuargs.extend(['-append', + ' '.join(quote_karg(a) for a in xenargs)]) + qemuargs.extend(['-initrd', '%s %s%s' % ( + kernel.kimg, + ' '.join(quote_karg(a).replace(',', ',,') for a in kernelargs), + (',%s' % initrdpath) if initrdpath is not None else '')]) + + # Handle --qemu-opt(s) + qemuargs.extend(args.qemu_opt) + if args.qemu_opts is not None: + qemuargs.extend(args.qemu_opts) + + if args.show_command: + print(' '.join(shlex.quote(a) for a in qemuargs)) + + # Go! + if not args.dry_run: + os.execv(qemu.qemubin, qemuargs) + + return 0 + +def main() -> int: + try: + return do_it() + except SilentError: + return 1 + +if __name__ == '__main__': + try: + exit(main()) + except SilentError: + exit(1) diff --git a/virtme/cpiowriter.py b/virtme/cpiowriter.py new file mode 100755 index 0000000..7f0a995 --- /dev/null +++ b/virtme/cpiowriter.py @@ -0,0 +1,86 @@ +# -*- mode: python -*- +# cpiowriter: A barebones initramfs writer +# Copyright © 2014 Andy Lutomirski +# Licensed under the GPLv2, which is available in the virtme distribution +# as a file called LICENSE with SHA-256 hash: +# 8177f97513213526df2cf6184d8ff986c675afb514d4e68a404010521b880643 + +import sys + +class CpioWriter(object): + TYPE_DIR = 0o0040000 + TYPE_REG = 0o0100000 + TYPE_SYMLINK = 0o0120000 + TYPE_CHRDEV = 0o0020000 + TYPE_MASK = 0o0170000 + + def __init__(self, f): + self.__f = f + self.__totalsize = 0 + self.__next_ino = 0 + + def __write(self, data): + self.__f.write(data) + self.__totalsize += len(data) + + def write_object(self, name, body, mode, ino=None, nlink=None, + uid=0, gid=0, mtime=0, devmajor=0, devminor=0, + rdevmajor=0, rdevminor=0): + if nlink is None: + nlink = (2 if (mode & CpioWriter.TYPE_MASK) == CpioWriter.TYPE_DIR + else 1) + + if b'\0' in name: + raise ValueError('Filename cannot contain a NUL') + + namesize = len(name) + 1 + + if isinstance(body, bytes): + filesize = len(body) + else: + filesize = body.seek(0, 2) + body.seek(0) + + if ino is None: + ino = self.__next_ino + self.__next_ino += 1 + + fields = [ino, mode, uid, gid, nlink, mtime, filesize, + devmajor, devminor, rdevmajor, rdevminor, namesize, 0] + hdr = ('070701' + ''.join('%08X' % f for f in fields)).encode('ascii') + + self.__write(hdr) + self.__write(name) + self.__write(b'\0') + self.__write(((2-namesize) % 4) * b'\0') + + if isinstance(body, bytes): + self.__write(body) + else: + while True: + buf = body.read(65536) + if buf == b'': + break + self.__write(buf) + + self.__write(((-filesize) % 4) * b'\0') + + def write_trailer(self): + self.write_object(name=b'TRAILER!!!', body=b'', mode=0, ino=0, nlink=1) + self.__write(((-self.__totalsize) % 512) * b'\0') + + def mkdir(self, name, mode): + self.write_object(name=name, mode=CpioWriter.TYPE_DIR | mode, body=b'') + + def symlink(self, src, dst): + self.write_object(name=dst, mode=CpioWriter.TYPE_SYMLINK | 0o777, + body=src) + + def write_file(self, name, body, mode): + self.write_object(name=name, body=body, mode=CpioWriter.TYPE_REG | mode) + + def mkchardev(self, name, dev, mode): + major,minor = dev + self.write_object(name=name, mode=CpioWriter.TYPE_CHRDEV | mode, + rdevmajor=major, rdevminor=minor, + body=b'') diff --git a/virtme/guest/__init__.py b/virtme/guest/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/virtme/guest/virtme-init b/virtme/guest/virtme-init new file mode 100755 index 0000000..ae93f3e --- /dev/null +++ b/virtme/guest/virtme-init @@ -0,0 +1,291 @@ +#!/bin/bash +# virtme-init: virtme's basic init (PID 1) process +# Copyright © 2014 Andy Lutomirski +# Licensed under the GPLv2, which is available in the virtme distribution +# as a file called LICENSE with SHA-256 hash: +# 8177f97513213526df2cf6184d8ff986c675afb514d4e68a404010521b880643 + +export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin + +log() { + if [[ -e /dev/kmsg ]]; then + echo "<6>virtme-init: $*" >/dev/kmsg + else + echo "virtme-init: $*" + fi +} + +mount -t sysfs -o nosuid,noexec,nodev sys /sys/ + +declare -A mount_tags +for i in /sys/bus/virtio/drivers/9pnet_virtio/virtio*/mount_tag; do + # mount_tag is terminated with a NUL byte, which leads to a + # "command substitution: ignored null byte in input" warning from + # bash; use sed instead of a bare 'cat' here to strip it off. + mount_tags["`sed '$s/\x00$//;' "$i"`"]=1 +done + +# Setup rw filesystem overlays +mount -t tmpfs tmpfs /tmp/ + +for tag in "${!virtme_rw_overlay@}"; do + td=`mktemp -d` + dir="${!tag}" + mount -t tmpfs none "$td" + mkdir "$td/cow" "$td/work" + mount -t overlay -o "lowerdir=$dir,upperdir=$td/cow,workdir=$td/work" cow "$dir" + umount "$td" + rmdir "$td" +done + +# Setup kernel modules +kver="`uname -r`" + +if [[ -n "$virtme_root_mods" ]]; then + # /lib/modules is already set up + true +elif [[ -n "$virtme_link_mods" ]]; then + mount -n -t tmpfs none /lib/modules + ln -s "$virtme_link_mods" "/lib/modules/$kver" +elif [[ -d "/lib/modules/$kver" ]]; then + # We may have mismatched modules. Mask them off. + mount -n -t tmpfs -o ro,mode=0000 disallow_modules "/lib/modules/$kver" +fi + +# Setup rw tmpfs directories +[ -e /var/log ] && mount -t tmpfs tmpfs /var/log/ +[ -e /var/tmp ] && mount -t tmpfs tmpfs /var/tmp/ + +# Additional rw dirs used by systemd +[ -e /var/spool/rsyslog ] && mount -t tmpfs tmpfs /var/spool/rsyslog +[ -e /var/lib/portables ] && mount -t tmpfs tmpfs /var/lib/portables +[ -e /var/lib/machines ] && mount -t tmpfs tmpfs /var/lib/machines +[ -e /var/lib/private ] && mount -t tmpfs tmpfs /var/lib/private +[ -e /var/cache/private ] && mount -t tmpfs tmpfs /var/cache/private + +# Additional rw dirs required by apt +[ -e /var/lib/apt ] && mount -t tmpfs tmpfs /var/lib/apt +[ -e /var/cache ] && mount -t tmpfs tmpfs /var/cache + +# Fix up /etc a little bit +touch /tmp/fstab +mount --bind /tmp/fstab /etc/fstab +rm /tmp/fstab + +# Populate dummy entries in /etc/shadow to allow switching to any user defined +# in the system +(umask 0644 && touch /tmp/shadow) +sed -e 's/^\([^:]\+\).*/\1:!:::::::/' < /etc/passwd > /tmp/shadow +mount --bind /tmp/shadow /etc/shadow +rm /tmp/shadow + +# Find udevd +if [[ -x /usr/lib/systemd/systemd-udevd ]]; then + udevd=/usr/lib/systemd/systemd-udevd +elif [[ -x /lib/systemd/systemd-udevd ]]; then + udevd=/lib/systemd/systemd-udevd +else + udevd=`which udevd` +fi + +# Mount proc (needed for stat, sadly) +mount -t proc -o nosuid,noexec,nodev proc /proc/ + +# devtmpfs might be automounted; if not, mount it. +if ! findmnt --kernel --mountpoint /dev &>/dev/null; then + # Ideally we'll use devtmpfs (but don't rely on /dev/null existing). + if [[ -c /dev/null ]]; then + mount -n -t devtmpfs -o mode=0755,nosuid,noexec devtmpfs /dev \ + &>/dev/null + else + mount -n -t devtmpfs -o mode=0755,nosuid,noexec devtmpfs /dev + fi + + if (( $? != 0 )); then + # The running kernel doesn't have devtmpfs. Use regular tmpfs. + mount -t tmpfs -o mode=0755,nosuid,noexec none /dev + + # Make some basic devices first, and let udev handle the rest + mknod -m 0666 /dev/null c 1 3 + mknod -m 0660 /dev/kmsg c 1 11 + mknod -m 0600 /dev/console c 5 1 + fi +fi + +for tag in "${!virtme_initmount@}"; do + if [[ ! -d "${!tag}" ]]; then + mkdir -p "${!tag}" + fi + mount -t 9p -o version=9p2000.L,trans=virtio,access=any "virtme.initmount${tag:16}" "${!tag}" || exit 1 +done + +if [[ -n "${virtme_chdir}" ]]; then + cd -- "${virtme_chdir}" +fi + +log "basic initialization done" + +######## The remainder of this script is a very simple init (PID 1) ######## + +# Does the system use systemd-tmpfiles? +tmpfiles=`which systemd-tmpfiles 2>/dev/null` && { + log "running systemd-tmpfiles" + systemd-tmpfiles --create --boot --exclude-prefix="/dev" --exclude-prefix="/root" +} + +# Make dbus work (if tmpfiles wasn't there or didn't create the directory). +install -d /run/dbus + +# Try to get udevd to coldplug everything. +if [[ -n "$udevd" ]]; then + if [[ -e '/sys/kernel/uevent_helper' ]]; then + # This kills boot performance. + log "you have CONFIG_UEVENT_HELPER on; turn it off" + echo '' >/sys/kernel/uevent_helper + fi + log "starting udevd" + "$udevd" --daemon --resolve-names=never + log "triggering udev coldplug" + udevadm trigger --type=subsystems --action=add >/dev/null 2>&1 + udevadm trigger --type=devices --action=add >/dev/null 2>&1 + log "waiting for udev to settle" + udevadm settle + log "udev is done" +else + log "udevd not found" +fi + +# Set up useful things in /sys, assuming our kernel supports it. +mount -t configfs configfs /sys/kernel/config &>/dev/null +mount -t debugfs debugfs /sys/kernel/debug &>/dev/null +mount -t tracefs tracefs /sys/kernel/tracing &>/dev/null + +# Set up cgroup mount points (mount cgroupv2 hierarchy by default) +if mount -t tmpfs tmpfs /sys/fs/cgroup &>/dev/null; then + mkdir /sys/fs/cgroup/unified + mount -t cgroup2 cgroup2 /sys/fs/cgroup/unified +fi + +# Set up filesystems that live in /dev +mkdir -p -m 0755 /dev/shm /dev/pts +mount -t devpts -o gid=tty,mode=620,noexec,nosuid devpts /dev/pts +mount -t tmpfs -o mode=1777,nosuid,nodev tmpfs /dev/shm + +# Install /proc/self/fd symlinks into /dev if not already present +declare -r -A fdlinks=(["/dev/fd"]="/proc/self/fd" + ["/dev/stdin"]="/proc/self/fd/0" + ["/dev/stdout"]="/proc/self/fd/1" + ["/dev/stderr"]="/proc/self/fd/2") + +for p in "${!fdlinks[@]}"; do + [[ -e "$p" ]] || ln -s "${fdlinks[$p]}" "$p" +done + +if [[ -n "$virtme_hostname" ]]; then + log "Setting hostname to $virtme_hostname..." + hostname "$virtme_hostname" +fi + +# Bring up networking +ip link set dev lo up + +if [[ -n "virtme_user" ]]; then + real_sudoers=/etc/sudoers + tmpfile="`mktemp --tmpdir=/tmp`" + echo "root ALL = (ALL) NOPASSWD: ALL" > $tmpfile + echo "${virtme_user} ALL = (ALL) NOPASSWD: ALL" >> $tmpfile + chmod 440 "$tmpfile" + mount --bind "$tmpfile" "$real_sudoers" + rm "$tmpfile" +fi + +if cat /proc/cmdline |grep -q -E '(^| )virtme.dhcp($| )'; then + real_resolv_conf=/etc/resolv.conf + if [[ -L "$real_resolv_conf" ]]; then + real_resolv_conf="/`readlink /etc/resolv.conf`" + if [[ ! -e $real_resolv_conf ]]; then + mkdir -p "`dirname $real_resolv_conf`" + touch $real_resolv_conf + fi + fi + if [[ -f "$real_resolv_conf" ]]; then + tmpfile="`mktemp --tmpdir=/tmp`" + chmod 644 "$tmpfile" + mount --bind "$tmpfile" "$real_resolv_conf" + rm "$tmpfile" + fi + + # udev is liable to rename the interface out from under us. + virtme_net=`ls "$(ls -d /sys/bus/virtio/drivers/virtio_net/virtio* |sort -g |head -n1)"/net` + busybox udhcpc -i "$virtme_net" -n -q -f -s "$(dirname $0)/virtme-udhcpc-script" +fi + +if [[ -x /run/virtme/data/script ]]; then + if [[ ! -e "/dev/virtio-ports/virtme.stdin" || ! -e "/dev/virtio-ports/virtme.stdout" ]]; then + echo "virtme-init: cannot find script I/O ports; make sure virtio-serial is available" + poweroff -f + exit 1 + fi + + log 'starting script' + setsid /run/virtme/data/script /dev/virtio-ports/virtme.stdout 2>&1 + log "script returned $?" + + # Hmm. We should expose the return value somehow. + sync + poweroff -f + exit 1 +fi + +# Figure out what the main console is +consdev="`grep ' ... (.C' /proc/consoles |cut -d' ' -f1`" +if [[ -z "$consdev" ]]; then + log "can't deduce console device" + exec bash --login # At least try to be helpful +fi + +deallocvt + +if [[ "$consdev" == "tty0" ]]; then + # Create some VTs + openvt -c 2 -- /bin/bash + openvt -c 3 -- /bin/bash + openvt -c 4 -- /bin/bash + + consdev=tty1 # sigh +fi + +if [[ ! -e "/dev/$consdev" ]]; then + log "/dev/$consdev doesn't exist." + exec bash --login +fi + +# Parameters that start with virtme_ shouldn't pollute the environment +for p in "${!virtme_@}"; do export -n "$p"; done + +echo "virtme-init: console is $consdev" + +# Set up a basic environment +install -d -m 0755 /tmp/roothome +export HOME=/tmp/roothome +mount --bind /tmp/roothome /root + +# Bring up a functioning shell on the console. This is a bit magical: +# We have no controlling terminal because we're attached to a fake +# console device (probably something like /dev/console), which can't +# be a controlling terminal. We are also not a member of a session. +# Init apparently can't setsid (whether that's a limitation of the +# setsid binary or the system call, I don't know). +while true; do + # Program the console sensibly + if [[ -n "${virtme_stty_con}" ]]; then + stty ${virtme_stty_con} <"/dev/$consdev" + fi + if [[ -n "${virtme_user}" ]]; then + setsid bash -c "su ${virtme_user}" 0<>"/dev/$consdev" 1>&0 2>&0 + else + setsid bash 0<>"/dev/$consdev" 1>&0 2>&0 + fi + echo "Shell died. Will respawn." + sleep 0.5 +done diff --git a/virtme/guest/virtme-udhcpc-script b/virtme/guest/virtme-udhcpc-script new file mode 100755 index 0000000..d9876d5 --- /dev/null +++ b/virtme/guest/virtme-udhcpc-script @@ -0,0 +1,24 @@ +#!/bin/bash +# virtme-udhcpc-script: A trivial udhcpc script +# Copyright © 2014 Andy Lutomirski +# Licensed under the GPLv2, which is available in the virtme distribution +# as a file called LICENSE with SHA-256 hash: +# 8177f97513213526df2cf6184d8ff986c675afb514d4e68a404010521b880643 + +if [[ "$1" == "deconfig" ]]; then + ip link set dev "$interface" up + ip addr flush dev "$interface" +elif [[ "$1" == "bound" ]]; then + ip addr add "$ip/$mask" dev "$interface" + ip route add default via "$router" dev "$interface" + if [[ -n "$dns" ]]; then + # A lot of systems will have /etc/resolv.conf symlinked to + # /run/NetworkManager/something_or_other. Debian symlinks to /run/resolvconf. + # Create both directories. + install -d /run/NetworkManager + install -d /run/resolvconf + + echo -e "# Generated by virtme-udhcpc-script\n\nnameserver $dns" \ + >/etc/resolv.conf + fi +fi diff --git a/virtme/mkinitramfs.py b/virtme/mkinitramfs.py new file mode 100644 index 0000000..1a21c9b --- /dev/null +++ b/virtme/mkinitramfs.py @@ -0,0 +1,172 @@ +# -*- mode: python -*- +# virtme-mkinitramfs: Generate an initramfs image for virtme +# Copyright © 2014 Andy Lutomirski +# Licensed under the GPLv2, which is available in the virtme distribution +# as a file called LICENSE with SHA-256 hash: +# 8177f97513213526df2cf6184d8ff986c675afb514d4e68a404010521b880643 + +from typing import List, Dict, Optional + +import shutil +import io +import os.path +import shlex +import itertools +from . import cpiowriter +from . import util + +def make_base_layout(cw): + for dir in (b'lib', b'bin', b'var', b'etc', b'newroot', b'dev', b'proc', + b'tmproot', b'run_virtme', b'run_virtme/data', b'run_virtme/guesttools'): + cw.mkdir(dir, 0o755) + + cw.symlink(b'bin', b'sbin') + cw.symlink(b'lib', b'lib64') + +def make_dev_nodes(cw): + cw.mkchardev(b'dev/null', (1, 3), mode=0o666) + cw.mkchardev(b'dev/kmsg', (1, 11), mode=0o666) + cw.mkchardev(b'dev/console', (5, 1), mode=0o660) + +def install_busybox(cw, config): + with open(config.busybox, 'rb') as busybox: + cw.write_file(name=b'bin/busybox', body=busybox, mode=0o755) + + for tool in ('sh', 'mount', 'umount', 'switch_root', 'sleep', 'mkdir', + 'mknod', 'insmod', 'cp', 'cat'): + cw.symlink(b'busybox', ('bin/%s' % tool).encode('ascii')) + + cw.mkdir(b'bin/real_progs', mode=0o755) + +def install_modprobe(cw): + cw.write_file(name=b'bin/modprobe', body=b'\n'.join([ + b'#!/bin/sh', + b'echo "virtme: initramfs does not have module $3" >/dev/console', + b'exit 1', + ]), mode=0o755) + +_LOGFUNC = """log() { + if [[ -e /dev/kmsg ]]; then + echo "<6>virtme initramfs: $*" >/dev/kmsg + else + echo "virtme initramfs: $*" + fi +} +""" + +def install_modules(cw, modfiles): + cw.mkdir(b'modules', 0o755) + paths = [] + for mod in modfiles: + with open(mod, 'rb') as f: + modpath = 'modules/' + os.path.basename(mod) + paths.append(modpath) + cw.write_file(name=modpath.encode('ascii'), + body=f, mode=0o644) + + script = _LOGFUNC + '\n'.join('log \'loading %s...\'; insmod %s' % + (os.path.basename(p), shlex.quote(p)) for p in paths) + cw.write_file(name=b'modules/load_all.sh', + body=script.encode('ascii'), mode=0o644) + +_INIT = """#!/bin/sh + +{logfunc} + +source /modules/load_all.sh + +log 'mounting hostfs...' + +if ! /bin/mount -n -t 9p -o {access},version=9p2000.L,trans=virtio,access=any /dev/root /newroot/; then + echo "Failed to mount real root. We are stuck." + sleep 5 + exit 1 +fi + +# Can we actually use /newroot/ as root? +if ! mount -t proc -o nosuid,noexec,nodev proc /newroot/proc 2>/dev/null; then + # QEMU 1.5 and below have a bug in virtfs that prevents mounting + # anything on top of a virtfs mount. + log "your host's virtfs is broken -- using a fallback tmpfs" + need_fallback_tmpfs=1 +else + umount /newroot/proc # Don't leave garbage behind +fi + +if ! [[ -d /newroot/run ]]; then + log "your guest's root does not have /run -- using a fallback tmpfs" + need_fallback_tmpfs=1 +fi + +if [[ "$need_fallback_tmpfs" != "" ]]; then + mount --move /newroot /tmproot + mount -t tmpfs root_workaround /newroot/ + cd tmproot + mkdir /newroot/proc /newroot/sys /newroot/dev /newroot/run /newroot/tmp + for i in *; do + if [[ -d "$i" && \! -d "/newroot/$i" ]]; then + mkdir /newroot/"$i" + mount --bind "$i" /newroot/"$i" + fi + done + mknod /newroot/dev/null c 1 3 + mount -o remount,ro -t tmpfs root_workaround /newroot + umount -l /tmproot +fi + +mount -t tmpfs run /newroot/run +cp -a /run_virtme /newroot/run/virtme + +# Find init +mount -t proc none /proc +for arg in `cat /proc/cmdline`; do + if [[ "${{arg%%=*}}" = "init" ]]; then + init="${{arg#init=}}" + break + fi +done +umount /proc + +if [[ -z "$init" ]]; then + log 'no init= option' + exit 1 +fi + +log 'done; switching to real root' +exec /bin/switch_root /newroot "$init" "$@" +""" + + +def generate_init(config) -> bytes: + out = io.StringIO() + out.write(_INIT.format( + logfunc=_LOGFUNC, + access=config.access)) + return out.getvalue().encode('utf-8') + +class Config: + __slots__ = ['modfiles', 'virtme_data', 'virtme_init_path', 'busybox', 'access'] + def __init__(self): + self.modfiles: List[str] = [] + self.virtme_data: Dict[bytes, bytes] = {} + self.virtme_init_path: Optional[str] = None + self.busybox: Optional[str] = None + self.access = 'ro' + +def mkinitramfs(out, config) -> None: + cw = cpiowriter.CpioWriter(out) + make_base_layout(cw) + make_dev_nodes(cw) + install_busybox(cw, config) + install_modprobe(cw) + if config.modfiles is not None: + install_modules(cw, config.modfiles) + for name,contents in config.virtme_data.items(): + cw.write_file(b'run_virtme/data/' + name, body=contents, mode=0o755) + cw.write_file(b'init', body=generate_init(config), + mode=0o755) + cw.write_trailer() + +def find_busybox(root, is_native) -> Optional[str]: + return util.find_binary(['busybox-static', 'busybox.static', 'busybox'], + root=root, use_path=is_native) diff --git a/virtme/modfinder.py b/virtme/modfinder.py new file mode 100644 index 0000000..88f7df57 --- /dev/null +++ b/virtme/modfinder.py @@ -0,0 +1,65 @@ +# -*- mode: python -*- +# modfinder: A simple tool to resolve required modules +# Copyright © 2014 Andy Lutomirski +# Licensed under the GPLv2, which is available in the virtme distribution +# as a file called LICENSE with SHA-256 hash: +# 8177f97513213526df2cf6184d8ff986c675afb514d4e68a404010521b880643 + +""" +This is a poor man's module resolver and loader. It does not support any +sort of hotplug. Instead it generates a topological order and loads +everything. The idea is to require very few modules. +""" + +from typing import List + +import re +import subprocess +import os +import itertools +from . import util + +_INSMOD_RE = re.compile('insmod (.*[^ ]) *$') + +def resolve_dep(modalias, root=None, kver=None, moddir=None): + # /usr/sbin might not be in the path, and modprobe is usually in /usr/sbin + modprobe = util.find_binary_or_raise(['modprobe']) + args = [modprobe, '--show-depends'] + args += ['-C', '/var/empty'] + if root is not None: + args += ['-d', root] + if kver is not None and kver != os.uname().release: + # If booting the loaded kernel, skip -S. This helps certain + # buggy modprobe versions that don't support -S. + args += ['-S', kver] + if moddir is not None: + args += ['--moddir', moddir] + args += ['--', modalias] + + deps = [] + + try: + with open('/dev/null', 'r+b') as devnull: + script = subprocess.check_output(args, stderr=devnull.fileno()).\ + decode('utf-8', errors='replace') + for line in script.split('\n'): + m = _INSMOD_RE.match(line) + if m: + deps.append(m.group(1)) + except subprocess.CalledProcessError: + pass # This is most likely because the module is built in. + + return deps + +def merge_mods(lists) -> List[str]: + found: set = set() + mods = [] + for mod in itertools.chain(*lists): + if mod not in found: + found.add(mod) + mods.append(mod) + return mods + +def find_modules_from_install(aliases, root=None, kver=None, moddir=None): + return merge_mods(resolve_dep(a, root=root, kver=kver, moddir=moddir) + for a in aliases) diff --git a/virtme/qemu_helpers.py b/virtme/qemu_helpers.py new file mode 100644 index 0000000..76a597c --- /dev/null +++ b/virtme/qemu_helpers.py @@ -0,0 +1,50 @@ +# -*- mode: python -*- +# qemu_helpers: Helpers to find QEMU and handle its quirks +# Copyright © 2014 Andy Lutomirski +# Licensed under the GPLv2, which is available in the virtme distribution +# as a file called LICENSE with SHA-256 hash: +# 8177f97513213526df2cf6184d8ff986c675afb514d4e68a404010521b880643 + +import os +import re +import shutil +import subprocess +from typing import Optional + +class Qemu(object): + qemubin: str + version: Optional[str] + + def __init__(self, qemubin, arch) -> None: + self.arch = arch + + if not qemubin: + qemubin = shutil.which('qemu-system-%s' % arch) + if qemubin is None and arch == os.uname().machine: + qemubin = shutil.which('qemu-kvm') + if qemubin is None: + raise ValueError('cannot find qemu for %s' % arch) + else: + if not os.path.isfile(qemubin): + raise ValueError('specified qemu binary "%s" does not exist' % qemubin) + if not os.access(qemubin, os.X_OK): + raise ValueError('specified qemu binary "%s" is not executable' % qemubin) + + self.qemubin = qemubin + self.version = None + + def probe(self) -> None: + if self.version is None: + self.version = subprocess.check_output([self.qemubin, '--version'])\ + .decode('utf-8') + self.cannot_overmount_virtfs = ( + re.search(r'version 1\.[012345]', self.version) is not None) + + # QEMU 4.2+ supports -fsdev multidevs=remap + self.has_multidevs = ( + re.search(r'version (?:1\.|2\.|3\.|4\.[01][^\d])', self.version) is None) + + def quote_optarg(self, a: str) -> str: + """Quote an argument to an option.""" + return a.replace(',', ',,') + diff --git a/virtme/resources.py b/virtme/resources.py new file mode 100644 index 0000000..d07e981 --- /dev/null +++ b/virtme/resources.py @@ -0,0 +1,42 @@ +# -*- mode: python -*- +# resources.py: Find virtme's resources +# Copyright © 2014-2019 Andy Lutomirski +# Licensed under the GPLv2, which is available in the virtme distribution +# as a file called LICENSE with SHA-256 hash: +# 8177f97513213526df2cf6184d8ff986c675afb514d4e68a404010521b880643 + +"""Helpers to find virtme's guest tools and host scripts.""" + +import os +import shutil +import pkg_resources +import subprocess + +def find_guest_tools(): + """Return the path of the guest tools installed with the running virtme. + """ + + if pkg_resources.resource_isdir(__name__, 'guest'): + return pkg_resources.resource_filename(__name__, 'guest') + + # No luck. This is somewhat surprising. + return None + +def find_script(name) -> str: + # If we're running out of a source checkout, we can find scripts through + # the 'virtme/scripts' symlink. + fn = pkg_resources.resource_filename(__name__, 'scripts/%s' % name) + if os.path.isfile(fn): + return fn + + # Otherwise assume we're actually installed and in PATH. + guess = shutil.which(name) + if guess is not None: + return guess + + # No luck. This is somewhat surprising. + raise Exception('could not find script %s' % name) + +def run_script(name, **kwargs) -> None: + fn = find_script(name) + subprocess.check_call(executable=fn, args=[fn], **kwargs) diff --git a/virtme/scripts b/virtme/scripts new file mode 120000 index 0000000..19f285a --- /dev/null +++ b/virtme/scripts @@ -0,0 +1 @@ +../bin \ No newline at end of file diff --git a/virtme/util.py b/virtme/util.py new file mode 100644 index 0000000..cc1787b --- /dev/null +++ b/virtme/util.py @@ -0,0 +1,40 @@ +# -*- mode: python -*- +# util.py: Misc helpers +# Copyright © 2014-2019 Andy Lutomirski +# Licensed under the GPLv2, which is available in the virtme distribution +# as a file called LICENSE with SHA-256 hash: +# 8177f97513213526df2cf6184d8ff986c675afb514d4e68a404010521b880643 + +from typing import Optional, Sequence + +import os +import shutil +import itertools + +def find_binary(names: Sequence[str], root: str = '/', + use_path: bool = True) -> Optional[str]: + dirs = [os.path.join(*i) for i in itertools.product( + ['usr/local', 'usr', ''], + ['bin', 'sbin'])] + + for n in names: + if use_path: + # Search PATH first + path = shutil.which(n) + if path is not None: + return path + + for d in dirs: + path = os.path.join(root, d, n) + if os.path.isfile(path): + return path + + # We give up. + return None + +def find_binary_or_raise(names: Sequence[str], root: str = '/', + use_path: bool = True) -> str: + ret = find_binary(names, root=root, use_path=use_path) + if ret is None: + raise RuntimeError('Could not find %r' % names) + return ret diff --git a/virtme/virtmods.py b/virtme/virtmods.py new file mode 100644 index 0000000..8f43652 --- /dev/null +++ b/virtme/virtmods.py @@ -0,0 +1,27 @@ +# -*- mode: python -*- +# virtmods: Default module configuration +# Copyright © 2014 Andy Lutomirski +# Licensed under the GPLv2, which is available in the virtme distribution +# as a file called LICENSE with SHA-256 hash: +# 8177f97513213526df2cf6184d8ff986c675afb514d4e68a404010521b880643 + +MODALIASES = [ + # These are most likely portable across all architectures. + 'fs-9p', + 'virtio:d00000009v00001AF4', # 9pnet_virtio + 'virtio:d00000003v00001AF4', # virtio_console + + # For virtio_pci architectures (which are, hopefully, all that we care + # about), there's really only one required driver, virtio_pci. + # For completeness, here are both of the instances we care about + # for basic functionality. + 'pci:v00001AF4d00001009sv00001AF4sd00000009bc00sc02i00', # 9pnet + 'pci:v00001AF4d00001003sv00001AF4sd00000003bc07sc80i00', # virtconsole + + # Basic system functionality + 'unix', # UNIX sockets, needed by udev + + # Basic emulated hardware + 'i8042', + 'atkbd', +]