diff --git a/debian/qubes-core-agent.install b/debian/qubes-core-agent.install index 5c1c92c8..78f7856c 100644 --- a/debian/qubes-core-agent.install +++ b/debian/qubes-core-agent.install @@ -16,6 +16,7 @@ etc/qubes-rpc/qubes.Filecopy etc/qubes-rpc/qubes.GetAppmenus etc/qubes-rpc/qubes.GetImageRGBA etc/qubes-rpc/qubes.InstallUpdatesGUI +etc/qubes-rpc/qubes.Log etc/qubes-rpc/qubes.OpenInVM etc/qubes-rpc/qubes.OpenURL etc/qubes-rpc/qubes.PostInstall @@ -194,6 +195,7 @@ usr/lib/qubes/update-proxy-configs usr/lib/qubes/upgrades-installed-check usr/lib/qubes/upgrades-status-notify usr/lib/qubes/vm-file-editor +usr/lib/qubes/vm-log usr/lib/qubes/xdg-icon usr/lib/qubes/set-default-text-editor usr/share/glib-2.0/schemas/* diff --git a/qubes-rpc/.gitignore b/qubes-rpc/.gitignore index 681721b8..11898196 100644 --- a/qubes-rpc/.gitignore +++ b/qubes-rpc/.gitignore @@ -6,4 +6,5 @@ qfile-agent-dvm qfile-unpacker qopen-in-vm vm-file-editor +vm-log tar2qfile diff --git a/qubes-rpc/Makefile b/qubes-rpc/Makefile index 0c2673f6..9cd5e941 100644 --- a/qubes-rpc/Makefile +++ b/qubes-rpc/Makefile @@ -17,13 +17,13 @@ LDLIBS := -lqubes-rpc-filecopy -lqubes-pure .PHONY: all clean install -all: vm-file-editor qopen-in-vm qfile-agent qfile-unpacker tar2qfile qubes-fs-tree-check bin-qfile-unpacker +all: vm-file-editor vm-log qopen-in-vm qfile-agent qfile-unpacker tar2qfile qubes-fs-tree-check bin-qfile-unpacker ifdef DEVEL_BUILD # Ensure that these programs can find their shared libraries, # even when installed in e.g. a TemplateBasedVM to somewhere other # than /usr. -vm-file-editor qopen-in-vm qfile-agent qfile-unpacker tar2qfile qubes-fs-tree-check: LDFLAGS += '-Wl,-rpath,$$ORIGIN/../../$$LIB' +vm-file-editor vm-log qopen-in-vm qfile-agent qfile-unpacker tar2qfile qubes-fs-tree-check: LDFLAGS += '-Wl,-rpath,$$ORIGIN/../../$$LIB' # This is installed in /usr/bin, not /usr/lib/qubes, so it needs a different rpath. bin-qfile-unpacker: LDFLAGS += '-Wl,-rpath,$$ORIGIN/../$$LIB' endif @@ -33,13 +33,14 @@ bin-qfile-unpacker: qfile-unpacker.o gui-fatal.o qubes-fs-tree-check: LDLIBS := -lqubes-pure qubes-fs-tree-check: qubes-fs-tree-check.o vm-file-editor: vm-file-editor.o +vm-log: vm-log.o qopen-in-vm: qopen-in-vm.o gui-fatal.o qfile-agent: qfile-agent.o gui-fatal.o qfile-unpacker: qfile-unpacker.o gui-fatal.o tar2qfile: tar2qfile.o gui-fatal.o clean: - -$(RM) -- qopen-in-vm qfile-agent qfile-unpacker tar2qfile vm-file-editor qubes-fs-tree-check bin-qfile-unpacker *.o + -$(RM) -- qopen-in-vm qfile-agent qfile-unpacker tar2qfile vm-file-editor vm-log qubes-fs-tree-check bin-qfile-unpacker *.o install: install -d $(DESTDIR)$(BINDIR) @@ -61,7 +62,7 @@ install: prepare-suspend resize-rootfs \ qfile-agent qopen-in-vm qrun-in-vm qubes-sync-clock \ tar2qfile vm-file-editor xdg-icon qvm-template-repo-query \ - qubes-fs-tree-check + qubes-fs-tree-check vm-log # Install qfile-unpacker as SUID, because it will fail to receive # files from other vm. install -t $(DESTDIR)$(QUBESLIBDIR) -m 4755 qfile-unpacker @@ -95,7 +96,8 @@ install: qubes.GetDate \ qubes.ShowInTerminal \ qubes.TemplateSearch \ - qubes.TemplateDownload + qubes.TemplateDownload \ + qubes.Log $(LN) qubes.VMExec $(DESTDIR)$(QUBESRPCCMDDIR)/qubes.VMExecGUI $(LN) /dev/tcp/127.0.0.1 $(DESTDIR)$(QUBESRPCCMDDIR)/qubes.ConnectTCP $(LN) /dev/tcp/127.0.0.1/8082 $(DESTDIR)$(QUBESRPCCMDDIR)/qubes.UpdatesProxy diff --git a/qubes-rpc/qubes.Log b/qubes-rpc/qubes.Log new file mode 100644 index 00000000..8da1736e --- /dev/null +++ b/qubes-rpc/qubes.Log @@ -0,0 +1,2 @@ +#!/bin/sh +exec /usr/lib/qubes/vm-log \ No newline at end of file diff --git a/qubes-rpc/vm-log.c b/qubes-rpc/vm-log.c new file mode 100644 index 00000000..fcf73bcd --- /dev/null +++ b/qubes-rpc/vm-log.c @@ -0,0 +1,218 @@ +/* + * The Qubes OS Project, http://www.qubes-os.org + * + * Copyright (C) 2025-2026 Piotr Bartman-Szwarc + + * + * 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. + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#define MAX_LINE_SIZE 4096 +#define DEFAULT_PRIO LOG_INFO + +struct LogBackend { + void (*open)(const char *ident); + void (*write)(int priority, const char *message); + void (*close)(void); +}; + +static void syslog_open(const char *ident) { + openlog(ident, LOG_PID, LOG_DAEMON); +} + +static void syslog_write(int priority, const char *message) { + syslog(priority, "%s", message); +} + +static void syslog_close(void) { + closelog(); +} + +static const struct LogBackend SYSLOG_BACKEND = { + .open = syslog_open, + .write = syslog_write, + .close = syslog_close, +}; + +// Syslog priority array (Severity 0 to 7) +const int SEV2SYSLOG_ARRAY[] = { + LOG_EMERG, + LOG_ALERT, + LOG_CRIT, + LOG_ERR, + LOG_WARNING, + LOG_NOTICE, + LOG_INFO, + LOG_DEBUG, +}; + + +/** + * Extracts the syslog severity (0-7). + * @param pri Full priority value (facility * 8 + severity) + * @return The severity value (LOG_EMERG, LOG_ALERT, etc.) + */ +int extract_priority(int pri) { + int severity = pri % 8; + if (severity >= 0 && severity <= 7) { + return SEV2SYSLOG_ARRAY[severity]; + } + return DEFAULT_PRIO; +} + +/** + * Analyzes priority and sanitizes the input string. + * + * @param msg_in Untrusted log line (read-only) + * @param msg_out Buffer for the sanitized message + * @param msg_out_size Size of the message buffer + * @return Syslog priority + */ +int sanitize(const char *untr_msg_in, char *msg_out, size_t msg_out_size) { + int prio = DEFAULT_PRIO; + + // Max size for the matched priority string (up to 3 digits + NUL) + char prio_str[4] = {0}; + + regex_t regex; + // Regex pattern: starts with '<' followed by 1-3 digits, followed by '>', + // and then anything + const char *pattern = "^<([0-9]{1,3})>.*$"; + const int priority_id = 1; + regmatch_t matches[2]; + + if (regcomp(®ex, pattern, REG_EXTENDED) == 0) { + if (regexec(®ex, untr_msg_in, 2, matches, 0) == 0) { + // Check if the priority group was matched + if (matches[priority_id].rm_so != -1) { + // Convert signed regoff_t difference to size_t + long len_signed = + matches[priority_id].rm_eo - matches[priority_id].rm_so; + + if (len_signed > 0) { + size_t len = (size_t) len_signed; + + // Comparison: size_t (len) vs size_t (sizeof) + if (len < sizeof(prio_str)) { + strncpy(prio_str, + untr_msg_in + matches[priority_id].rm_so, len); + prio_str[len] = '\0'; + + // Convert string to integer + int full_prio = atoi(prio_str); + prio = extract_priority(full_prio); + } + } + } + } + regfree(®ex); + } + + // Sanitize the message and write it to msg_out + // Cast return value to void as it is ignored right now + (void) qubes_pure_sanitize_string_safe_for_display( + untr_msg_in, msg_out, msg_out_size); + + return prio; +} + +void handle_untrusted(const char *vm, const struct LogBackend *backend) { + char ident[256]; + snprintf(ident, sizeof(ident), "qubes.Log(%s)", vm); + + backend->open(ident); + + char buffer[MAX_LINE_SIZE + 2]; + char trusted_msg[MAX_LINE_SIZE + 4]; + + // Initial confirmation to the sender, i.e., we are ready to receive messages + printf("OK\n"); + fflush(stdout); + + while (fgets(buffer, sizeof(buffer), stdin) != NULL) { + // Determine the actual length of the string read (before NUL) + size_t len = strlen(buffer); + + int truncated = 0; + // If fgets stopped due to buffer size (len >= MAX_LINE_SIZE + 1) + // AND the last character is NOT '\n', the line was truncated. + if (len == sizeof(buffer) - 1 && buffer[MAX_LINE_SIZE] != '\n') { + truncated = 1; + + int c; + while ((c = getchar()) != '\n' && c != EOF); + } + + // Remove trailing newline and carriage return + if (len > 0 && buffer[len - 1] == '\n') { + buffer[--len] = '\0'; + } + if (len > 0 && buffer[len - 1] == '\r') { + buffer[--len] = '\0'; + } + + // Check for empty lines + if (len == 0 && !feof(stdin) && !truncated) { + continue; + } + + int trusted_prio = sanitize(buffer, trusted_msg, MAX_LINE_SIZE); + + if (truncated) { + strcat(trusted_msg, "..."); + } + + + backend->write(trusted_prio, trusted_msg); + + // confrimation to the sender + printf("OK\n"); + fflush(stdout); + } + + backend->close(); +} + + +int main() { + const char *remote_domain = getenv("QREXEC_REMOTE_DOMAIN"); + if (!remote_domain) { + fprintf( + stderr, + "Error: Failed to identify the source VM (QREXEC_REMOTE_DOMAIN not set).\n"); + return 1; + } + + struct QubesSlice remote_domain_slice = + qubes_pure_buffer_init_from_nul_terminated_string(remote_domain); + if (qubes_pure_is_valid_qube_name(remote_domain_slice) != QUBE_NAME_OK) { + fprintf(stderr, "Error: Invalid QREXEC_REMOTE_DOMAIN.\n"); + return 1; + } + + handle_untrusted(remote_domain, &SYSLOG_BACKEND); + + return 0; +} diff --git a/rpm_spec/core-agent.spec.in b/rpm_spec/core-agent.spec.in index e6d79b96..761afddf 100644 --- a/rpm_spec/core-agent.spec.in +++ b/rpm_spec/core-agent.spec.in @@ -898,6 +898,7 @@ rm -f %{name}-%{version} %dir /etc/qubes-rpc %config(noreplace) /etc/qubes-rpc/qubes.ShowInTerminal %config(noreplace) /etc/qubes-rpc/qubes.Filecopy +%config(noreplace) /etc/qubes-rpc/qubes.Log %config(noreplace) /etc/qubes-rpc/qubes.OpenInVM %config(noreplace) /etc/qubes-rpc/qubes.OpenURL %config(noreplace) /etc/qubes-rpc/qubes.GetAppmenus @@ -1027,6 +1028,7 @@ rm -f %{name}-%{version} /usr/lib/qubes/qubes-fs-tree-check /usr/lib/qubes/tar2qfile /usr/lib/qubes/vm-file-editor +/usr/lib/qubes/vm-log /usr/lib/qubes/xdg-icon /usr/lib/qubes/update-proxy-configs /usr/lib/qubes/upgrades-installed-check