Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions debian/qubes-utils.install
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
usr/bin/meminfo-writer
lib/systemd/system/qubes-meminfo-writer.service
lib/systemd/system/qubes-block-mount-watcher.service
usr/lib/qubes/*
usr/lib/udev/*
usr/lib/tmpfiles.d/xen-devices-qubes.conf
etc/xen/scripts/qubes-block
etc/qubes-rpc/qubes.RefreshBlockDevices
19 changes: 19 additions & 0 deletions rpm_spec/qubes-utils.spec.in
Original file line number Diff line number Diff line change
Expand Up @@ -115,14 +115,29 @@ make install install-selinux DESTDIR=%{buildroot} PYTHON=%{__python3} NO_REBUILD
%systemd_post qubes-meminfo-writer-dom0.service
# VM
%systemd_post qubes-meminfo-writer.service
%systemd_post qubes-block-mount-watcher.service

# Apply the preset on upgrade
if [ $1 -gt 1 ] && \
[ ! -e %{_localstatedir}/lib/rpm-state/qubes-utils-mount-watcher ]; then
mkdir -p %{_localstatedir}/lib/rpm-state
touch %{_localstatedir}/lib/rpm-state/qubes-utils-mount-watcher
systemctl preset qubes-block-mount-watcher.service >/dev/null 2>&1 || :
fi
if systemctl is-enabled --quiet qubes-block-mount-watcher.service \
>/dev/null 2>&1; then
systemctl start qubes-block-mount-watcher.service >/dev/null 2>&1 || :
fi

%preun
%systemd_preun qubes-meminfo-writer-dom0.service
%systemd_preun qubes-meminfo-writer.service
%systemd_preun qubes-block-mount-watcher.service

%postun
%systemd_postun_with_restart qubes-meminfo-writer-dom0.service
%systemd_postun_with_restart qubes-meminfo-writer.service
%systemd_postun_with_restart qubes-block-mount-watcher.service

%post libs -p /sbin/ldconfig
%postun libs -p /sbin/ldconfig
Expand All @@ -136,11 +151,15 @@ rm -rf $RPM_BUILD_ROOT
%_tmpfilesdir/xen-devices-qubes.conf
%dir %{_prefix}/lib/qubes
%{_prefix}/lib/qubes/udev-*
%{_prefix}/lib/qubes/qubes-block-mount-watcher
%{_bindir}/meminfo-writer
%{_unitdir}/qubes-meminfo-writer.service
%{_unitdir}/qubes-meminfo-writer-dom0.service
%{_unitdir}/qubes-block-mount-watcher.service
%{_prefix}/lib/systemd/system-preset/75-qubes-block-mount-watcher.preset
%dir %_includedir/qubes
/etc/xen/scripts/qubes-block
/etc/qubes-rpc/qubes.RefreshBlockDevices

%files -n python%{python3_pkgversion}-qubesimgconverter
%dir %{python3_sitelib}/qubesimgconverter
Expand Down
19 changes: 19 additions & 0 deletions udev/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ install:
mkdir -p $(DESTDIR)$(SYSLIBDIR)/udev/rules.d
cp udev-qubes-block.rules $(DESTDIR)$(SYSLIBDIR)/udev/rules.d/99-qubes-block.rules
cp udev-qubes-usb.rules $(DESTDIR)$(SYSLIBDIR)/udev/rules.d/99-qubes-usb.rules
cp udev-qubes-vbd.rules $(DESTDIR)$(SYSLIBDIR)/udev/rules.d/99-qubes-vbd.rules
cp udev-qubes-misc.rules $(DESTDIR)$(SYSLIBDIR)/udev/rules.d/99-qubes-misc.rules
cp udev-qubes-dmroot.rules $(DESTDIR)$(SYSLIBDIR)/udev/rules.d/90-qubes-dmroot.rules
mkdir -p $(DESTDIR)$(SYSLIBDIR)/tmpfiles.d
Expand All @@ -14,3 +15,21 @@ install:
cp udev-block-remove $(DESTDIR)$(SCRIPTSDIR)
cp udev-usb-add-change $(DESTDIR)$(SCRIPTSDIR)
cp udev-usb-remove $(DESTDIR)$(SCRIPTSDIR)
cp qubes-block-mount-watcher $(DESTDIR)$(SCRIPTSDIR)

ifeq (1,${DEBIANBUILD})
mkdir -p $(DESTDIR)/lib/systemd/system
install -m 0644 qubes-block-mount-watcher.service \
$(DESTDIR)/lib/systemd/system/
else
mkdir -p $(DESTDIR)/usr/lib/systemd/system
install -m 0644 qubes-block-mount-watcher.service \
$(DESTDIR)/usr/lib/systemd/system/

mkdir -p $(DESTDIR)/usr/lib/systemd/system-preset
install -m 0644 qubes-block-mount-watcher.preset \
$(DESTDIR)/usr/lib/systemd/system-preset/75-qubes-block-mount-watcher.preset
endif

mkdir -p $(DESTDIR)/etc/qubes-rpc
cp qubes.RefreshBlockDevices $(DESTDIR)/etc/qubes-rpc
75 changes: 75 additions & 0 deletions udev/qubes-block-mount-watcher
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#!/usr/bin/python3
#
# The Qubes OS Project, https://www.qubes-os.org
#
# Copyright (C) 2026 Piotr Bartman-Szwarc <prbartman@invisiblethingslab.com>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
# USA.

"""
Re-evaluate exported block devices when something is (un)mounted.

Mounting a filesystem does not generate a block uevent.
Note: Swap is not covered.
"""

import os
import select
import subprocess

MOUNTINFO = "/proc/self/mountinfo"


def get_mounted(mountinfo) -> set[str]:
"""Returns numbers ("major:minor") of currently mounted devices."""
mountinfo.seek(0)
result = set()
for line in mountinfo.read().splitlines():
fields = line.split()
# 3rd field is the number
if len(fields) > 2:
result.add(fields[2])
return result


def refresh(device: str):
"""Re-run the udev rules for a device."""
syspath = "/sys/dev/block/" + device
if not os.path.isdir(syspath):
# virtual
return
subprocess.call(["udevadm", "trigger", "--action=change", syspath])


def main():
with open(MOUNTINFO, "r", encoding="ascii", errors="replace") as mountinfo:
poller = select.poll()
poller.register(mountinfo, select.POLLPRI | select.POLLERR)

previous = get_mounted(mountinfo)
for dev in previous:
refresh(dev)

while True:
poller.poll()
current = get_mounted(mountinfo)
for dev in current.symmetric_difference(previous):
refresh(dev)
previous = current


if __name__ == "__main__":
main()
1 change: 1 addition & 0 deletions udev/qubes-block-mount-watcher.preset
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
enable qubes-block-mount-watcher.service
16 changes: 16 additions & 0 deletions udev/qubes-block-mount-watcher.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[Unit]
Description=Block device mount watcher
After=qubes-db.service qubes-db-dom0.service systemd-udevd.service
Wants=qubes-db.service

[Service]
Type=simple
# Shows to dom0 that this backend supports the `used` markers.
ExecStartPre=/usr/bin/qubesdb-write /qubes-device-usage-tracking True
ExecStopPost=-/usr/bin/qubesdb-rm /qubes-device-usage-tracking
ExecStart=/usr/lib/qubes/qubes-block-mount-watcher
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
33 changes: 33 additions & 0 deletions udev/qubes.RefreshBlockDevices
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#!/bin/sh

# Refresh exported devices, by re-triggering udev change events.
# Called after exporting a device: the partitions of an exported block device
# are removed from QubesDB.
#
# 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"

# Wait (3s max) for vbd to appear; attach is async and no uevent is sent
# to fix an early recompute. On timeout recompute anyway: attach likely
# failed; if not, partitions stay listed until the next block event.
dev_hex=$(stat -c %t:%T "/dev/$name" 2>/dev/null)
i=0
while [ "$i" -lt 30 ] && [ -n "$dev_hex" ] && [ "$dev_hex" != 0:0 ]; do
grep -qxF "$dev_hex" \
/sys/bus/xen-backend/drivers/vbd/vbd-*/physical_device 2>/dev/null \
&& break
sleep 0.1
i=$((i + 1))
done

for dev in "/dev/$name" "/dev/$name"?*; do
[ -b "$dev" ] && udevadm trigger --action=change "$dev"
done

exec udevadm settle --timeout=5
99 changes: 89 additions & 10 deletions udev/udev-block-add-change
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/bin/bash

shopt -s nullglob
shopt -s nullglob globstar


export LC_CTYPE=en_US.UTF-8
Expand All @@ -10,6 +10,27 @@ 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_set_usb_used can use it.
USB_PARENT=""
USB_PARENT_SYSPATH=""
if echo "$DEVPATH" | grep -q '/host'; then
# DEVPATH for a USB-connected block device ends with:
# .../<usb-dev>/<usb-iface>/host.../...
# Walk up /sys to find the USB device directory name matching bus-port
# format (e.g. "1-1", not "1-1:1.0").
walk="/sys$DEVPATH"
while [ -n "$walk" ] && [ "$walk" != "/" ] && [ "$walk" != "/sys" ]; do
walk=${walk%/*}
candidate=${walk##*/}
if [[ "$candidate" =~ ^[0-9]+-[0-9]+(\.[0-9]+)*$ ]]; then
USB_PARENT_SYSPATH=$walk
USB_PARENT=$candidate
break
fi
done
fi

xs_remove() {
if is_attached /sys$DEVPATH; then
return 0
Expand All @@ -21,6 +42,21 @@ xs_remove() {
fi
}

# Mark only the USB ancestor (if any) as used. 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_set_usb_used() {
if [ -n "$USB_PARENT" ]; then
local usb_safe="${USB_PARENT//./_}"
# check if USB device is exposed
if qubesdb-read -q "/qubes-usb-devices/${usb_safe}/desc" >/dev/null
then
qubesdb-write "/qubes-usb-devices/${usb_safe}/used" "$1"
qubesdb-write /qubes-usb-devices ''
fi
fi
}

is_used() {
local sys_devpath=$1
local devname=$(grep ^DEVNAME= $sys_devpath/uevent | cut -f 2 -d =)
Expand Down Expand Up @@ -49,6 +85,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 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
# The interface-to-host depth varies by driver
for blockdev in "$USB_PARENT_SYSPATH"/**/block/*; do
if is_used "$blockdev"; then
return 0
fi
for part in "$blockdev/${blockdev##*/}"*; do
if [ -d "$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
Expand Down Expand Up @@ -95,21 +151,23 @@ 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 this script is not run by itself when local use starts or ends.
# See `qubes-block-mount-watcher`.

# device itself is already used
if is_used /sys$DEVPATH; then
xs_remove
xs_set_usb_used True
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)
USED=""
for part in /sys$DEVPATH/$NAME*; do
if [ -d $part ]; then
if is_used $part || is_attached $part; then
xs_remove
exit 0
fi
if [ -d $part ] && is_used $part; then
USED=1
break
fi
done

Expand Down Expand Up @@ -166,8 +224,14 @@ 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

if [ -n "$USED" ]; then
USED_VALUE=True
else
USED_VALUE=False
fi

# The last one is meant to trigger watches
Expand All @@ -176,7 +240,22 @@ qubesdb-write \
"$QDB_KEY/size" "$SIZE" \
"$QDB_KEY/mode" "$MODE" \
"$QDB_KEY/parent" "$parent" \
"$QDB_KEY/used" "$USED_VALUE" \
/qubes-block-devices ''

# If this device or one of its children is in use: keep the parent used.
if [ -n "$USED" ]; then
xs_set_usb_used True
exit 0
fi

# This device is free, but a sibling device may still be in use
# in that case keep the USB ancestor used.
if usb_parent_still_in_use; then
xs_set_usb_used True
else
xs_set_usb_used False
fi

# Make sure that block backend is loaded
/sbin/modprobe xen-blkback 2> /dev/null || /sbin/modprobe blkbk
10 changes: 10 additions & 0 deletions udev/udev-qubes-vbd.rules
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Re-evaluate exported block devices when a xen vbd disappears.
#
# Not on the udev watch event: it fires when blkback closes the device,
# still before the vbd is unregistered, so a refresh there would see it as
# attached!
# By now physical_device is gone too, so we cannot tell which device it was,
# hence we check all; the block rules drop the irrelevant ones early.
# This works for `detach`; `attach` is handled by `qubes.RefreshBlockDevices`.
SUBSYSTEM=="xen-backend", KERNEL=="vbd-*", ACTION=="remove", \
RUN+="/usr/bin/udevadm trigger --action=change --subsystem-match=block"
4 changes: 4 additions & 0 deletions udev/udev-usb-add-change
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,15 @@ fi

QDB_KEY="/qubes-usb-devices/$XSNAME"

USED=`qubesdb-read -q "$QDB_KEY/used" 2>/dev/null`
[ -n "$USED" ] || USED=False

# The last one is meant to trigger watches
qubesdb-write \
"$QDB_KEY/desc" "$DESC" \
"$QDB_KEY/usb-ver" "$VERSION" \
"$QDB_KEY/interfaces" "$ID_USB_INTERFACES" \
"$QDB_KEY/used" "$USED" \
/qubes-usb-devices ''

# Make sure PVUSB backend driver is loaded.
Expand Down