Skip to content

GUACAMOLE-261: Add native SPICE protocol support (libguac-client-spice)#688

Open
ciroiriarte wants to merge 36 commits into
apache:mainfrom
ciroiriarte:feature/spice-protocol
Open

GUACAMOLE-261: Add native SPICE protocol support (libguac-client-spice)#688
ciroiriarte wants to merge 36 commits into
apache:mainfrom
ciroiriarte:feature/spice-protocol

Conversation

@ciroiriarte

@ciroiriarte ciroiriarte commented Jul 10, 2026

Copy link
Copy Markdown

Implements GUACAMOLE-261: a native SPICE client protocol plugin for guacd (libguac-client-spice), built on spice-client-glib.

Features

  • Display: connect + render, dynamic client-to-guest display resize, multi-monitor (extend) on a single combined surface.
  • Input: mouse/cursor; keyboard with server-layout keymaps (including correct handling of shift-lock layouts).
  • Clipboard: bidirectional text and image, with session-recording capture and per-transfer direction annotation.
  • Audio: playback and microphone input (record channel), disable-audio-opus, preferred-compression.
  • File transfer: WebDAV shared-folder browser plus SPICE-agent push, selectable via file-transfer-mode (none/agent/drive/both).
  • Video: opt-in efficient codecs (H264/VP9/VP8) for streamed regions via preferred-video-codec.
  • Connection params: proxy, TLS public-key pinning, swap-red-blue, and more.
  • guacclip: utility to extract clipboard artifacts (text/images) from session recordings.
  • Unit tests (CUnit) for the protocol module.

Scope note

This branch also carries the GUACAMOLE-288 multi-monitor primitive (ported from #560 by @corentin-soriano, original authorship preserved), which the SPICE multi-monitor implementation builds on.

Testing

End-to-end QA battery — 14/14 PASS against live SPICE guests: clean -Werror build, unit tests, connect+render, dynamic resize, bidirectional text/image clipboard, session recording with clipboard direction, multi-monitor extend (validated up to 4 heads at 1080p), agent file-copy, and audio. Results are summarized on GUACAMOLE-261.

Companion PRs: client (apache/guacamole-client) and documentation (apache/guacamole-manual).

@necouchman

Copy link
Copy Markdown
Contributor

@ciroiriarte Just at a glance you have 39 commits in this PR, and most of them are not tagged with the GUACAMOLE-261: prefix. ALL of the commits need to have that prefix.

@ciroiriarte

Copy link
Copy Markdown
Author

I cherry picked some commits associated to GUACAMOLE-288 as pre-req/enabler, should I label those as GUACAMOLE-261 too or should I use GUACAMOLE-288 ?

@necouchman

Copy link
Copy Markdown
Contributor

@ciroiriarte All commits that go in as part of this particular PR should be tagged with GUACAMOLE-261:, not GUACAMOLE-288. My overall advice about pulling commits from GUACAMOLE-288 would be that it might be best to coordinate that with @corentin-soriano as far as timing/readiness:

  • If GUACAMOLE-288 happens to be ready and go in, first, then you won't have to cherry-pick commits, you'll just be able to implement multi-monitor support based on that work.
  • If this one goes in before GUACAMOLE-288, then it is probably best not to pull in any of those commits and just wait until overall multi-monitor support has been merged.

ciroiriarte and others added 27 commits July 10, 2026 17:46
Introduces a new in-tree protocol module under src/protocols/spice/ which
allows guacd to proxy SPICE servers (as used by QEMU/KVM/libvirt), built as
libguac-client-spice.so with the standard guac_client_init entry point.

The module is built against spice-gtk (spice-client-glib-2.0) and mirrors the
structure of the existing VNC module. Because spice-gtk is event-driven, the
client thread runs a private GMainContext/GMainLoop and dispatches channels via
the SpiceSession "channel-new" signal.

Implemented:

  * Build integration: --with-spice, PKG_CHECK_MODULES for
    spice-client-glib-2.0 (and optional libphodav for folder sharing),
    ENABLE_SPICE/ENABLE_SPICE_WEBDAV conditionals, wired into the root
    configure.ac and Makefile.am with a configure-summary entry.
  * Display channel -> guac_display (primary surface + damage rects).
  * Keyboard and absolute/relative mouse via SpiceInputsChannel, with an
    X11-keysym to PC-scancode keymap.
  * Cursor channel -> guac cursor layer.
  * Clipboard (SPICE guest agent) <-> guac_common_clipboard.
  * Audio playback -> guac_audio_stream.
  * Plaintext and TLS transport (CA file, cert-subject, ignore-cert).
  * Folder sharing via the SPICE WebDAV channel (shared-dir), gated on
    libphodav.
  * SFTP via common-ssh, screen recording, Wake-on-LAN, and runtime argv
    password/username updates.

USB redirection and smartcard passthrough are intentionally left unconnected
(no physical devices on a headless proxy); multi-monitor renders the primary
monitor only.
The merge of the patch branch left src/terminal/terminal.c uncompilable
(broke main as well, independent of the SPICE work):

  * A duplicate, truncated modifier-tracking block (using the
    GUAC_TERMINAL_KEY_* aliases) preceded the canonical GUAC_KEYSYM_*
    block; its final "else if (... SHIFT ...)" had no body, triggering
    -Werror=dangling-else.
  * A stray '}' inside the Ctrl+letter handling closed the function early,
    cascading into "expected identifier before 'return'" at end of function.
  * Four helper functions (__guac_terminal_is_function_keysym,
    __guac_terminal_is_editing_keysym, __guac_terminal_send_modified_editing,
    __guac_terminal_send_modified_function) were defined but never wired in,
    failing -Werror=unused-function.

Removes the duplicate block and the stray brace, and wires the editing/
function modified-key helpers in alongside the existing arrow-key handling
(mirroring __guac_terminal_send_modified_arrow), so modified editing and
function keys emit their xterm-style CSI sequences.
Two changes to the SPICE module:

1. Fix the SPICE connection never fully establishing.

   The client thread ran its GLib event loop on a private GMainContext
   (created with g_main_context_new() and pushed as thread-default).
   spice-gtk, however, schedules each channel's connection coroutine on
   the *default* main context, so that loop never drove the link: only the
   main channel was ever created, the display/inputs/cursor/playback
   channels never connected, and the connection rendered a blank frame and
   eventually timed out ("User is not responding").

   Drive the event loop from the thread's default GMainContext instead
   (matching how spicy/virt-viewer use spice-gtk). guacd forks a dedicated
   process per connection, so the default context is private to the
   connection and safe to use. With this, all channels open, the remote
   primary surface is received, and display + input work end to end.

2. Add keyboard-layout-aware keysym translation.

   Replaces the single static US keysym->scancode table with the keymap
   subsystem used by the RDP module: per-layout .keymap files compiled to
   _generated_keymaps.c by keymaps/generate.pl, a guac_spice_keyboard that
   tracks modifier/lock state and picks the lowest-cost scancode sequence
   for each keysym, and dead-key decomposition (decompose.c) so precomposed
   accented characters can be typed on layouts that produce them via a dead
   key. The layout is selected with the new "server-layout" parameter
   (default en-us-qwerty); 20 layouts are included.

   generate.pl additionally encodes the "+ext" flag into the 0x100 bit of
   the scancode so extended keys (arrows, navigation cluster, right-hand
   modifiers, keypad Enter/Divide) are transmitted correctly -- the RDP
   generator this was adapted from drops that flag.

Verified end to end against a QEMU/SPICE desktop through the Guacamole web
client: real desktop renders, and "ñ á é í ó ú" type correctly via
es-latam-qwerty (ñ direct, accented vowels via dead-acute decomposition).

Ported from the draft by Nick Couchman in apache#394
(GUACAMOLE-261).

Co-authored-by: Virtually Nick <vnick@apache.org>
…arams

Adds the connection parameters from the upstream draft (GUACAMOLE-261) that
map cleanly onto spice-gtk / the guac_display rendering path:

  * proxy          - connect to the SPICE server through a proxy
                     (SpiceSession "proxy" property).
  * pubkey         - base64 (DER) public key to pin the server's TLS
                     certificate against (SpiceSession "pubkey" +
                     SPICE_SESSION_VERIFY_PUBKEY).
  * swap-red-blue  - swap the red/blue channels when copying the remote
                     framebuffer, for servers reporting BGR rather than RGB.

The remaining draft parameters are intentionally not ported: "clipboard-
encoding" (SPICE vd_agent clipboard is always UTF-8), "encodings"/"cursor"
(tied to the deprecated guac_common_surface rendering the draft used, not the
guac_display path used here), and "autoretry" (guacd connections are
single-shot by design, matching the VNC/RDP modules).

Verified: default connections and swap-red-blue=true both render correctly
against a QEMU/SPICE desktop.
…annel

Forwards audio from the connected Guacamole user to the SPICE server,
mirroring the playback path in reverse:

  * The SPICE record channel is connected (gated on the new
    "enable-audio-input" parameter), with record-start/record-stop signal
    handlers that ack the owner's audio input stream so the client starts or
    stops sending audio when the guest opens or closes its capture device.
  * The user's audio handler parses the offered audio/L16 mimetype and wires
    a blob handler that forwards received PCM data to the record channel via
    spice_record_channel_send_data().
  * "enable-audio" is negotiated when either playback or input is requested,
    since spice-gtk's single property governs both audio channels.

Verified against a QEMU/SPICE desktop with an intel-hda duplex codec:
capturing audio in the guest (arecord) fires the record-start handler, and
the record/playback channels connect as expected.

Ported from the draft by Nick Couchman in apache#394
(GUACAMOLE-261).

Co-authored-by: Virtually Nick <vnick@apache.org>
Exposes a host-side directory to the connected Guacamole user as a file
browser (upload/download/directory listing) and to the SPICE guest via the
WebDAV shared-folder channel, ported from the upstream draft (GUACAMOLE-261)
and modeled on the existing SFTP/RDP-drive filesystem.

  * channels/file.c/.h    - guac_spice_folder: a local-directory filesystem
                            exposed as a Guacamole filesystem object, plus an
                            auto-served "Download/" subfolder watched via
                            inotify.
  * channels/file-ls.c    - directory listing (stream-index JSON).
  * channels/file-download.c - download/get handler + the Download/ monitor.
  * channels/file-upload.c   - upload/put handler.

New parameters: file-transfer, file-directory, file-transfer-create-folder,
file-transfer-ro, disable-download, disable-upload. When file-transfer is set,
spice.c sets the spice-gtk "shared-dir"/"share-dir-ro" properties (taking
precedence over the basic enable-drive path), allocates the folder, and
exposes it to the connection owner; client.c frees it on disconnect; user.c
wires the drag-and-drop upload handler.

Two bugs in the ported draft were fixed during integration:
  * file-upload.c generated upload paths with a leading Windows-style '\'
    (RDP heritage), which guac_spice_folder_normalize_path() rejects; use '/'.
  * file.c created the Download/ folder by mkdir()'ing the wrong path (the
    shared root rather than .../Download) into an undersized strdup'd buffer
    (a heap overflow via guac_strlcat); use a correctly-sized buffer and the
    Download path.

Verified end to end against a QEMU/SPICE desktop: directory listing, file
download, and file upload all work through the exposed filesystem object, the
Download/ folder and its inotify monitor initialize cleanly, and SFTP remains
available alongside.

Ported from the draft by Nick Couchman in apache#394
(GUACAMOLE-261).

Co-authored-by: Virtually Nick <vnick@apache.org>
  * preferred-compression: new connection parameter mapping "off", "auto-glz",
    "auto-lz", "quic", "glz", "lz", or "lz4" to the SpiceImageCompression enum
    and setting the session "preferred-compression" property. Unknown values
    log a warning and are ignored (the connection still proceeds).
  * Audio mute: the playback channel's mute state (notify::mute) is tracked and
    received PCM is dropped while the SPICE server reports playback as muted.
    The channel's volume is intentionally not re-applied -- the guest's PCM is
    already scaled to the reported volume, so re-applying would double-attenuate.

Verified against a QEMU/SPICE desktop: preferred-compression=quic renders, an
unknown value warns and still renders, and the mute-state handler fires. Closes
the low-hanging-fruit items from the SPICE enhancement backlog (#9, #13).
…nel expects

The audio input (microphone) path forwarded the connected user's PCM to the
SPICE record channel unchanged, even when the user was capturing at a different
sample rate or channel count than the rate/channels the record channel
negotiated (reported by record-start). That mismatch is sent to the guest
mislabeled, producing wrong-pitch/artifacted audio.

Record the format requested by record-start, remember the format advertised by
the user, and convert inbound audio to the expected rate/channels (linear
resampling + simple mono<->stereo mixing) before spice_record_channel_send_data
when they differ. Matching formats are forwarded directly as before.

Ported from the draft by Nick Couchman in apache#394
(GUACAMOLE-261).

Co-authored-by: Virtually Nick <vnick@apache.org>
Add a "disable-audio-opus" connection parameter which, when set, exports
SPICE_DISABLE_OPUS=1 before the SPICE session is created, causing spice-gtk to
negotiate the legacy CELT audio codec instead of Opus. guacd runs a dedicated
process per connection, so the environment variable is scoped to the individual
connection.

This is the only client-side audio-codec control spice-gtk exposes; it is
intended for compatibility with SPICE servers whose Opus support is missing or
unreliable, and does not improve audio quality. Choosing raw/lossless playback
remains a server-side setting (QEMU -spice playback-compression=off).
Empirically, disabling Opus does not force CELT on modern SPICE servers:
QEMU/spice-server dropped CELT 0.5.1 years ago, so the server falls back to
raw (lossless) audio when the client stops advertising Opus. Only older
servers fall back to CELT. Update the comments to describe this accurately
(the option can therefore yield lossless raw audio on a LAN, not merely a
legacy compatibility fallback).
…work on shift-lock layouts

guac_spice_keyboard_get_cost() estimated the cost of a key definition using the
modifier fields (set_modifiers/clear_modifiers vs keyboard->modifiers) for BOTH
the lock term and the modifier term, never reading the separate set_locks/
clear_locks fields the keymaps actually carry. On layouts that model Caps Lock
as Shift-Lock (e.g. de-de-qwertz), a shifted symbol has two definitions -- one
via Shift and one via Caps Lock -- and the mis-costed function selected the
Caps-Lock definition, releasing Shift and toggling Caps Lock. On a modern guest
(where Caps Lock does not shift the number row) this produced digits instead of
symbols, matching the German-keyboard bug reported against the original SPICE
draft (GUACAMOLE-261, apache#394).

Use def->set_locks/clear_locks against the lock state (keyboard->modifiers, in
SPICE_INPUTS_* form) for the lock term, mirroring the RDP keyboard subsystem.

Closes #19.
…read

spice-gtk is not thread-safe: each channel's outbound messages are dispatched by
a coroutine on the GMainContext driven by the SPICE client thread, and invoking
channel functions from other threads races with that loop via
spice_channel_wakeup(). Keyboard, mouse, and clipboard-grab calls were made
directly from Guacamole user/handler threads, which can freeze the session under
load -- the crash reported against the original SPICE draft (GUACAMOLE-261,
Nick Downer: "narrowed the clipboard grab freeze to spice_channel_wakeup").

Add guac_spice_defer_call(), which schedules a spice-gtk call on the default
GMainContext (owned by the loop thread) via g_main_context_invoke_full(), and
route the off-thread inputs-channel (key/mouse/lock) and main-channel (clipboard
grab) calls through it. The clipboard "selection" and "request" paths already
ran inside spice-gtk signal handlers (on the loop thread) and are unchanged.

Closes #20.
The disable-display-resize parameter was parsed but never used: there was no
user size_handler, so resizing the client window did not resize the guest (only
the reverse direction — resizing the Guacamole display to match the SPICE
server's surface — was handled).

Add guac_spice_user_size_handler, registered for each user unless
disable-display-resize is set. It requests that the guest resize its primary
display via spice_main_channel_update_display(), marshalled onto the SPICE
event-loop thread with guac_spice_defer_call() (spice-gtk channel functions must
not be called from user threads). Width is aligned down to a multiple of 8, as
SPICE guests generally require.

Closes #5.
…izes reach the guest agent

spice-gtk debounces the implicit monitors-config send (update=TRUE), so only the
first resize per session reached spice-vdagent. Set the geometry without the
implicit send and call spice_main_channel_send_monitor_config() explicitly.
spice_main_channel_send_monitor_config() did not send the config (the first
resize stopped working), so revert to the implicit send via update=TRUE, which
reliably applies the first resize of a session.
…inistically

Dynamic resize was unreliable: the size handler sent a monitors config at a
fixed point after connect, but spice-gtk only delivers one when the agent is
connected with VD_AGENT_CAP_MONITORS_CONFIG and the display's primary surface
exists; sent earlier it is silently dropped. update_display(...,update=TRUE)
also only arms a one-second coalescing timer, so successive resizes collapsed
or were lost.

Queue the requested size and send it only once ready: track agent readiness via
the main channel "notify::agent-connected" (checking the monitors-config
capability) and display readiness via display-primary-create. guac_spice_resize_try()
sends the queued size with an explicit spice_main_channel_send_monitor_config()
(update=FALSE) so every resize is delivered, and is re-invoked whenever a
readiness condition changes to flush a request queued before the guest was ready.

Refs #5.
…t-update

notify::agent-connected fires before the guest agent announces its capabilities,
so VD_AGENT_CAP_MONITORS_CONFIG was not yet available and the gated resize never
sent. Also watch main-agent-update (emitted when capabilities arrive) and
re-evaluate readiness there, flushing any queued resize.
send_monitor_config() did not reliably deliver the monitors config to the guest
agent (session vdagent received nothing). With the readiness gate now ensuring
the agent is connected and monitors-config-capable, use update=TRUE (spice-gtk's
short coalescing-timer send), which does reach the agent; real resizes are spaced
beyond the timer interval so each is delivered.
The readiness gate (agent connected + VD_AGENT_CAP_MONITORS_CONFIG, detected via
main-agent-update) plus explicit spice_main_channel_send_monitor_config() is
validated: send_monitor_config returns TRUE and the config is delivered to the
guest agent every resize. Remove the temporary diagnostic INFO logging.
…amed regions

spice-server chooses the video-stream codec from an ordered list whose default
puts MJPEG first, and only reorders it when the client sends a preferred-codec
message. guacd never sent one, so streamed video was always MJPEG even when the
GStreamer H264/VP9/VP8 encoders/decoders are present. On display-channel open,
call spice_display_channel_change_preferred_video_codec_types() with
[H264, VP9, VP8, MJPEG] so the server prefers efficient codecs (MJPEG fallback).
No-op if the server lacks SPICE_DISPLAY_CAP_PREF_VIDEO_CODEC_TYPE.
…codec param

Unconditionally requesting H.264 exposed a crash: libspice-server 0.15.2's
GStreamer encoder segfaults when it actually encodes H.264/VP8/VP9, taking down
the whole VM (MJPEG never crashes). Requesting a non-MJPEG codec by default is
therefore unsafe against buggy servers.

Gate the preferred-codec request behind a new "preferred-video-codec" parameter
(h264|vp9|vp8|mjpeg), NULL/blank by default so guacd leaves the server's default
(MJPEG-first) untouched and never triggers the crash out of the box. When set,
send the requested codec first with MJPEG appended as fallback. Opt-in lets
deployments with a fixed/newer spice-server use efficient codecs.
The drive-read-only parameter was parsed and logged but never applied — guacd
set spice-gtk's shared-dir but not share-dir-ro, so drive-read-only=true was a
no-op and the shared folder was always writable. Set share-dir-ro from
settings->drive_read_only.
Adds a CUnit test suite (test_spice) for libguac-client-spice, following the
existing per-module tests/ convention (RDP/kubernetes). Covers the keymap
lookup API: guac_spice_keymap_find() resolution, registry well-formedness, and
mapping-array integrity. Wires the tests/ subdir into the build (SUBDIRS,
configure.ac substitutions + AC_CONFIG_FILES).
Ports the reusable, protocol-agnostic multi-monitor support added
upstream in apache#560 (GUACAMOLE-288) by Corentin
Soriano (@corentin-soriano):

  * guac_user_size_handler gains per-monitor x_position/top_offset
  * new "multimon-layout" default-layer parameter (a JSON monitor map)
  * __guac_handle_size parses the new size-instruction fields
  * guac_rect_shrink guards against divide-by-zero

Every protocol's size handler is updated to the new signature; the new
per-monitor arguments are ignored by all protocols except SPICE (see the
following commit), preserving existing single-monitor behavior.

Original work by @corentin-soriano, adapted for this fork:
apache#560
Builds on the ported libguac multi-monitor primitive to add
client-driven multi-monitor support to the SPICE protocol, following the
design of the RDP implementation in apache#560 (by
@corentin-soriano) adapted to SPICE's model, where a QXL guest presents
all heads as regions of a single combined framebuffer.

  * settings: new "secondary-monitors" parameter (max_secondary_monitors,
    default 0 = disabled)
  * the size handler tracks a per-monitor array (fixed, up to
    GUAC_SPICE_MAX_MONITORS) keyed by x_position, tiling monitors
    left-to-right and computing each monitor's left_offset; a
    non-positive size closes a secondary monitor
  * resize pushes the whole layout to the guest agent in a single
    monitors config (enabling/positioning active monitors and disabling
    removed ones)
  * on each combined-surface (re)create, the current layout is sent to
    the client as the "multimon-layout" parameter on the default layer
    so a multi-monitor client can split it into per-monitor windows
  * joining users are told the permitted secondary-monitor count via a
    "secondary-monitors" argv stream

The monitor state uses a fixed array (no dynamic allocation), avoiding
the pointer-ownership concerns raised in review of the upstream RDP
change, and the default layer is referenced via GUAC_DEFAULT_LAYER
rather than casting a guac_display_layer.
Derive the multimon-layout sent to the client from the guest's actual
SpiceDisplayMonitorConfig (read via the display channel's monitors
property) instead of the client-requested geometry, clamped to the
current combined surface and keyed by the guest head id. Republish on
notify::monitors, and reset the recorded surface dimensions on
display-primary-destroy so a delayed publish cannot validate against a
destroyed surface. Fixes secondary windows rendering blank/mirrored when
the guest's real layout diverged from what the client requested.
Coalesce rapid client resize requests into a single guest monitors
config, sent 500ms after the first request via a GLib timeout on the
SPICE event-loop context. A burst of resize events (primary + secondary
windows) previously pushed the monitors config to the guest several
times per second, so the guest agent never settled and thrashed between
clone/extend/disabled states. The timer is cancelled on teardown so it
cannot fire after the client is freed.
Extend the previously text-only SPICE clipboard to images (image/png,
image/bmp, image/jpeg, image/tiff) in both directions. Maps Guacamole
clipboard mimetypes to/from VD_AGENT clipboard types, requests the
most-preferred type the guest offers (text, then PNG, then other image
formats), and offers the client's clipboard to the guest using the type
matching its mimetype. Data is handled by length throughout, so binary
image payloads are safe. The clipboard lock is held across the guest's
request so a concurrent paste cannot swap contents mid-transfer.
Add the recording-include-clipboard connection parameter (matching RDP
and VNC) and pass it to guac_recording_create, so clipboard state changes
are captured in session recordings. Previously hardcoded off.
#7)

The guest-to-client clipboard (data copied OUT of the guest) is broadcast
per-user and so was not captured by the recording's client-socket tee,
leaving the data-exfiltration direction unrecorded. Record it explicitly
via guac_recording_report_clipboard, and annotate every clipboard transfer
in the recording with a direction/mimetype/stream log line
(guest-to-client vs client-to-guest) so recordings can be audited for
clipboard-based data movement. The annotation is written only to the
recording socket and is gated identically to the clipboard data it
describes.
…oses #14)

Add a client->guest upload path over the SPICE agent
(spice_main_channel_file_copy_async), selected by a new file-transfer-mode
parameter: none | agent | drive | both. There is only one Guacamole upload
handler, so the mode deterministically routes the drag/upload gesture:

  - agent: push files into the guest via spice-vdagent (no mount, upload-only)
  - drive: existing bidirectional WebDAV shared folder
  - both:  browse/download via the folder, drag-drop upload via the agent

A blank mode preserves the legacy file-transfer behaviour (drive if enabled).

Uploaded files are staged in a private temp dir (O_EXCL|O_NOFOLLOW) and pushed
on the SPICE event-loop thread. A data_destroy hook on deferred calls guarantees
cleanup if the loop ends before dispatch, an aborted flag prevents pushing a
truncated file after a staging error, and the live main channel is resolved on
the loop thread to avoid dangling references. Completion is logged with
direction and mechanism for auditing.
Hardens the SPICE handler against the memory-safety and DoS findings from the
security assessment (#21):

- F1 (High, CWE-190->787): bound the client-advertised audio rate/channels and
  use the checked-multiply allocation in the resampler, so a crafted audio
  format (e.g. rate=1) can no longer overflow the output-size arithmetic into a
  heap OOB write (32-bit) or a multi-GB allocation (64-bit).
- F2 (Medium, CWE-400/770): cap the SPICE-agent upload staging size so an
  over-large or endless upload cannot exhaust the guacd host's temp storage.
- F3 (Medium, CWE-416): do not start the download-monitor inotify thread. Its
  transfer action was unimplemented (dead code) and it dereferenced the folder
  after guac_spice_folder_free() released it (use-after-free). A completed
  feature must re-introduce it with a proper shutdown path.
- F4 (Medium, CWE-400/789): clamp SPICE-server-supplied cursor/primary-surface
  dimensions to GUAC_DISPLAY_MAX_* before resizing, preventing a malicious or
  MITM'd server from driving an oversized allocation.
…on recordings (#27)

New `guacclip` CLI (peer to guaclog/guacenc) that recovers clipboard
artifacts — text and images — from a `.guac` session recording, filling the
gap those tools leave (guacenc→video, guaclog→keystrokes, neither touches
clipboard). Phase 1 / MVP of #27; no recording-format change, purely
additive tooling. Enabled by the clipboard recording added in #7.

`guacclip [-f] [-o OUTDIR] [--direction guest-to-client|client-to-guest]
[--include image|text|all] [--max-item-bytes N] FILE...` writes
`OUTDIR/manifest.json` + `OUTDIR/items/` with per-item direction, timing,
mimetype, byte count, and SHA-256.

Design mirrors guaclog (guac_parser/guac_socket loop). Clipboard streams are
reassembled strictly by the stream index opened by a `clipboard` instruction,
so graphical `img`/file/pipe blob streams are never misread as clipboard
data. Direction comes from the recording-only `log stream=N direction=...`
annotation, buffered per stream index and consumed at stream open; older
recordings without it get direction "unknown". A self-contained FIPS 180-4
SHA-256 (sha256.c) keeps the tool dependency-free — it links only libguac.

Hardened against hostile recordings: mimetype is slugged to [A-Za-z0-9_] for
filenames (no path traversal) and \u-escaped in JSON (valid manifest even
with non-ASCII bytes); per-item size is capped (64 MiB default, `0` opts
out) and the manifest item count is bounded to prevent OOM; item files are
written via O_EXCL|O_NOFOLLOW temp + atomic rename; stream indices are parsed
with strtol validation. Builds clean under -Werror -Wall; gated by
--disable-guacclip.

Part of #27 (Phase 1). Phase 2 (in-browser audit cockpit) is separate.
Complete the guacclip Phase 1 code: add duplicate detection and a unit-test
suite.

--dedup none|skip (default none): duplicates are detected by SHA-256 of the
reassembled item bytes. Every manifest item gains a duplicate_of field (the
sequence number of the first identical item, or null). In skip mode a
duplicate's file is not written (filename null) and a "duplicate" warning is
added; none mode writes every item. Repeated copies are legitimate audit
events, so none is the default.

tests/: a CUnit suite (test_guacclip, wired into make check) covering SHA-256
FIPS 180-4 vectors and, via the guacclip_interpret() integration path,
multi-blob reassembly, interleaved streams, direction correlation (and the
unknown-direction fallback), the non-clipboard-stream guard, truncation,
oversized capping, bad base64, non-numeric stream index, and both dedup modes.
14/14 pass; -Werror -Wall clean. Requires libcunit (already the test dep for
the other modules).
@ciroiriarte ciroiriarte force-pushed the feature/spice-protocol branch from 90115e3 to 79d366a Compare July 10, 2026 20:47
@ciroiriarte

Copy link
Copy Markdown
Author

Understood. Will wait for @corentin-soriano feedback in the Jira ticket regarding those original commits.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants