Changing NetVM of Windows qube to None can sometimes keep its network access
Qubes OS release
Qubes OS 4.3
Brief summary
Changing a qube's NetVM to None does not actually prohibit it from accessing the network under certain conditions.
Steps to reproduce
Disclaimer: these are the conditions under which I have been able to reproduce this bug. They are probably not the only conditions under which this appears.
- Create a Windows 11 SVM HVM without QWT installed
- Mark it as a disposable template and create a named disposable from it, with network access
- Boot that named disposable
- Configure a static IP address, netmask (/8), gateway and DNS inside the qube
The qube can now access the network
- Go to the Qube manager and change the NetVM for the named disposable to None
- Try accessing the internet in the qube
Expected behavior
Qube cannot access the internet.
Actual behavior
Qube can access the internet.
Additional information
I believe that this is a security issue. I am submitting this on Github, as it's unlikely to be exploited actively. It's more likely to cause accidental problems to users that would believe that the internet access is cut off to the qube while it is not. Case in point, this was exactly me, about to do some malware analysis on a qube I hoped was air-gapped.
Disabling and reenabling the network adapter in Windows does not "help", the Qube still has network access.
I don't have windows, can you repeat the same steps and watch the qubesd unit:
sudo journalctl -fu qubesd | grep -v pam_unix
Attaching and detaching network is not logged, but if there is an operation failure, it will be logged.
Nothing interesting as far as the qubesd unit goes:
user@dom0 ~ % sudo journalctl -fu qubesd | grep -v pam_unix
Dec 08 09:25:03 dom0 qubesd[3442]: INFO: Renamed file: '/var/lib/qubes/appvms/disp-windows-11-noqwt/private-dirty.img~m_fbvf6t' -> '/var/lib/qubes/appvms/disp-windows-11-noqwt/private-dirty.img'
Dec 08 09:25:03 dom0 qubesd[3442]: INFO: Reflinked file: '/var/lib/qubes/appvms/disp-windows-11-noqwt/root.img' -> '/var/lib/qubes/appvms/disp-windows-11-noqwt/root-dirty.img~bbp8hd4f'
Dec 08 09:25:04 dom0 qubesd[3442]: INFO: Renamed file: '/var/lib/qubes/appvms/disp-windows-11-noqwt/root-dirty.img~bbp8hd4f' -> '/var/lib/qubes/appvms/disp-windows-11-noqwt/root-dirty.img'
Dec 08 09:25:05 dom0 qubesd[3442]: INFO: vm.disp-windows-11-noqwt: Setting Qubes DB info for the qube
Dec 08 09:25:05 dom0 qubesd[3442]: INFO: vm.disp-windows-11-noqwt: Starting Qubes DB
Dec 08 09:25:05 dom0 qubesd[3442]: INFO: vm.disp-windows-11-noqwt: Activating qube
Plain old sudo journalctl -f also does not show anything new when detaching the interface. Interestingly, attaching the qube to a net qube is almost instant, detaching makes the dialog window hang for a couple of seconds until it goes through.
detaching makes the dialog window hang for a couple of seconds until it goes through.
Libvirt detachDevice has a 10 seconds timeout. Can't help further, will leave this for the Windows developer.
FWIW I cannot duplicate with OpenBSD, TinyCore, or Android based qubes. (Or, of course, with stock qubes.)
I suppose that you have to reboot the Windows VM. Probably that stays just stays sitting on its networks adapter and does not change its internal state when the Qubes environment has changed.
I suppose that you have to reboot the Windows VM. Probably that stays just stays sitting on its networks adapter and does not change its internal state when the Qubes environment has changed.
That would probably work, but: a) That would be counter-productive for this use-case as it's a dispvm b) QubesOS should most probably forcefully disconnect adapters from VMs that do not cooperate
Disclaimer: I am not well-versed in the inner workings of the Qubes and libvirt admin APIs, but I've tried to diagnose the issue to the best of my ability.
I believe the issue may lie here:
https://github.com/QubesOS/qubes-core-admin/blob/36f3474b9853f317211823f0a92e53d0cb1719e9/qubes/vm/mix/net.py#L463
This method does not seem to return errors on failure, which makes the code proceed and tell the user "All is good, NetVM is now N/A".
What I see to be done here is to:
- Add error handling
- Consider adding the
VIR_DOMAIN_DEVICE_MODIFY_FORCEto thedetachDevicecall if it's possible to do that. Either immediately or as a "second try" if the first call does not work.
OpenQA test for Windows is not ready. It would be better if you tried and
reported results. Restart qubesd after making the change.
On Sat, Dec 13, 2025, 2:05 PM Atrate @.***> wrote:
Atrate left a comment (QubesOS/qubes-issues#10459) https://github.com/QubesOS/qubes-issues/issues/10459#issuecomment-3649394836
Disclaimer: I am not well-versed in the inner workings of the Qubes and libvirt admin APIs, but I've tried to diagnose the issue to the best of my ability.
I believe the issue may lie here:
https://github.com/QubesOS/qubes-core-admin/blob/36f3474b9853f317211823f0a92e53d0cb1719e9/qubes/vm/mix/net.py#L463
This method does not seem to return errors on failure, which makes the code proceed and tell the user "All is good, NetVM is now N/A".
What I see to be done here is to:
- Add error handling
- Consider adding the VIR_DOMAIN_DEVICE_MODIFY_FORCE https://libvirt.org/html/libvirt-libvirt-domain.html#virDomainDeviceModifyFlags to the detachDevice call if it's possible to do that. Either immediately or as a "second try" if the first call does not work.
— Reply to this email directly, view it on GitHub https://github.com/QubesOS/qubes-issues/issues/10459#issuecomment-3649394836, or unsubscribe https://github.com/notifications/unsubscribe-auth/BCE2O4N27WEHGO5N6KGEMS34BQFI3AVCNFSM6AAAAACOJ67VGGVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZTMNBZGM4TIOBTGY . You are receiving this because you commented.Message ID: @.***>
This method does not seem to return errors on failure, which makes the code proceed and tell the user "All is good, NetVM is now N/A".
Please note Python is not C. Python has exceptions :) If method fails, it raises an exception (which then qvm-prefs reports in a bit confusing way, something like "there is no netvm property"...).
The issue is probably somewhere deeper in libvirt or libxl about handling QEMU-emulated device for Windows (in contrast to PV network device that Linux uses).
Update: this issue also occurs in qubes with QWT installed, just managed to reproduce it.
OpenQA test for Windows is not ready. It would be better if you tried and reported results. Restart
qubesdafter making the change. …
I tried it with this patch (replacing L463):
self.libvirt_domain.detachDeviceFlags(
lxml.etree.tostring(eth).decode(),
libvirt.VIR_DOMAIN_DEVICE_MODIFY_LIVE
| libvirt.VIR_DOMAIN_DEVICE_MODIFY_FORCE,
)
Unfortunately it seems that something (libxl?) does not support the VIR_DOMAIN_DEVICE_MODIFY_FORCE flag :/.
Dec 13 17:52:49 dom0 virtxend[4983]: unsupported flags (0x4) in function libxlDomainDetachDeviceFlags
Dec 13 17:52:49 dom0 qubesd[756942]: ERROR: unhandled exception while calling src=b'dom0' meth=b'admin.vm.property.Set' dest=b'disp3753' arg=b'netvm' len(untrusted_payload)=0
Dec 13 17:52:49 dom0 qubesd[756942]: Traceback (most recent call last):
Dec 13 17:52:49 dom0 qubesd[756942]: File "/usr/lib/python3.13/site-packages/qubes/api/__init__.py", line 339, in respond
Dec 13 17:52:49 dom0 qubesd[756942]: response = await self.mgmt.execute(
Dec 13 17:52:49 dom0 qubesd[756942]: ^^^^^^^^^^^^^^^^^^^^^^^^
Dec 13 17:52:49 dom0 qubesd[756942]: untrusted_payload=untrusted_payload
Dec 13 17:52:49 dom0 qubesd[756942]: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Dec 13 17:52:49 dom0 qubesd[756942]: )
Dec 13 17:52:49 dom0 qubesd[756942]: ^
Dec 13 17:52:49 dom0 qubesd[756942]: File "/usr/lib/python3.13/site-packages/qubes/api/admin.py", line 299, in vm_property_set
Dec 13 17:52:49 dom0 qubesd[756942]: return self._property_set(
Dec 13 17:52:49 dom0 qubesd[756942]: ~~~~~~~~~~~~~~~~~~^
Dec 13 17:52:49 dom0 qubesd[756942]: self.dest, untrusted_payload=untrusted_payload
Dec 13 17:52:49 dom0 qubesd[756942]: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Dec 13 17:52:49 dom0 qubesd[756942]: )
Dec 13 17:52:49 dom0 qubesd[756942]: ^
Dec 13 17:52:49 dom0 qubesd[756942]: File "/usr/lib/python3.13/site-packages/qubes/api/admin.py", line 318, in _property_set
Dec 13 17:52:49 dom0 qubesd[756942]: setattr(dest, self.arg, newvalue)
Dec 13 17:52:49 dom0 qubesd[756942]: ~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^
Dec 13 17:52:49 dom0 qubesd[756942]: File "/usr/lib/python3.13/site-packages/qubes/vm/__init__.py", line 670, in __set__
Dec 13 17:52:49 dom0 qubesd[756942]: super().__set__(instance, value)
Dec 13 17:52:49 dom0 qubesd[756942]: ~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^
Dec 13 17:52:49 dom0 qubesd[756942]: File "/usr/lib/python3.13/site-packages/qubes/__init__.py", line 281, in __set__
Dec 13 17:52:49 dom0 qubesd[756942]: instance.fire_event(
Dec 13 17:52:49 dom0 qubesd[756942]: ~~~~~~~~~~~~~~~~~~~^
Dec 13 17:52:49 dom0 qubesd[756942]: "property-pre-set:" + self.__name__,
Dec 13 17:52:49 dom0 qubesd[756942]: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Dec 13 17:52:49 dom0 qubesd[756942]: ...<3 lines>...
Dec 13 17:52:49 dom0 qubesd[756942]: oldvalue=oldvalue, # pylint: disable=possibly-used-before-assignment
Dec 13 17:52:49 dom0 qubesd[756942]: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Dec 13 17:52:49 dom0 qubesd[756942]: )
Dec 13 17:52:49 dom0 qubesd[756942]: ^
Dec 13 17:52:49 dom0 qubesd[756942]: File "/usr/lib/python3.13/site-packages/qubes/events.py", line 200, in fire_event
Dec 13 17:52:49 dom0 qubesd[756942]: sync_effects, async_effects = self._fire_event(
Dec 13 17:52:49 dom0 qubesd[756942]: ~~~~~~~~~~~~~~~~^
Dec 13 17:52:49 dom0 qubesd[756942]: event, kwargs, pre_event=pre_event
Dec 13 17:52:49 dom0 qubesd[756942]: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Dec 13 17:52:49 dom0 qubesd[756942]: )
Dec 13 17:52:49 dom0 qubesd[756942]: ^
Dec 13 17:52:49 dom0 qubesd[756942]: File "/usr/lib/python3.13/site-packages/qubes/events.py", line 169, in _fire_event
Dec 13 17:52:49 dom0 qubesd[756942]: effect = func(self, event, **kwargs)
Dec 13 17:52:49 dom0 qubesd[756942]: File "/usr/lib/python3.13/site-packages/qubes/vm/mix/net.py", line 618, in on_property_pre_set_netvm
Dec 13 17:52:49 dom0 qubesd[756942]: self.detach_network()
Dec 13 17:52:49 dom0 qubesd[756942]: ~~~~~~~~~~~~~~~~~~~^^
Dec 13 17:52:49 dom0 qubesd[756942]: File "/usr/lib/python3.13/site-packages/qubes/vm/mix/net.py", line 463, in detach_network
Dec 13 17:52:49 dom0 qubesd[756942]: self.libvirt_domain.detachDeviceFlags(
Dec 13 17:52:49 dom0 qubesd[756942]: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^
Dec 13 17:52:49 dom0 qubesd[756942]: lxml.etree.tostring(eth).decode(),
Dec 13 17:52:49 dom0 qubesd[756942]: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Dec 13 17:52:49 dom0 qubesd[756942]: libvirt.VIR_DOMAIN_DEVICE_MODIFY_LIVE
Dec 13 17:52:49 dom0 qubesd[756942]: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Dec 13 17:52:49 dom0 qubesd[756942]: | libvirt.VIR_DOMAIN_DEVICE_MODIFY_FORCE,
Dec 13 17:52:49 dom0 qubesd[756942]: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Dec 13 17:52:49 dom0 qubesd[756942]: )
Dec 13 17:52:49 dom0 qubesd[756942]: ^
Dec 13 17:52:49 dom0 qubesd[756942]: File "/usr/lib/python3.13/site-packages/qubes/app.py", line 107, in wrapper
Dec 13 17:52:49 dom0 qubesd[756942]: return getattr(self._vm, attrname)(*args, **kwargs)
Dec 13 17:52:49 dom0 qubesd[756942]: ~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^
Dec 13 17:52:49 dom0 qubesd[756942]: File "/usr/lib64/python3.13/site-packages/libvirt.py", line 1570, in detachDeviceFlags
Dec 13 17:52:49 dom0 qubesd[756942]: raise libvirtError('virDomainDetachDeviceFlags() failed')
Dec 13 17:52:49 dom0 qubesd[756942]: libvirt.libvirtError: unsupported flags (0x4) in function libxlDomainDetachDeviceFlags
Most likely issue is missing stubdom and/or QEMU handling in device_remove (or rather, one for the callbacks used down the road). Unfortunately, the code is quite hard to follow due to manual async/callback structure. I'll take a look later...
In the meantime, can you try to reproduce with Linux HVM that has xen_emul_unplug=ide-disks,aux-ide-disks or nopv in kernelopts (not sure which one will work here). The former may result in two network interfaces, and probably you'll need to configure it manually (possibly after manually disabling the other one). Adding module_blacklist=xen-netfront might be also useful for testing.