diff --git a/Makefile b/Makefile index 2cc7f9ee..83af3b3e 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,7 @@ all: $(MAKE) -C qrexec-lib all $(MAKE) -C qmemman all $(MAKE) -C imgconverter all + $(MAKE) -C not-script all selinux: $(MAKE) -f /usr/share/selinux/devel/Makefile -C selinux qubes-meminfo-writer.pp @@ -18,6 +19,7 @@ install: $(MAKE) -C qrexec-lib install $(MAKE) -C qmemman install $(MAKE) -C imgconverter install + $(MAKE) -C not-script install install-selinux: install -m 0644 -D -t $(DESTDIR)/usr/share/selinux/packages selinux/qubes-meminfo-writer.pp @@ -37,6 +39,7 @@ clean: $(MAKE) -C qrexec-lib clean $(MAKE) -C qmemman clean $(MAKE) -C imgconverter clean + $(MAKE) -C not-script clean rm -rf selinux/*.pp selinux/tmp/ rm -rf debian/changelog.* rm -rf pkgs diff --git a/archlinux/PKGBUILD b/archlinux/PKGBUILD index 44d3800a..a0f6b163 100644 --- a/archlinux/PKGBUILD +++ b/archlinux/PKGBUILD @@ -31,8 +31,8 @@ md5sums=(SKIP) build() { -for source in qrexec-lib udev qmemman core kernel-modules Makefile dracut imgconverter grub; do - (ln -s -- "$srcdir/../$source" "$srcdir/$source") +for source in qrexec-lib udev qmemman core kernel-modules Makefile dracut imgconverter grub not-script; do + ln -s -- "$srcdir/../$source" "$srcdir/$source" done make all diff --git a/debian/qubes-utils.install b/debian/qubes-utils.install index 31f6c977..4231c22e 100644 --- a/debian/qubes-utils.install +++ b/debian/qubes-utils.install @@ -3,3 +3,4 @@ lib/systemd/system/qubes-meminfo-writer.service usr/lib/qubes/* usr/lib/udev/* usr/lib/tmpfiles.d/xen-devices-qubes.conf +etc/xen/scripts/qubes-block diff --git a/not-script/Makefile b/not-script/Makefile new file mode 100644 index 00000000..11504b84 --- /dev/null +++ b/not-script/Makefile @@ -0,0 +1,16 @@ +CC=gcc +CFLAGS := $(CFLAGS) -g3 -O2 -Wall -Wextra -Werror -fPIE -D_FORTIFY_SOURCE=2 -D_GNU_SOURCE +.PHONY: clean install all +all: not-script + +%: %.c Makefile + s=$$(pkg-config --cflags --libs xenstore) && $(CC) $(CFLAGS) -MD -MP -MF $@.dep -o $@ $< $$s + +clean: + rm -f ./*.o ./*~ ./*.a ./*.so.* ./*.dep unicode-class-table.c + +install: + install -d $(DESTDIR)/etc/xen/scripts + install not-script $(DESTDIR)/etc/xen/scripts/qubes-block + +-include ./*.o.dep diff --git a/not-script/not-script.c b/not-script/not-script.c new file mode 100644 index 00000000..c91eeaf4 --- /dev/null +++ b/not-script/not-script.c @@ -0,0 +1,362 @@ +#define _GNU_SOURCE 1 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include +#include + +static int open_file(const char *path) { + return open(path, O_RDONLY | O_NOCTTY | O_CLOEXEC | O_NOFOLLOW); +} + +/* A simple library for loop device ioctls */ +struct loop_context { + uint32_t fd; +}; + +static int setup_loop(struct loop_context *ctx, + uint32_t fd, + uint64_t offset, + uint64_t sizelimit, + bool writable) { + struct loop_config config = { + .fd = fd, + .block_size = 0, /* FIXME! */ + .info = { + .lo_offset = offset, + .lo_sizelimit = sizelimit, + .lo_flags = LO_FLAGS_AUTOCLEAR | LO_FLAGS_DIRECT_IO | + (writable ? 0 : LO_FLAGS_READ_ONLY), + }, + }; + + char buf[sizeof("/dev/loop") + 10]; + + int dev_file = -1, status; + for (int retry_count = 0; retry_count < 5 /* arbitrary */; retry_count++) { + if ((status = ioctl(ctx->fd, LOOP_CTL_GET_FREE)) == -1) + return -errno; + if ((unsigned)snprintf(buf, sizeof buf, "/dev/loop%u", (unsigned)status) >= sizeof buf) + abort(); + dev_file = open(buf, (writable ? O_RDWR : O_RDONLY) | + O_EXCL | O_CLOEXEC | O_NOCTTY | O_NOFOLLOW); + if (dev_file > 0) { + switch (ioctl(dev_file, LOOP_CONFIGURE, &config)) { + case 0: + return dev_file; + case -1: + if (close(dev_file)) + abort(); /* cannot happen on Linux */ + return -1; + default: + abort(); + } + } + if (errno != EBUSY) + break; + } + return -1; +} + +#define DEV_MAPPER "/dev/mapper/" +#define DEV_MAPPER_SIZE (sizeof DEV_MAPPER - 1) + +static void +process_blk_dev(int fd, const char *path, bool writable, dev_t *dev, + uint64_t *diskseq, bool permissive) +{ + struct stat statbuf; + char buf[45]; + + if (fstat(fd, &statbuf)) + err(1, "fstat"); + + if (S_ISBLK(statbuf.st_mode)) { + if (permissive) + goto skip; + + /* block device */ + if (strncmp(DEV_MAPPER, path, DEV_MAPPER_SIZE)) + errx(1, "Only device-mapper block devices are supported (got %s)", path); + + const char *const devname = path + DEV_MAPPER_SIZE; + size_t const devname_len = strlen(devname); + if (strcmp(devname, "control") == 0 || + strcmp(devname, ".") == 0 || + strcmp(devname, "..") == 0 || + memchr(devname, '/', devname_len) != NULL) + { + errx(1, "Forbidden path %s", devname); + } + + if ((unsigned)snprintf(buf, sizeof buf, "/sys/dev/block/%" PRIu32 ":%" PRIu32 "/dm/name", + major(statbuf.st_rdev), minor(statbuf.st_rdev)) >= sizeof(buf)) + err(1, "snprintf"); + + int const sysfs_fd = open(buf, O_RDONLY | O_NOCTTY | O_NOFOLLOW); + if (sysfs_fd < 0) + err(1, "open(%s)", buf); + + char *const cmp_ptr = malloc(devname_len + 2); + if (!cmp_ptr) + err(1, "malloc()"); + + ssize_t const read_res = read(sysfs_fd, cmp_ptr, devname_len + 2); + if (read_res < 0) + err(1, "read(%s)", buf); + + if (((size_t)read_res != devname_len + 1) || + (memcmp(devname, cmp_ptr, devname_len) != 0) || + (cmp_ptr[devname_len] != '\n')) + { + errx(1, "Opened the wrong device-mapper device!"); + } + + free(cmp_ptr); + if (close(sysfs_fd)) + err(1, "close()"); + } else if (S_ISREG(statbuf.st_mode)) { + int ctrl_fd = open_file("/dev/loop-control"); + if (ctrl_fd < 0) + err(1, "open(/dev/loop-control)"); + struct loop_context ctx = { .fd = ctrl_fd }; + int loop_fd = setup_loop(&ctx, fd, 0, (uint64_t)statbuf.st_size, writable); + if (loop_fd < 0) + err(1, "loop device setup failed"); + if (dup3(loop_fd, fd, O_CLOEXEC) != fd) + err(1, "dup3(%d, %d, O_CLOEXEC)", loop_fd, fd); + if (close(loop_fd)) + err(1, "close(%d)", loop_fd); + if (close(ctx.fd)) + err(1, "close(%d)", ctx.fd); + if (fstat(fd, &statbuf)) + err(1, "fstat"); + } else { + errx(1, "Path %s is neither a directory nor regular file", path); + } +skip: + *dev = statbuf.st_rdev; +#ifndef BLKGETDISKSEQ +#define BLKGETDISKSEQ _IOR(0x12,128,__u64) +#else + static_assert(BLKGETDISKSEQ == _IOR(0x12,128,__u64), + "wrong BLKGETDISKSEQ definition?"); +#endif + if (ioctl(fd, BLKGETDISKSEQ, diskseq)) + err(1, "ioctl(%d, BLKGETDISKSEQ, %p)", fd, diskseq); +} + +static void validate_int_start(char **p, unsigned long *out) +{ + char const s = **p; + if (s == '0') { + *out = 0; + (*p)++; + } else if (s >= '1' && s <= '9') { + errno = 0; + *out = strtoul(*p, p, 10); + if (errno) + err(1, "strtoul()"); + } else { + errx(1, "Bad char '%c' in XenStore path %s", s, *p); + } +} + +static void redirect_stderr(void) +{ + int const redirect_fd = open("/var/log/xen/xen-hotplug.log", O_RDWR|O_NOCTTY|O_CLOEXEC|O_APPEND|O_CREAT, 0640); + if (redirect_fd < 0) { + if (errno == ENOENT || errno == EACCES) + return; + err(1, "open()"); + } + if (dup2(redirect_fd, 2) != 2) + err(1, "dup2(%d, 2)", redirect_fd); + if (close(redirect_fd)) + err(1, "close(%d)", redirect_fd); +} + +int main(int argc, char **argv) +{ + bool permissive = false; + redirect_stderr(); +#define XENBUS_PATH_PREFIX "XENBUS_PATH=" +#define BACKEND_VBD "backend/vbd/" +#define ARGV2_PREFIX (XENBUS_PATH_PREFIX BACKEND_VBD) + + const char *xs_path_raw = getenv("XENBUS_PATH"); + + if (argc < 2 || argc > 3) + errx(1, "Usage: [add|remove] [XENBUS_PATH=backend/vbd/REMOTE_DOMAIN/ID] (got %d arguments, expected 2 or 3)", argc); + + const char *last_slash = strrchr(argv[0], '/'); + last_slash = last_slash ? last_slash + 1 : argv[0]; + + if (strcmp(last_slash, "block") == 0) + permissive = true; + + if (strcmp(argv[1], "add") == 0) { + if (argc >= 3) { + if (strncmp(argv[2], ARGV2_PREFIX, sizeof(ARGV2_PREFIX) - 1)) + errx(1, "Second argument must begin with XENBUS_PATH=backend/vbd/"); + const char *const new_path = argv[2] + (sizeof(XENBUS_PATH_PREFIX) - 1); + + if ((xs_path_raw != NULL) && (strcmp(xs_path_raw, new_path) != 0)) + errx(1, "XENBUS_PATH was passed both on the command line and in" + " the environment, but the values were different"); + + xs_path_raw = new_path; + } else if (xs_path_raw == NULL) { + errx(1, "Xenstore path required when adding"); + } else if (strncmp(xs_path_raw, BACKEND_VBD, sizeof(BACKEND_VBD) - 1)) { + errx(1, "Bad Xenstore path %s", xs_path_raw); + } + } else if (strcmp(argv[1], "remove") == 0) + exit(0); + else + errx(1, "Bad command (expected \"add\" or \"remove\")"); + + const char *const xs_path = xs_path_raw; + size_t xs_path_len; + + { + /* strtoul() is not const-correct, sorry... */ + char *xs_path_extra = (char *)(xs_path + (sizeof(BACKEND_VBD) - 1)); + unsigned long peer_domid, tmp; + validate_int_start(&xs_path_extra, &peer_domid); + if (*xs_path_extra++ != '/') + errx(1, "Peer domain ID %lu not followed by '/'", peer_domid); + validate_int_start(&xs_path_extra, &tmp); + if (*xs_path_extra) + errx(1, "Junk after XenStore device ID %lu", tmp); + if (peer_domid >= DOMID_FIRST_RESERVED) + errx(1, "Peer domain ID %lu too large (limit %d)", peer_domid, DOMID_FIRST_RESERVED); + xs_path_len = (size_t)(xs_path_extra - xs_path); + } + + struct xs_handle *const h = xs_open(0); + if (!h) + err(1, "Cannot connect to XenStore"); + + size_t const buffer_size = xs_path_len + sizeof("/physical-device-path"); + char *const xenstore_path_buffer = malloc(buffer_size); + if (!xenstore_path_buffer) + err(1, "malloc()"); + memcpy(xenstore_path_buffer, xs_path, xs_path_len); + xenstore_path_buffer[xs_path_len] = '/'; + /* Buffer to copy extra data into */ + char *const extra_path = xenstore_path_buffer + xs_path_len + 1; + unsigned int len, path_len; + + strcpy(extra_path, "params"); + char *data = xs_read(h, 0, xenstore_path_buffer, &path_len); + if (data == NULL) + err(1, "Cannot obtain parameters from XenStore path %s", xenstore_path_buffer); + + if (strlen(data) != path_len) + errx(1, "NUL in parameters"); + + if (data[0] != '/') + errx(1, "Parameters not an absolute path"); + + unsigned int writable; + + { + strcpy(extra_path, "mode"); + char *rw = xs_read(h, 0, xenstore_path_buffer, &len); + if (rw == NULL) { + if (errno != ENOENT) + err(1, "xs_read(%s)", xenstore_path_buffer); + writable = false; + } else { + switch (rw[0]) { + case 'r': + writable = false; + break; + case 'w': + writable = true; + break; + default: + len = 0; + break; + } + if (len != 1) + errx(1, "Bad data in XenStore key %s: expected 'r' or 'w'", xenstore_path_buffer); + free(rw); + } + } + + int fd; + dev_t dev; + uint64_t diskseq; + + if ((fd = open(data, O_RDONLY | O_NOCTTY | O_CLOEXEC | O_NONBLOCK)) < 0) + err(1, "open(%s)", data); + char phys_dev[18], hex_diskseq[17]; + + process_blk_dev(fd, data, writable, &dev, &diskseq, permissive); + unsigned const int l = + (unsigned)snprintf(phys_dev, sizeof phys_dev, "%lx:%lx", + (unsigned long)major(dev), (unsigned long)minor(dev)); + + if (l >= sizeof(phys_dev)) + err(1, "snprintf"); + if (snprintf(hex_diskseq, sizeof(hex_diskseq), "%016llx", (unsigned long long)diskseq) != 16) + err(1, "snprintf"); + + const char *watch_token = "state"; + strcpy(extra_path, watch_token); + + if (!xs_watch(h, xenstore_path_buffer, watch_token)) + err(1, "Cannot setup XenStore watch on %s", xenstore_path_buffer); + + for (;;) { + xs_transaction_t t = xs_transaction_start(h); + if (!t) + err(1, "Cannot start XenStore transaction"); + + strcpy(extra_path, "physical-device"); + if (!xs_write(h, t, xenstore_path_buffer, phys_dev, l)) + err(1, "xs_write(\"%s\", \"%s\")", xenstore_path_buffer, phys_dev); + + strcpy(extra_path, "physical-device-path"); + if (!xs_write(h, t, xenstore_path_buffer, data, path_len)) + err(1, "xs_write(\"%s\", \"%s\")", xenstore_path_buffer, data); + + strcpy(extra_path, "diskseq"); + if (!xs_write(h, t, xenstore_path_buffer, hex_diskseq, 16)) + err(1, "xs_write(\"%s\", \"%s\")", xenstore_path_buffer, hex_diskseq); + + if (xs_transaction_end(h, t, false)) + break; + + if (errno != EAGAIN) + err(1, "xs_transaction_end"); + } + + unsigned int num; + char **watch_res = xs_read_watch(h, &num); + if (!watch_res) + err(1, "xs_read_watch"); + + free(watch_res); + xs_close(h); +} diff --git a/rpm_spec/qubes-utils.spec.in b/rpm_spec/qubes-utils.spec.in index 0093f778..ec9a4bd7 100644 --- a/rpm_spec/qubes-utils.spec.in +++ b/rpm_spec/qubes-utils.spec.in @@ -123,6 +123,7 @@ rm -rf $RPM_BUILD_ROOT %{_sbindir}/meminfo-writer %{_unitdir}/qubes-meminfo-writer.service %{_unitdir}/qubes-meminfo-writer-dom0.service +/etc/xen/scripts/qubes-block %files -n python%{python3_pkgversion}-qubesimgconverter %{python3_sitelib}/qubesimgconverter/__init__.py