ref:9cc1e1adc5bae0a2705d2abaf6497a5b4faa3252

fix(linux): open libvirt p2p displays with virDomainOpenGraphics

Found by running Sunshine against a real system libvirt (10.0) as an unprivileged member of the libvirt group: the D-Bus handshake on the socket from virDomainOpenGraphicsFD failed with 'Exhausted all available authentication mechanisms'. QEMU authenticates p2p clients with EXTERNAL, comparing the uid the client claims with the socket's peer credentials, and those belong to whoever created the pair: libvirtd, for virDomainOpenGraphicsFD. Sunshine now creates the pair itself and hands one end to libvirt with virDomainOpenGraphics, which passes it to QEMU with the same add_client. Verified: qemu:///system domain with <graphics type='dbus' p2p='yes'> (QEMU 8.2) streamed to the E2E client with the pattern matched, and qemu:///session bus mode (QEMU 11.1). Docs: libvirt 10.0 rejects <input type='touch'>. Refs #6 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BPNw4PCgkEfhyCjQT19wsb
SHA: 9cc1e1adc5bae0a2705d2abaf6497a5b4faa3252
Author: Cole Christensen <cole.christensen@gmail.com>
Date: 2026-09-13 10:32
Parents: b91a635
8 files changed +59 -36
Type
docs/configuration.md +4 −4
@@ -2240,9 +2240,9 @@
(gamepads still use virtual host devices). An absolute pointer such as `-device virtio-tablet-pci`
works best; with a relative mouse (PS/2) Moonlight's absolute pointer positions only work while the
guest shows a hardware cursor. Horizontal scrolling and text input aren't supported.
Touches reach the VM as touches only when it has `-device virtio-multitouch-pci` (libvirt:
`<input type='touch' bus='virtio'/>`), which Sunshine finds on QEMU's command line or in the domain
XML; otherwise, or when it can't tell, they move and click the mouse. QEMU sends mouse buttons to a
Touches reach the VM as touches only when it has `-device virtio-multitouch-pci` (libvirt releases that
support `<input type='touch' bus='virtio'/>`; libvirt 10.0 doesn't, so pass the device with
`<qemu:commandline>` there), which Sunshine finds on QEMU's command line or in the domain XML; otherwise, or when it can't tell, they move and click the mouse. QEMU sends mouse buttons to a
multi-touch device and drops wheel events, so add it only for touch-first guests. Pens always move
the mouse.
@note{Applies to Linux only.}</td>
@@ -2272,7 +2272,7 @@
<li>`qmp:<socket>`: a QMP socket of a QEMU started with `-display dbus,p2p=on`; Sunshine adds itself
as a display client with `add_client`</li>
<li>`libvirt:<domain>` or `libvirt:<domain>?uri=<uri>`: a libvirt domain with `<graphics type='dbus'>`;
with `p2p='yes'` Sunshine connects through `virDomainOpenGraphicsFD`, otherwise to the domain's bus.
with `p2p='yes'` Sunshine connects through `virDomainOpenGraphics`, otherwise to the domain's bus.
Without `?uri=` the domain is looked up on `qemu:///system`; an empty `?uri=` uses libvirt's
default URI</li>
</ul>
docs/qemu.md +7 −6
@@ -46,9 +46,10 @@
libvirt's `p2p='yes'` (which gives Sunshine only the display) or a bus.
- **libvirt** (`libvirt:`): Sunshine loads `libvirt.so.0` at run time. `libvirt:<domain>` uses the system libvirt
(`qemu:///system`), also when Sunshine runs as an unprivileged user whose own default would be `qemu:///session`;
`?uri=` picks another connection (an empty `?uri=` leaves the choice to libvirt). For a `p2p='yes'` display it calls
`virDomainOpenGraphicsFD`, which does the same `add_client` through libvirt; libvirt labels the socket for the
domain, so it works with SELinux and AppArmor confinement. For a display without `p2p`, Sunshine reads the bus
`?uri=` picks another connection (an empty `?uri=` leaves the choice to libvirt). For a `p2p='yes'` display it gives libvirt
one end of a socket pair with `virDomainOpenGraphics`, which does the same `add_client` through libvirt; libvirt
labels the socket for the domain, so it works with SELinux and AppArmor confinement. (QEMU checks the D-Bus peer's
user id on that socket, so the pair must come from Sunshine, not from libvirtd as with `virDomainOpenGraphicsFD`.) For a display without `p2p`, Sunshine reads the bus
address from the running domain's XML and connects to that bus.
QEMU serves one peer-to-peer (`p2p`) control connection at a time. Another client that attaches the same way, for
@@ -319,6 +320,6 @@
- `no QMP greeting`: another client holds the QMP socket; give Sunshine its own `-qmp` socket.
- `QMP add_client failed ... p2p connections not accepted in bus mode`: QEMU runs `-display dbus` without `p2p=on`.
- `couldn't load libvirt.so.0`: install libvirt's client library (`libvirt0` or `libvirt-libs`).
- `virDomainOpenGraphicsFD ... failed` or an access error: Sunshine's user lacks the libvirt permission above, or the
- `virDomainOpenGraphics ... failed` or an access error: Sunshine's user lacks the libvirt permission above, or the
domain's display has no `p2p='yes'`.
- `domain [...] has no <graphics type='dbus'> device`: the domain XML still has a SPICE or VNC display.
@@ -334,7 +335,7 @@
Sunshine sends absolute positions when QEMU reports an absolute pointer (`IsAbsolute`). A VM with only a PS/2 mouse is
relative: Moonlight positions then only work while the guest shows a hardware cursor. Add a virtio or USB tablet.
QEMU offers native touch on every VM, but only a VM with `-device virtio-multitouch-pci` (libvirt:
`<input type='touch' bus='virtio'/>`) receives the touches. Sunshine looks for that device on QEMU's command line or
QEMU offers native touch on every VM, but only a VM with `-device virtio-multitouch-pci` (libvirt releases that support
`<input type='touch' bus='virtio'/>`; libvirt 10.0 rejects it, so use `<qemu:commandline>` there) receives the touches. Sunshine looks for that device on QEMU's command line or
in the domain XML and otherwise sends touches as mouse input; the log says which route it took. A device added later
with `device_add` isn't seen, so touches then still move the mouse.
src/platform/linux/qemu/p2p.cpp +16 −7
@@ -289,6 +289,6 @@
void *(*domain_lookup_by_name)(void *conn, const char *name) {nullptr}; ///< virDomainLookupByName.
int (*domain_free)(void *domain) {nullptr}; ///< virDomainFree.
char *(*domain_get_xml_desc)(void *domain, unsigned int flags) {nullptr}; ///< virDomainGetXMLDesc.
int (*domain_open_graphics_fd)(void *domain, unsigned int idx, unsigned int flags) {nullptr}; ///< virDomainOpenGraphicsFD.
int (*domain_open_graphics)(void *domain, unsigned int idx, int fd, unsigned int flags) {nullptr}; ///< virDomainOpenGraphics.
const char *(*get_last_error_message)() {nullptr}; ///< virGetLastErrorMessage.
};
@@ -321,13 +321,13 @@
{(void **) &api->domain_lookup_by_name, "virDomainLookupByName"},
{(void **) &api->domain_free, "virDomainFree"},
{(void **) &api->domain_get_xml_desc, "virDomainGetXMLDesc"},
{(void **) &api->domain_open_graphics_fd, "virDomainOpenGraphicsFD"},
{(void **) &api->domain_open_graphics, "virDomainOpenGraphics"},
{(void **) &api->get_last_error_message, "virGetLastErrorMessage"},
};
for (const auto &[slot, name] : symbols) {
*slot = dlsym(handle, name);
if (!*slot) {
error = library + " has no "s + name + " (libvirt 0.9.7 or newer is needed)";
error = library + " has no "s + name + " (libvirt 1.2.8 or newer is needed)";
dlclose(handle);
return nullptr;
}
@@ -491,12 +491,21 @@
return display;
}
// Create the socket pair ourselves and let libvirt hand one end to QEMU. QEMU authenticates the D-Bus
// client with EXTERNAL, which compares the uid Sunshine claims with the socket's peer credentials: those
// are the creator's, so a pair made by libvirtd (virDomainOpenGraphicsFD) fails for any other user.
int pair[2] = {-1, -1};
if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, pair) != 0) {
error = "socketpair(): " + errno_text();
return std::nullopt;
}
fd_t ours {pair[0]};
fd_t theirs {pair[1]};
if (api->domain_open_graphics(dom.get(), (unsigned int) graphics->index, theirs.get(), 0) != 0) {
error = "virDomainOpenGraphics on domain [" + domain + "] failed: " + api->get_last_error_message();
const auto fd = api->domain_open_graphics_fd(dom.get(), (unsigned int) graphics->index, 0);
if (fd < 0) {
error = "virDomainOpenGraphicsFD on domain [" + domain + "] failed: " + api->get_last_error_message();
return std::nullopt;
}
display.peer = peer_socket_t {fd_t {fd}, std::nullopt};
display.peer = peer_socket_t {std::move(ours), std::nullopt};
return display;
}
} // namespace qemu
src/platform/linux/qemu/p2p.h +5 −4
@@ -3,7 +3,7 @@
* @brief Declarations for reaching a QEMU D-Bus display without a message bus.
* @details QEMU started with `-display dbus,p2p=on` exports its display on connections handed to it
* with QMP `add_client protocol=@dbus-display`. Sunshine gets such a connection either from a QMP
* socket it may use (raw QEMU) or from libvirt's `virDomainOpenGraphicsFD`, which lets a process
* socket it may use (raw QEMU) or through libvirt's `virDomainOpenGraphics`, which lets a process
* running as another user reach a VM of the system libvirt daemon. libvirt is loaded at run time, so
* Sunshine neither links nor requires it.
*/
@@ -82,7 +82,7 @@
* @brief The `<graphics type='dbus'>` device of a libvirt domain.
*/
struct dbus_graphics_t {
std::size_t index {0}; ///< Index among the domain's `<graphics>` devices, as `virDomainOpenGraphics` expects.
std::size_t index {0}; ///< Index among the domain's `<graphics>` devices, as `virDomainOpenGraphicsFD` expects.
bool p2p {false}; ///< Whether the device has `p2p='yes'`.
std::string address; ///< The `address` attribute (live XML of a running bus-mode domain), unescaped.
};
@@ -101,7 +101,7 @@
* address of the private bus libvirt started for the domain.
*/
struct libvirt_display_t {
std::optional<peer_socket_t> peer; ///< Our end of the socket given to `virDomainOpenGraphics`, for `p2p='yes'`.
std::optional<peer_socket_t> peer; ///< Socket from `virDomainOpenGraphicsFD`, for `p2p='yes'`.
std::string bus_address; ///< Bus address from the live domain XML, for bus mode.
std::optional<bool> touch_device; ///< Whether the domain has an `<input type='touch'>` device.
};
@@ -109,7 +109,8 @@
/**
* @brief Open the D-Bus display of a running libvirt domain.
* @details Loads libvirt at run time, connects to `uri`, looks up the domain and reads its live XML.
* For `p2p='yes'` it calls `virDomainOpenGraphicsFD`, which needs the `domain:open_graphics`
* For `p2p='yes'` it gives libvirt one end of a new socket pair with `virDomainOpenGraphics` (not
* `virDomainOpenGraphicsFD`: QEMU authenticates the peer's uid, which would be libvirtd's), which needs the `domain:open_graphics`
* permission; libvirt passes the other end to QEMU with `add_client` and labels the socket for
* the domain. Blocks while libvirt answers.
*
src/platform/linux/qemu/session.h +1 −1
@@ -502,7 +502,7 @@
* @brief Run the D-Bus client handshake on a peer-to-peer socket QEMU accepted and discover its consoles.
* @details For QEMU started with `-display dbus,p2p=on`, after the other end of the socket was
* handed to QEMU with QMP `add_client protocol=@dbus-display` (or libvirt's
* `virDomainOpenGraphicsFD`). QEMU serves one such control connection at a time: a newer one
* `virDomainOpenGraphics`). QEMU serves one such control connection at a time: a newer one
* takes the VM objects away from an older one.
*
* @param fd Socket; ownership moves to the session.
tests/e2e/README.md +1 −1
@@ -128,7 +128,7 @@
QEMU runs with `-display dbus,p2p=on` and no bus. Sunshine gets `qemu_dbus_address = qmp:<socket>`, adds itself as a
display client with QMP `getfd` and `add_client protocol=@dbus-display`, and connects again the same way after a
client disconnects or the guest reboots. Combines with `E2E_AUDIO`, `E2E_INPUT`, `E2E_SESSIONS` and
`E2E_RESET_AFTER_FRAMES`. This is the path libvirt's `virDomainOpenGraphicsFD` uses for `<graphics type='dbus' p2p='yes'>`
`E2E_RESET_AFTER_FRAMES`. This is the `add_client` path libvirt's `virDomainOpenGraphics` uses for `<graphics type='dbus' p2p='yes'>`
(REQ-DEP-002); libvirt itself isn't part of the E2E.
## Several VMs at once
tests/unit/platform/linux/qemu/fake_libvirt/fake_libvirt.c +10 −8
@@ -16,16 +16,16 @@
int unused; ///< Placeholder member.
} *virDomainPtr; ///< Opaque domain handle.
typedef int (*fake_open_fd_fn)(unsigned int idx, void *user); ///< Supplies the socket for virDomainOpenGraphicsFD.
typedef int (*fake_open_fd_fn)(unsigned int idx, int fd, void *user); ///< Receives the socket given to virDomainOpenGraphics; returns 0 on success.
static struct {
char domain[256]; ///< Name of the only domain.
char *xml; ///< Its XML description.
fake_open_fd_fn open_fd; ///< Called by virDomainOpenGraphics.
fake_open_fd_fn open_fd; ///< Called by virDomainOpenGraphicsFD.
void *user; ///< Passed to open_fd.
char last_uri[256]; ///< URI given to the last virConnectOpen, "(null)" for NULL.
int last_index; ///< idx of the last virDomainOpenGraphics.
int last_index; ///< idx of the last virDomainOpenGraphicsFD.
unsigned int last_flags; ///< flags of the last virDomainOpenGraphicsFD.
unsigned int last_flags; ///< flags of the last virDomainOpenGraphics.
int open_connections; ///< Connections opened and not closed.
int open_domains; ///< Domain handles not freed.
const char *error; ///< Last error message.
@@ -93,14 +93,16 @@
return strdup(state.xml);
}
int virDomainOpenGraphicsFD(virDomainPtr domain, unsigned int idx, unsigned int flags) {
int virDomainOpenGraphics(virDomainPtr domain, unsigned int idx, int fd, unsigned int flags) {
state.last_index = (int) idx;
state.last_flags = flags;
// like libvirt, which sends a copy of the descriptor to QEMU; the caller keeps its own
int ret = state.open_fd && fd >= 0 ? state.open_fd(idx, fd, state.user) : -1;
int fd = state.open_fd ? state.open_fd(idx, state.user) : -1;
if (ret != 0) {
if (fd < 0) {
state.error = "Operation not supported: can't open graphics";
return -1;
}
return fd;
return 0;
}
const char *virGetLastErrorMessage(void) {
tests/unit/platform/linux/qemu/test_p2p.cpp +15 −5
@@ -263,6 +263,15 @@
return std::move(ours);
}
/**
* @brief Hand a copy of a socket the caller created to the fake, as libvirt's `virDomainOpenGraphics` does.
*
* @param fd Socket; the caller keeps its own descriptor.
*/
void accept_client(int fd) {
fake->add_client(dup(fd));
}
protected:
std::unique_ptr<qemu_test::fake_qemu_t> fake;
std::filesystem::path dir;
@@ -298,7 +307,7 @@
}
void *handle {nullptr};
void (*setup)(const char *, const char *, int (*)(unsigned int, void *), void *) {nullptr};
void (*setup)(const char *, const char *, int (*)(unsigned int, int, void *), void *) {nullptr};
const char *(*last_uri)() {nullptr};
int (*last_index)() {nullptr};
int (*open_handles)() {nullptr};
@@ -594,15 +603,16 @@
}
// @tag requirements: [REQ-DEP-002]
TEST_F(QemuP2pTest, LibvirtOpensP2pDisplayWithOpenGraphics) {
TEST_F(QemuP2pTest, LibvirtOpensP2pDisplayWithOpenGraphicsFD) {
fake_libvirt_t libvirt;
ASSERT_NE(libvirt.setup, nullptr) << "couldn't load " << fake_libvirt_path();
libvirt.setup(
"win11",
p2p_domain_xml,
[](unsigned int, void *user) -> int {
[](unsigned int, int fd, void *user) -> int {
auto self = (QemuP2pTest *) user;
return self->add_client().release();
self->accept_client(fd);
return 0;
},
this
);
@@ -676,7 +686,7 @@
}
// @tag requirements: [REQ-DEP-002]
TEST(QemuLibvirtTest, ReportsAFailedOpenGraphicsFD) {
TEST(QemuLibvirtTest, ReportsAFailedOpenGraphics) {
fake_libvirt_t libvirt;
ASSERT_NE(libvirt.setup, nullptr) << "couldn't load " << fake_libvirt_path();
libvirt.setup("win11", p2p_domain_xml, nullptr, nullptr);