diff --git a/debian/qubes-utils.install b/debian/qubes-utils.install index 29846073..54b15aa6 100644 --- a/debian/qubes-utils.install +++ b/debian/qubes-utils.install @@ -4,3 +4,4 @@ usr/lib/qubes/* usr/lib/udev/* usr/lib/tmpfiles.d/xen-devices-qubes.conf etc/xen/scripts/qubes-block +etc/qubes-rpc/qubes.RefreshBlockDevices diff --git a/rpm_spec/qubes-utils.spec.in b/rpm_spec/qubes-utils.spec.in index a6eb2134..70dae4fd 100644 --- a/rpm_spec/qubes-utils.spec.in +++ b/rpm_spec/qubes-utils.spec.in @@ -141,6 +141,7 @@ rm -rf $RPM_BUILD_ROOT %{_unitdir}/qubes-meminfo-writer-dom0.service %dir %_includedir/qubes /etc/xen/scripts/qubes-block +/etc/qubes-rpc/qubes.RefreshBlockDevices %files -n python%{python3_pkgversion}-qubesimgconverter %dir %{python3_sitelib}/qubesimgconverter diff --git a/udev/Makefile b/udev/Makefile index ac64a84e..2475cdaf 100644 --- a/udev/Makefile +++ b/udev/Makefile @@ -14,3 +14,6 @@ install: cp udev-block-remove $(DESTDIR)$(SCRIPTSDIR) cp udev-usb-add-change $(DESTDIR)$(SCRIPTSDIR) cp udev-usb-remove $(DESTDIR)$(SCRIPTSDIR) + + mkdir -p $(DESTDIR)/etc/qubes-rpc + cp qubes.RefreshBlockDevices $(DESTDIR)/etc/qubes-rpc diff --git a/udev/qubes.RefreshBlockDevices b/udev/qubes.RefreshBlockDevices new file mode 100755 index 00000000..b8774515 --- /dev/null +++ b/udev/qubes.RefreshBlockDevices @@ -0,0 +1,19 @@ +#!/bin/sh + +# Refresh `busy` attribute for a block device subtree by re-triggering udev +# change events. Called after detaching a block device. +# +# stdin: device node path relative to /dev (e.g. "sda" or "mapper/dmroot"). + +read -r untrusted_name + +case "$untrusted_name" in + ""|-*|*[!a-zA-Z0-9/_-]*) exit 1 ;; +esac +name="$untrusted_name" + +for dev in "/dev/$name" "/dev/$name"?*; do + [ -b "$dev" ] && udevadm trigger --action=change "$dev" +done + +exec udevadm settle --timeout=5 diff --git a/udev/udev-block-add-change b/udev/udev-block-add-change index 7add05c0..cc671a70 100755 --- a/udev/udev-block-add-change +++ b/udev/udev-block-add-change @@ -10,6 +10,19 @@ SIZE=$[ $(cat /sys/$DEVPATH/size) * 512 ] MODE=w QDB_KEY="/qubes-block-devices/$NAME" +# USB ancestor port name (e.g. "1-1") if this block device is exposed via USB; +# empty otherwise. Computed early so xs_mark_busy can use it. +USB_PARENT="" +USB_PARENT_SYSPATH="" +if echo "$DEVPATH" | grep -q '/host'; then + # DEVPATH for a USB-connected block device ends with: + # ...///host.../... + # Strip from /host onward to get the USB interface path, then go up one + # level to get the USB device directory name (e.g. "1-1", not "1-1:1.0"). + USB_PARENT_SYSPATH=$(dirname "$(sed 's|/host.*$||' <<< "/sys$DEVPATH")") + USB_PARENT=$(basename "$USB_PARENT_SYSPATH") +fi + xs_remove() { if is_attached /sys$DEVPATH; then return 0 @@ -21,6 +34,35 @@ xs_remove() { fi } +# Mark this device as present-but-unavailable (busy) in QubesDB because one of +# its child devices is currently in use. Keeps all existing device properties +# intact so the device remains visible. +# We only care about this block device and its USB ancestor (if it exists). +# A parent block device can handle itself by checking the status of its children +# (see below). +xs_mark_busy() { + if qubesdb-read -q "$QDB_KEY/desc" >/dev/null; then + qubesdb-write "$QDB_KEY/busy" "True" + qubesdb-write /qubes-block-devices '' + fi + # Also marks the USB ancestor (if any) unavailable for USB passthrough. + xs_mark_usb_busy +} + +# Mark only the USB ancestor (if any) as busy. Needed on its own when the +# device itself is removed from QDB (used locally, so not manageable from +# dom0) but the whole USB device still must not be attachable elsewhere. +xs_mark_usb_busy() { + if [ -n "$USB_PARENT" ]; then + local usb_safe="${USB_PARENT//./_}" + if qubesdb-read -q "/qubes-usb-devices/${usb_safe}/desc" >/dev/null + then + qubesdb-write "/qubes-usb-devices/${usb_safe}/busy" "True" + qubesdb-write /qubes-usb-devices '' + fi + fi +} + is_used() { local sys_devpath=$1 local devname=$(grep ^DEVNAME= $sys_devpath/uevent | cut -f 2 -d =) @@ -49,6 +91,26 @@ refresh_another() { env -i PATH=$PATH $launch_env $0 } +# If any block device exposed by the USB ancestor (including sibling block +# devices and all their partitions) is currently attached or used locally. +# Used to decide if the USB ancestor can be set free. +usb_parent_still_in_use() { + [ -n "$USB_PARENT_SYSPATH" ] || return 1 + local blockdev part + for blockdev in "$USB_PARENT_SYSPATH"/*/host*/target*/*/block/*; do + if is_attached "$blockdev" || is_used "$blockdev"; then + return 0 + fi + for part in "$blockdev/$(basename "$blockdev")"*; do + if [ -d "$part" ] && { is_attached "$part" || is_used "$part"; } + then + return 0 + fi + done + done + return 1 +} + is_attached() { dev_hex=$(stat -c %t:%T /dev/$(basename $1)) if [ -z "$dev_hex" -o "$dev_hex" = "0:0" ]; then @@ -95,23 +157,37 @@ if [ -z "$QUBES_EXPORT_BLOCK_DEVICE" ] && [ "$DM_UDEV_DISABLE_DISK_RULES_FLAG" = exit 0 fi +# Note: mounting or unmounting a filesystem does not generate a block uevent, +# so the state derived from is_used is refreshed only on partition rescans, +# device change events or an explicit `udevadm trigger`. +# Setting/clearing of the busyness may lag behind the actual state. +# The consequences are moderate: it's possible to +# (i) forcibly remove a mounted device, or +# (ii) get an error that the device is being used when it's free, in which +# case we need to manually trigger a udev event or reconnect the device. +# For the future: consider using a watcher. + # device itself is already used if is_used /sys$DEVPATH; then xs_remove + xs_mark_usb_busy exit 0 fi # or one of its partitions is used # or already attached (prevent attaching both device and its partition(s) at # the same time) -for part in /sys$DEVPATH/$NAME*; do - if [ -d $part ]; then - if is_used $part || is_attached $part; then - xs_remove - exit 0 +BUSY="" +if is_attached /sys$DEVPATH; then + BUSY=1 +else + for part in /sys$DEVPATH/$NAME*; do + if [ -d $part ] && { is_used $part || is_attached $part; }; then + BUSY=1 + break fi - fi -done + done +fi # or "empty" loop device if [ "$MAJOR" -eq 7 -a ! -d /sys/$DEVPATH/loop ]; then @@ -166,8 +242,8 @@ fi if [ -f /sys$DEVPATH/partition ]; then parent=$(basename "$(dirname "$DEVPATH")") -elif echo "$DEVPATH" | grep -q '/host'; then - parent=$(basename "$(sed 's|/host.*$||' <<< "$DEVPATH")") +elif [ -n "$USB_PARENT" ]; then + parent="$USB_PARENT" fi # The last one is meant to trigger watches @@ -178,5 +254,27 @@ qubesdb-write \ "$QDB_KEY/parent" "$parent" \ /qubes-block-devices '' +# If this device or one of its children is in use: keep the parent busy. +if [ -n "$BUSY" ]; then + xs_mark_busy + exit 0 +fi + +# Device is now free -> clear busy marker for this device +# and optionally for its USB ancestor (if any). +qubesdb-rm "$QDB_KEY/busy" 2>/dev/null || true +if [ -n "$USB_PARENT" ]; then + usb_safe="${USB_PARENT//./_}" + # This device is free, but a sibling device may still be in use + # in that case keep the USB parent's busy marker set. + if usb_parent_still_in_use; then + qubesdb-write "/qubes-usb-devices/${usb_safe}/busy" "True" + else + qubesdb-rm "/qubes-usb-devices/${usb_safe}/busy" 2>/dev/null || true + fi + # trigger watches for USB ancestor as well + qubesdb-write /qubes-usb-devices '' +fi + # Make sure that block backend is loaded /sbin/modprobe xen-blkback 2> /dev/null || /sbin/modprobe blkbk