From 61f16062b4ae08b11c1cb71603f976bd380d2f25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Marczykowski-G=C3=B3recki?= Date: Fri, 14 Nov 2025 15:02:48 +0100 Subject: [PATCH 1/2] Delay connecting with gui-daemon until after starting Xorg Until now, Xorg was started only after connecting with gui-daemon and receiving xconf message. This was mostly because initial resolution/video mem specified in the xorg.conf couldn't later be changed and that affects maximum working resolution. Since 49420c5 "Increase screen pixmap size beyond initial video RAM if needed", this is no longer a problem. The only parameter (still) needed before starting Xorg is GUI domain id, but this is available on the command line. Change gui-agent to start Xorg earlier and only then wait for gui-daemon. This allows starting all of the user session earlier, optimizing startup time. This is especially relevant for VMs started on boot (before user logs in), when GUI daemon isn't started yet. And also helpful for preloaded disposables, which may start user session as part of preloading now. The current implementation is rather naive: it starts Xorg with hardcoded 1920x1080 resolution (which will later be updated by the qubes.SetMonitorLayout qrexec call), selects which events it want to receive and then waits for the GUI daemon. Especially, no events are actually processed before GUI daemon connects. This assumes all events will be queued and can be processed after GUI daemon connection. Very similar approach is already taken on GUI daemon re-connection, and it works. This makes the msg_xconf sent by the gui-daemon completely ignored. There is a potential issue, if too many events get queued before GUI daemon connects and some get dropped. It's unclear how Xlib handles that (and what is the limit), but if that happens there are two alternative solutions: 1. Start processing events normally, especially collect info about all relevant windows, but dont send anything to GUI daemon before it connects - this basically requires changing write_struct() (and other similar places) to check if GUI daemon is connected. And once GUI daemon connects (first event received on the vchan FD), call send_all_windows_info(). 2. Wait for GUI daemon before registering for events, but then iterate over all windows to collect necessary info and send it to dom0. This approach theoretically can be more reliable (and might even allow restarting gui-agent without restarting Xorg as a side effect), but it's unclear if all data can be rebuilt this way. Especially tray icons may be problematic, as it isn't any window property, but a message sent to relevant selection owner (but theoretically https://tronche.com/gui/x/icccm/sec-2.html#s-2.8 provides solution for this issue). Announce the feature as `late-gui-daemon`. https://github.com/QubesOS/qubes-issues/issues/9940#issuecomment-2847640715 --- Makefile | 2 + .../post-install.d/20-qubes-gui-agent.sh | 4 ++ appvm-scripts/usrbin/qubes-run-xorg | 9 ++- debian/qubes-gui-agent.install | 1 + gui-agent/vmside.c | 68 ++++++++----------- rpm_spec/gui-agent.spec.in | 1 + 6 files changed, 43 insertions(+), 42 deletions(-) create mode 100644 appvm-scripts/etc/qubes/post-install.d/20-qubes-gui-agent.sh diff --git a/Makefile b/Makefile index 582a14fd7..1e5f69efd 100644 --- a/Makefile +++ b/Makefile @@ -168,6 +168,8 @@ install-common: install -d $(DESTDIR)/etc/qubes/post-install.d install -m 0755 appvm-scripts/etc/qubes/post-install.d/20-qubes-guivm-gui-agent.sh \ $(DESTDIR)/etc/qubes/post-install.d/20-qubes-guivm-gui-agent.sh + install -m 0755 appvm-scripts/etc/qubes/post-install.d/20-qubes-gui-agent.sh \ + $(DESTDIR)/etc/qubes/post-install.d/20-qubes-gui-agent.sh install -D appvm-scripts/usrbin/qubes-session \ $(DESTDIR)/usr/bin/qubes-session install -D appvm-scripts/usrbin/qubes-run-xorg \ diff --git a/appvm-scripts/etc/qubes/post-install.d/20-qubes-gui-agent.sh b/appvm-scripts/etc/qubes/post-install.d/20-qubes-gui-agent.sh new file mode 100644 index 000000000..e45fd8a24 --- /dev/null +++ b/appvm-scripts/etc/qubes/post-install.d/20-qubes-gui-agent.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +# announce support for late gui-daemon connection +qvm-features-request supported-feature.late-gui-daemon=1 diff --git a/appvm-scripts/usrbin/qubes-run-xorg b/appvm-scripts/usrbin/qubes-run-xorg index 04a4f8284..e5ca772fd 100755 --- a/appvm-scripts/usrbin/qubes-run-xorg +++ b/appvm-scripts/usrbin/qubes-run-xorg @@ -26,7 +26,14 @@ ## This script is triggered by qubes-gui-agent systemd service. -#expects W, H, MEM and DEPTH env vars to be set by invoker +#expects W, H, MEM and DEPTH env vars to be set by invoker, but use default +# values if they aren't + +[ -n "$W" ] || W=1920 +[ -n "$H" ] || H=1080 +[ -n "$MEM" ] || MEM=$(( W * H * 4 / 1024 )) +[ -n "$DEPTH" ] || DEPTH=24 + DUMMY_MAX_CLOCK=300 #hardcoded in dummy_drv PREFERRED_HSYNC=50 RES="$W"x"$H" diff --git a/debian/qubes-gui-agent.install b/debian/qubes-gui-agent.install index f51cb0232..d5fd06107 100644 --- a/debian/qubes-gui-agent.install +++ b/debian/qubes-gui-agent.install @@ -10,6 +10,7 @@ etc/security/limits.d/90-qubes-gui.conf etc/qubes-rpc/qubes.SetMonitorLayout etc/qubes-rpc/qubes.GuiVMSession etc/qubes/post-install.d/20-qubes-guivm-gui-agent.sh +etc/qubes/post-install.d/20-qubes-gui-agent.sh etc/xdg/autostart/qubes-gtk4-workarounds.desktop etc/xdg/autostart/qubes-icon-sender.desktop etc/xdg/autostart/qubes-qrexec-fork-server.desktop diff --git a/gui-agent/vmside.c b/gui-agent/vmside.c index 09286c5bb..af2b6ca9c 100644 --- a/gui-agent/vmside.c +++ b/gui-agent/vmside.c @@ -2176,12 +2176,11 @@ static void handle_close(Ghandles * g, XID winid) /* start X server, returns its PID */ -static pid_t do_execute_xorg( - char *w_str, char *h_str, char *mem_str, char *depth_str, - char *gui_domid_str) +static pid_t do_execute_xorg(Ghandles *g) { pid_t pid; int fd; + char gui_domid_str[12]; pid = fork(); switch (pid) { @@ -2189,10 +2188,7 @@ static pid_t do_execute_xorg( perror("fork"); return -1; case 0: - setenv("W", w_str, 1); - setenv("H", h_str, 1); - setenv("MEM", mem_str, 1); - setenv("DEPTH", depth_str, 1); + snprintf(gui_domid_str, sizeof(gui_domid_str), "%u", g->domid); setenv("GUI_DOMID", gui_domid_str, 1); /* don't leak other FDs */ for (fd = 3; fd < 256; fd++) @@ -2374,27 +2370,11 @@ static void handle_message(Ghandles * g) } } -static pid_t get_xconf_and_run_x(Ghandles *g) -{ - struct msg_xconf xconf; - char w_str[12], h_str[12], mem_str[12], depth_str[12], gui_domid_str[12]; - pid_t x_pid; - read_struct(g->vchan, xconf); - snprintf(w_str, sizeof(w_str), "%d", xconf.w); - snprintf(h_str, sizeof(h_str), "%d", xconf.h); - snprintf(mem_str, sizeof(mem_str), "%d", xconf.mem); - snprintf(depth_str, sizeof(depth_str), "%d", xconf.depth); - snprintf(gui_domid_str, sizeof(gui_domid_str), "%u", g->domid); - x_pid = do_execute_xorg(w_str, h_str, mem_str, depth_str, gui_domid_str); - if (x_pid == (pid_t)-1) { - errx(1, "X server startup failed"); - } - return x_pid; -} - static void handshake(Ghandles *g) { uint32_t version = PROTOCOL_VERSION; + struct msg_xconf xconf; + write_struct(g->vchan, version); version = 0; read_struct(g->vchan, version); @@ -2407,12 +2387,14 @@ static void handshake(Ghandles *g) PROTOCOL_VERSION_MAJOR, PROTOCOL_VERSION_MINOR, major_version, minor_version); g->protocol_version = version; + + /* discard */ + read_struct(g->vchan, xconf); } static void handle_guid_disconnect(void) { Ghandles *g = ghandles_for_vchan_reinitialize; - struct msg_xconf xconf; if (!ghandles_for_vchan_reinitialize) { fprintf(stderr, "gui-daemon disconnected before fully initialized, " @@ -2425,8 +2407,6 @@ static void handle_guid_disconnect(void) while (libvchan_is_open(g->vchan) == VCHAN_WAITING) libvchan_wait(g->vchan); handshake(g); - /* discard */ - read_struct(g->vchan, xconf); send_all_windows_info(g); } @@ -2580,18 +2560,8 @@ int main(int argc, char **argv) g.clipboard_wipe = access("/run/qubes-service/gui-agent-clipboard-wipe", F_OK) == 0; - g.vchan = libvchan_server_init(g.domid, 6000, 4096, 4096); - if (!g.vchan) { - fprintf(stderr, "vchan initialization failed\n"); - exit(1); - } - /* wait for gui daemon */ - while (libvchan_is_open(g.vchan) == VCHAN_WAITING) - libvchan_wait(g.vchan); - saved_argv = argv; - vchan_register_at_eof(handle_guid_disconnect); - ghandles_for_vchan_reinitialize = &g; + struct sigaction sigchld_handler = { .sa_sigaction = handle_sigchld, .sa_flags = SA_SIGINFO, @@ -2607,8 +2577,10 @@ int main(int argc, char **argv) if (sigaction(SIGTERM, &sigterm_handler, NULL)) err(1, "sigaction"); - handshake(&g); - g.x_pid = get_xconf_and_run_x(&g); + g.x_pid = do_execute_xorg(&g); + if (g.x_pid == (pid_t)-1) { + errx(1, "X server startup failed"); + } mkghandles(&g); /* Turn on Composite for all children of root window. This way X server @@ -2677,6 +2649,20 @@ int main(int argc, char **argv) fprintf(stderr, "Acquired MANAGER selection for tray\n"); } + + g.vchan = libvchan_server_init(g.domid, 6000, 4096, 4096); + if (!g.vchan) { + fprintf(stderr, "vchan initialization failed\n"); + exit(1); + } + /* wait for gui daemon */ + while (libvchan_is_open(g.vchan) == VCHAN_WAITING) + libvchan_wait(g.vchan); + saved_argv = argv; + vchan_register_at_eof(handle_guid_disconnect); + + handshake(&g); + xfd = ConnectionNumber(g.display); struct pollfd fds[] = { { .fd = -1, .events = POLLIN | POLLHUP, .revents = 0 }, diff --git a/rpm_spec/gui-agent.spec.in b/rpm_spec/gui-agent.spec.in index ac95d9974..9ba1d75d7 100644 --- a/rpm_spec/gui-agent.spec.in +++ b/rpm_spec/gui-agent.spec.in @@ -315,6 +315,7 @@ rm -f %{name}-%{version} /etc/qubes-rpc/qubes.SetMonitorLayout /etc/qubes-rpc/qubes.GuiVMSession /etc/qubes/post-install.d/20-qubes-guivm-gui-agent.sh +/etc/qubes/post-install.d/20-qubes-gui-agent.sh %if 0%{?is_opensuse} %{_fillupdir}/desktop.qubes-gui-agent %else From a981e36ac7354bafcb91e7e0d8f2491185dd2074 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Marczykowski-G=C3=B3recki?= Date: Sat, 13 Dec 2025 17:38:30 +0100 Subject: [PATCH 2/2] Create status file with info about gui-daemon connection Create /run/qubes/gui-agent.status with one of the following lines: - starting - GUI agent is starting (Xorg, initial settings etc) - started - GUI agent is started, applications can start normally, but GUI daemon isn't connected yet - connected - GUI daemon connected, user can now interact with the applications Besides diagnostic information, this also can be used by applications to wait for GUI daemon to connect - by using inotify on the file. QubesOS/qubes-issues#9940 --- gui-agent/vmside.c | 37 ++++++++++++++++++++++++++++++++++++- selinux/qubes-gui-agent.fc | 1 + selinux/qubes-gui-agent.te | 8 ++++++++ 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/gui-agent/vmside.c b/gui-agent/vmside.c index af2b6ca9c..fbf7b0915 100644 --- a/gui-agent/vmside.c +++ b/gui-agent/vmside.c @@ -73,6 +73,8 @@ }) + sizeof(x)/sizeof((x)[0])) #define SOCKET_ADDRESS "/var/run/xf86-qubes-socket" +#define STATUS_FILE_PATH "/run/qubes/gui-agent.status" + /* Supported protocol version */ #define PROTOCOL_VERSION_MAJOR 1 @@ -2392,6 +2394,31 @@ static void handshake(Ghandles *g) read_struct(g->vchan, xconf); } +static void write_status_file(const char *status) +{ + int fd = open(STATUS_FILE_PATH, O_CREAT | O_WRONLY | O_NOFOLLOW | O_TRUNC, 0644); + if (fd < 0) { + perror("open status file"); + return; + } + FILE *f = fdopen(fd, "w"); + if (!f) { + perror("fdopen status file"); + close(fd); + return; + } + if (fwrite(status, strlen(status), 1, f) != 1) { + perror("write error"); + } + fclose(f); + close(fd); +} + +static void cleanup_status_file(void) +{ + unlink(STATUS_FILE_PATH); +} + static void handle_guid_disconnect(void) { Ghandles *g = ghandles_for_vchan_reinitialize; @@ -2401,6 +2428,7 @@ static void handle_guid_disconnect(void) "cannot reconnect, exiting!\n"); exit(1); } + write_status_file("started\n"); libvchan_close(g->vchan); g->vchan = libvchan_server_init(g->domid, 6000, 4096, 4096); /* wait for gui daemon */ @@ -2408,6 +2436,7 @@ static void handle_guid_disconnect(void) libvchan_wait(g->vchan); handshake(g); send_all_windows_info(g); + write_status_file("connected\n"); } static _Noreturn void handle_sigterm(int UNUSED(sig), @@ -2498,6 +2527,9 @@ int main(int argc, char **argv) int xfd; Ghandles g = { .x_pid = -1 }; + write_status_file("starting\n"); + atexit(cleanup_status_file); + g.created_input_device = access("/run/qubes-service/gui-agent-virtual-input-device", F_OK) == 0; if(g.created_input_device) { @@ -2551,7 +2583,6 @@ int main(int argc, char **argv) g.last_known_modifier_states = 0; } - parse_args(&g, argc, argv); /* Clipboard wipe functionality is controlled by the @@ -2650,6 +2681,8 @@ int main(int argc, char **argv) "Acquired MANAGER selection for tray\n"); } + write_status_file("started\n"); + g.vchan = libvchan_server_init(g.domid, 6000, 4096, 4096); if (!g.vchan) { fprintf(stderr, "vchan initialization failed\n"); @@ -2663,6 +2696,8 @@ int main(int argc, char **argv) handshake(&g); + write_status_file("connected\n"); + xfd = ConnectionNumber(g.display); struct pollfd fds[] = { { .fd = -1, .events = POLLIN | POLLHUP, .revents = 0 }, diff --git a/selinux/qubes-gui-agent.fc b/selinux/qubes-gui-agent.fc index 04ab6504d..afc9da47e 100644 --- a/selinux/qubes-gui-agent.fc +++ b/selinux/qubes-gui-agent.fc @@ -2,3 +2,4 @@ /usr/bin/qubes-gui-runuser -- gen_context(system_u:object_r:xdm_exec_t,s0) /usr/lib/qubes/qubes-xorg-wrapper -- gen_context(system_u:object_r:xserver_exec_t,s0) /usr/bin/qubes-run-xephyr -- gen_context(system_u:object_r:xserver_exec_t,s0) +/run/qubes/gui-agent.status -- gen_context(system_u:object_r:qubes_gui_agent_state_file_t,s0) diff --git a/selinux/qubes-gui-agent.te b/selinux/qubes-gui-agent.te index 7b1755c66..dca695092 100644 --- a/selinux/qubes-gui-agent.te +++ b/selinux/qubes-gui-agent.te @@ -7,6 +7,9 @@ require { class tcp_socket name_connect; } +type qubes_gui_agent_state_file_t; +files_type(qubes_gui_agent_state_file_t) + allow xdm_t etc_t:file { create write }; allow xdm_t self:passwd rootok; allow domain local_login_t:unix_stream_socket { rw_inherited_sock_file_perms ioctl }; @@ -16,6 +19,11 @@ init_domtrans_script(xdm_t) type_transition xdm_t qubes_var_run_t:sock_file qubes_qrexec_socket_t; manage_sock_files_pattern(xdm_t, qubes_var_run_t, qubes_qrexec_socket_t) write_sock_files_pattern(xdm_t, qubes_var_run_t, qubes_var_run_t) + +allow xdm_t qubes_gui_agent_state_file_t:file { create_file_perms write_file_perms delete_file_perms }; +allow xdm_t qubes_var_run_t:dir { add_entry_dir_perms del_entry_dir_perms }; +filetrans_pattern(xdm_t, qubes_var_run_t, qubes_gui_agent_state_file_t, file, "gui-agent.status") + optional { pulseaudio_domtrans(xdm_t) dev_rw_xen(pulseaudio_t)