Commit Graph
394 Commits
Author SHA1 Message Date
rustdeskandClaude Opus 5 e8ec370b08 webrtc: split the send budget, make close_webrtc uncancellable, restore log retention
- send_bytes computed one deadline before acquiring the send gate and
  reused it for the write, so time queued behind another clone's message
  was charged to this message. A caller that only just lost the gate
  race got a few milliseconds to write in and then tore the whole peer
  connection down for missing them - the narrower the miss, the more
  certain the teardown. The write gets its own budget; the gate wait
  keeps the one it had, and still never closes (it never held the
  permit).

- Stream::close_webrtc awaited WebRTCStream::close, which is exactly the
  form close_detached exists to avoid: most callers sit in a select! arm
  or a future the UI can abandon, and a cancelled close is unretryable
  (is_closed is latched before the first await, so later attempts
  early-return and the handler that evicts the session never runs). It
  is now a plain fn calling close_detached - with no await point there
  is nothing to cancel. close_detached is public and carries the
  runtime-teardown guard the parent repo had written separately for its
  Drop path, so that copy goes away.

- Log retention goes back to 31 files. Raising it to 31*8 defended the
  flood case badly (a file count cannot outrun a flood; only the rate
  limits at the log sites can) while silently multiplying steady-state
  retention and disk for every ordinary install, on every platform.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ExUfAkYbq8UC9pQCiLy8TQ
2026-09-04 23:33:56 +08:00
rustdeskandClaude Opus 5 faf1723577 webrtc: make the cache guard actually apply; drop unusable ICE servers
Two findings, both cases of a check that reads correct but never runs.

- The SESSIONS admissibility test was applied at the lookup only. The
  insert-time duplicate check reads the SAME key and returned whatever
  it found, so an entry the lookup had just rejected came straight back
  - the freshly built peer connection was closed and the rejected one
  returned in its place. A Relay-only request could therefore be served
  by a cached All-policy pc (free to pick a direct pair, with
  is_relayed() answering for a policy nobody asked for), and a caller
  could be handed a pc already latched Closed. Both sites now share
  `is_reusable_for`, which is the only way a test on a shared key holds.

- A TURN server with no credentials is not just useless: webrtc-rs
  validates every configured server when the peer connection is built,
  so one such entry fails EVERY connection, including plain non-relay
  ones that never wanted TURN. The RFC 7065 spelling this branch taught
  us to parse has nowhere to put credentials, so it produced exactly
  that entry - and has_turn_server() then reported TURN as available,
  making callers skip their "don't build a guaranteed-dead Relay-only
  pc" guard. Drop such entries at the source (with a log naming the
  spelling that does carry credentials); has_turn_server() goes back to
  a plain scheme test, which is sound once the constructor guarantees a
  host and credentials.

Malformed entries are now dropped rather than repaired: an unparsable
port used to be folded back into the host ("host:99999:3478") and an
unbracketed IPv6 literal was split at its last colon, both of which
webrtc-ice rejects - again taking every server down, not just the bad
one.

Also spell `&'static str` on the two associated consts (an elided
lifetime there is a future hard error on the pinned 1.75 toolchain) and
drop a test import left behind when the tests stopped touching config.

Regression test for the cache guard; mutation-checked, as are the
credential and malformed-entry paths.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ExUfAkYbq8UC9pQCiLy8TQ
2026-09-04 23:33:56 +08:00
rustdeskandClaude Opus 5 6a175cf5ee webrtc: fix the send/cache/ICE-lifetime findings; bound log retention by volume
Continues the review pass. Eight findings, each verified against the
vendored webrtc-rs (0.13 / -data 0.11 / -ice 0.13) rather than inferred:

- a mid-message dc.write() failure returned the error and left the pc
  open, so the peer kept an unterminated FRAG_MORE prefix and appended
  the next message to it - undetectable, since this framing carries no
  length or sequence number, and callers wrap sends in allow_err!. Close
  the stream when fragments are already on the wire.

- one deadline covered the connect-wait, the gate queue and every write.
  A slow ICE/DTLS completion therefore ate the budget and the write
  timed out into a pc.close() an RTT from working; and the gate arm
  closed the pc from a task that never held the permit, aborting a
  healthy sender's fragment sequence - the exact corruption the permit
  exists to prevent. Connection setup gets its own budget, and only the
  arm holding the permit tears down.

- a SESSIONS hit returned a pc built for the first caller, so a replayed
  offer could get an All-policy connection where the mediator had just
  computed Relay-only, with is_relayed() answering from the cached
  handle. It could also hand back a stream the state handler had already
  closed (it closes before it evicts). Reject both; peer_verified stays
  shared, being a fact about the certificate the entry is keyed by.

- the ICE-candidate sender lives in the on_ice_candidate handler and
  close() clears no handler, so the receiver never closed and the
  forwarder loop this API asks callers to write parked on recv() holding
  a stream clone - one leaked task plus one leaked pc per connection.
  The terminal-state handler now drops that closure. Regression test
  included (fails, by 20s timeout, with the drop removed).

- has_turn_server() accepted the RFC 7065 spelling `turn:host:port`,
  which url makes cannot-be-a-base: host_str() is None, so the server
  became a hostless "turn::3478" that still passed the scheme check.
  force_relay then built a Relay-only pc that could only time out. Parse
  host and port out of the path (IPv6 literals included), and make the
  gate require a host.

- is_relayed() read the stats report's `nominated` flag, which
  webrtc-ice sets per checklist entry and never clears, so after a pair
  switch several entries carry it and HashMap order picked the answer.
  Read the selected pair instead - which also drops a redundant stats
  round trip.

- the ICE-server parsing tests rewrote the process-global, on-disk
  `ice-servers` option while sibling loopback tests were building peer
  connections from it. Split parsing out of get_ice_servers so they can
  test it directly; the suite now passes in parallel.

- log retention is a file count, so pairing it with the new 16 MiB size
  criterion let a flooder rotate away every file predating its own
  activity. Keep enough files that ~31 days survives even at full size.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ExUfAkYbq8UC9pQCiLy8TQ
2026-09-04 23:33:56 +08:00
rustdeskandClaude Opus 5 917907a356 webrtc: detach teardown, bound reassembly before growing, vet the data channel
Four review findings on the receive path, all verified against the
vendored webrtc-rs rather than inferred:

- next() awaited pc.close() on every error/EOF path while every consumer
  polls next() inside a select! against a 1s timer. close() latches
  is_closed before its first await and fires the state handler last, so
  losing that race left a pc no later close() could retry, a SESSIONS
  entry only that handler evicts, and a state_notify that never reaches
  Closed. close_detached() hands the teardown to the runtime; being a
  non-async fn, its callers have no await point to be cancelled at. The
  send path's timeout arm passes its logical-message permit along, so
  the exclusion it relies on now outlives the caller too.

- the reassembly cap was checked after extend_from_slice, so the peak
  was the cap plus a fragment, and BytesMut's reallocate-and-copy growth
  held old and new buffers at once. Check before appending, and stop
  borrowing bytes_codec's ~1 GiB MAX_FRAME_LENGTH: that bound is only
  affordable for TCP because its length prefix rejects an oversize frame
  before buffering any of it, while this framing can only discover the
  overrun by accumulating it - and the answerer runs before any password
  check. MAX_RECV_MESSAGE (64 MiB) bounds both directions.

- the EOF path claimed an empty message could never be confused with a
  reset. It can: webrtc-data maps the StringEmpty/BinaryEmpty PPIDs to
  n == 0 and dc.read() discards the flag that separates them. Both mean
  the same thing to us, so the handling stands - the comment and the log
  line now say what actually happened.

- on_data_channel bound whatever the remote opened, however it opened
  it. Reassembly spans messages, so it is sound only on an ordered,
  fully-reliable channel, and webrtc-rs derives those parameters
  verbatim from the remote's DCEP OPEN; extra channels additionally
  split teardown from the channel carrying traffic and re-arm Open over
  a latched Closed. Refuse both.

Also release the accumulator on the EOF and read-error exits, which were
the only paths that left a partial message reachable through the
SESSIONS clone.

Regression tests for the detached teardown and the bind-once guard, both
mutation-checked; the test comments state what is and is not covered.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ExUfAkYbq8UC9pQCiLy8TQ
2026-09-04 23:33:56 +08:00
rustdeskandClaude Opus 5 402cb24f59 log_throttle: add throttled_log!, the general per-call-site form
The type alone still needs a static plus an `if let` at every use, which
is why the codebase kept hand-rolling equivalents. The macro declares the
static for itself, so adding a bounded site is one line, and it appends
the multiplicity only when there is one to report - an isolated event
logs exactly as it would unthrottled.

Count semantics stay inclusive (the reported number is the total this
line stands for, first occurrence = 1), so a reader needs no arithmetic;
the type's docs now point at the macro and say when to reach past it.

Also rustfmt the module and webrtc.rs, which had drifted (no CI gate
enforces it on this branch).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ExUfAkYbq8UC9pQCiLy8TQ
2026-09-04 23:33:56 +08:00
rustdeskandClaude Opus 5 603edc7581 config: OPTION_ENABLE_WEBRTC, defaulted like the punch options
Same pattern as enable-udp-punch / enable-ipv6-punch: empty value reads
as on against the public server and off against a private one (the
injection lives in rustdesk's get_local_option), so self-hosted
deployments opt in once their server / TURN is ready.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ExUfAkYbq8UC9pQCiLy8TQ
2026-09-04 23:33:56 +08:00
rustdeskandClaude Opus 5 a94ba5955c fmt the envelope-marker test
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ExUfAkYbq8UC9pQCiLy8TQ
2026-09-04 23:33:31 +08:00
rustdeskandClaude Opus 5 041213e44c webrtc: declare the ICE policy inside the offer envelope, not a proto field
Reverts the webrtc_all_ice proto field (64b54ab) in favor of an
`ice_policy: "all"` key inside the webrtc:// envelope JSON, next to the
RTCSessionDescription fields. Same information, better carrier:

- it is a property of the offer itself, so it rides with the offer;
- the rendezvous server never has to know: the envelope is an opaque,
  length-bounded string to hbbs, so no forwarding code and no vendored
  proto copies to keep in sync;
- serde ignores unknown JSON keys when parsing RTCSessionDescription,
  so every skew combination degrades exactly like the proto field did:
  absence - not an error - is the old Relay-only reading.

endpoint_declares_all_ice() is the receiving side: parse failure,
foreign scheme or missing key all read as "not declared".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ExUfAkYbq8UC9pQCiLy8TQ
2026-09-04 23:33:31 +08:00
rustdeskandClaude Opus 5 adc10a5e0c proto: webrtc_all_ice — full-ICE offers under transport-forced relay
use_ws() folds into force_relay because a ws tunnel kills classic TCP/UDP
punching — but ICE opens its own sockets and does not care how signaling
reaches the server. Without a signal, the controlled side must treat every
force_relay offer as Relay-only ICE (answer gated on TURN), which locks
WebSocket deployments out of direct WebRTC entirely.

webrtc_all_ice marks an offer that gathered every candidate type: the
controller's force_relay covers only classic punching, not ICE policy.
Absent/false keeps today's semantics on every skew combination (old
controller, old server dropping the field, old controlled side).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ExUfAkYbq8UC9pQCiLy8TQ
2026-09-04 23:33:31 +08:00
rustdeskandClaude Opus 5 a122e8f4d5 proto: drop the reserved tag in PunchHole
`requester_id = 11` was added and removed in the same rebase batch, never
reached main, and never reached hbbs — whose vendored copy of this file still
stops at field 9. So nothing has ever written or read tag 11, and reserving it
guards a wire format that does not exist.

It was also inconsistent with what this branch already does: `IceCandidate`
retyped tag 2 from `string to_id` to `bytes socket_addr` in place, which is only
sound because none of this proto has shipped. Same premise, so tag 11 is free.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 23:33:31 +08:00
rustdeskandClaude Opus 5 c4cc0b4429 fix(webrtc): reject malformed fragment framing, correct receive-path docs
`next()` read every non-FRAG_END header as "more fragments", so a peer whose
framing had diverged was only caught by the MAX_FRAME_LENGTH cap — and a
FRAG_MORE carrying no payload was never caught at all: it adds nothing to the
accumulator, so the cap never trips and the loop spins for as long as the peer
keeps writing, with no error and no teardown. Decide the header's meaning in one
match, so a future header kind cannot be handled in one place and missed in the
other. Neither case is reachable from send_bytes_inner, which emits FRAG_MORE
only for a full MAX_FRAGMENT_PAYLOAD chunk.

Release the accumulator on the error paths rather than truncating it: at the cap
that is ~1 GiB still referenced through the SESSIONS clone.

Doc corrections, all of them overclaims in the previous pass:

- the cancel-safety entry held only for the successful read path.
  read_data_channel does await after dequeuing on its ErrShortBuffer and DCEP
  branches, and next() awaits pc.close() on its error paths — where
  RTCPeerConnection::close latches is_closed before its first await, so a
  cancelled close silently turns every later close into a no-op and leaves the
  pc in SESSIONS.
- recv_state: cancellation drops the guard mid-message, so it is the
  single-reader assumption, not the mutex, that ultimately keeps two readers
  from splicing into one accumulator.
- is_relayed: stream.rs promised None before pair selection while webrtc.rs
  documented Some(true) under Relay policy; align both.
- get_local_endpoint: examples/webrtc.rs calls it too, not only the tests.
- PunchHole.reserved 11: named the wrong writer — PunchHole is written by the
  rendezvous server, not by peers. Reserve the name as well as the tag.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 23:33:31 +08:00
rustdeskandClaude Fable 5 fcc713b65b fix: cap the log file by size, and keep LogThrottle usable after poisoning
Rotating on age alone let a single day's file grow without limit, so whoever
can drive a hot log site decided how much disk this uses and no amount of
per-site throttling could bound it. Add a size criterion, which covers every
call site at once — including ones no throttle was added to.

LogThrottle: recover the guard on a poisoned lock rather than returning None.
Poisoning only means another thread panicked while holding it; the guarded
data is two counters that are still usable, and going silent for the rest of
the process is worse than a stale count. AGENTS.md permits handling lock
poisoning directly, and it forbids swallowing the error.

Keep map_or over clippy's is_none_or: that was stabilized in Rust 1.82 and CI
pins 1.75.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ExUfAkYbq8UC9pQCiLy8TQ
2026-09-04 23:33:31 +08:00
rustdeskandClaude Fable 5 bf95d52749 feat: add LogThrottle for sites whose rate a peer controls
Debug output is written to the log file, so a log site that fires per received
message lets whoever is sending decide how much a machine writes to disk.
Dropping the line instead would hide real faults, so collapse it: one line per
interval carrying the count of everything suppressed since the last one, with
the first occurrence after a quiet period always reported so an isolated fault
is not delayed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ExUfAkYbq8UC9pQCiLy8TQ
2026-09-04 23:33:31 +08:00
rustdeskandClaude Fable 5 59ce36f606 docs: webrtc 0.13 MSRV pin rationale and upgrade checklist
- Cargo.toml: record why webrtc is pinned to 0.13 — >=0.14 pulls sdp 0.10 /
  webrtc-util 0.12 using usize::is_multiple_of (needs rustc >=1.87), while
  rustdesk CI builds with Rust 1.75 (sciter i128 ABI pin)
- module-level upgrade checklist in src/webrtc.rs listing the version-coupled
  webrtc-rs internals this transport relies on (SCTP write backpressure,
  64KB message cap, detach() semantics, handler-capture leak cycle,
  Disconnected transience, stats-based is_relayed), all verified against
  webrtc 0.13 / webrtc-data 0.11 / webrtc-sctp 0.12
- send_bytes: document the bounded-backpressure mechanism (128 KiB PendingQueue
  semaphore + cwnd/rwnd cap) and that it is NOT cancel-safe

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-04 23:33:31 +08:00
rustdesk 776e71c789 fix: preserve WebRTC endpoint and send semantics 2026-09-04 23:33:31 +08:00
rustdeskandClaude Opus 4.8 e9cdb2fe5d feat: WebRTC data-plane framing, DTLS binding, and pc-leak fixes
- 1-byte-header fragmentation past the 64KB SCTP cap; empty-message and clean-EOF handling
- is_relayed() via selected candidate-pair stats for the direct/relayed flag
- IdPk.dtls_fingerprint + rendezvous webrtc SDP/IceCandidate proto fields
- fix pc leaks: Weak capture breaks the state-handler Arc self-cycle; close pc on new() error paths

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-04 23:33:31 +08:00
rustdesk 1ee667e78b fix: route WebRTC ICE without requester id 2026-09-04 23:33:31 +08:00
rustdesk 411d23b104 feat: support trickle ICE in WebRTCStream 2026-09-04 23:33:31 +08:00
rustdesk 6fffd8842a feat: add rendezvous WebRTC signaling fields 2026-09-04 23:33:31 +08:00
RustDesk f94e3fef6c Merge pull request #595 from rustdesk/config-local-settings
config: list four local settings that custom clients could not preset
2026-09-04 16:04:35 +08:00
rustdeskandClaude Fable 5.1 750739f7f8 config: list five local settings that custom clients could not preset
Every key rustdesk reads through `LocalConfig` was checked against the
settings lists; these were in no list, so a custom client's
`default-settings` / `override-settings` could not set them:

- allow-sync-clipboard-between-sessions (rustdesk/rustdesk#15934): the
  constant lived in rustdesk's `src/clipboard.rs`; it is defined here
  now, with the other option keys.
- printer-incomming-job-action, allow-printer-auto-print,
  printer-selected-name: the incoming-print-job policy, read in
  `client/io_loop.rs`.
- enable-check-update: listed for completeness; `check_software_update`
  returns early for custom clients, so presetting it changes nothing
  there today.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab
2026-09-04 16:00:32 +08:00
RustDesk 840c8ec1f7 Merge pull request #594 from rustdesk/port-forward-mux
Port forward mux
2026-09-04 15:37:34 +08:00
RustDeskandCopilot Autofix powered by AI 60c7fd80ea Update comment for connection punch-through options
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-09-04 15:37:18 +08:00
rustdeskandClaude Opus 5 1e617ddf4d config: enable-port-forward-mux local setting
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab
2026-09-04 01:46:34 +08:00
rustdeskandClaude Opus 5 6918d35935 proto: PortForwardChannel family for a multiplexed port-forward tunnel
Adds PortForward.multiplex, Features.port_forward_mux and the
open/opened/data/close/window_update channel frames, union tag 33.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab
2026-09-03 20:14:42 +08:00
RustDesk 05ed68fed8 Merge pull request #591 from fxd0h/fix/flipped-transform-warn
warn once when a flipped output transform loses its mirror
2026-09-01 07:35:57 +08:00
RustDesk adff91bad1 Merge pull request #592 from fxd0h/fix/wlroots-output-position
fix(linux): read output positions from xdg-output on wlroots
2026-09-01 07:33:31 +08:00
Mariano Abad 77d0b2a140 fix(linux): read output positions from xdg-output on wlroots
wl_output.geometry carries (0, 0) for every output on wlroots compositors;
the real layout is published through xdg-output only. Reading location from
wl_output therefore stacks every monitor on the origin, so the desktop rect
collapses to the widest single output and the uinput range never reaches a
monitor that sits to the right of the first one.

Measured on sway 1.11: wl_output reports x=0 for both DP-6 and eDP-1 while
xdg-output reports 0 and 1920; the uinput range came out 1920x1080 instead of
the 3360x1080 desktop. Mutter fills both, so GNOME is unchanged.
2026-08-31 19:24:58 -03:00
RustDesk 1a43eb2873 Merge pull request #590 from fufesou/refact/logs-90-days
refact(logs): keep logs, 31 -> 90 files
2026-08-31 10:38:35 +08:00
Mariano Abad addc398091 warn once when a flipped output transform loses its mirror
transform_degrees folds the four Flipped variants into their rotation
and drops the mirror half, so a consumer correcting frames by this
value serves a flipped output mirrored. No compositor of ours can
produce a flipped output to measure the mirror against, so carrying
it waits for a measured producer; until then the loss is said once in
the log instead of silently, and the doc states it.
2026-08-30 23:13:48 -03:00
fufesou c9823ad735 refact(logs): keep logs, 31 -> 90 days
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-08-31 10:00:01 +08:00
RustDesk 399030eaf3 Merge pull request #589 from 21pages/fix_deadlock
fix(webrtc): make session cleanup deadlock-safe
2026-08-31 08:41:12 +08:00
21pages 219f917d5a fix(webrtc): make session cleanup deadlock-safe
Release data channel and session cache locks before awaiting close
  operations.

  Remove cached sessions by PeerConnection identity instead of SDP
  fingerprint so delayed callbacks cannot remove replacement sessions.

  Add regression tests for duplicate-session close re-entry and
  identity-based cleanup.

Signed-off-by: 21pages <sunboeasy@gmail.com>
2026-08-30 19:45:17 +08:00
rustdeskandClaude Opus 5 c2d8a6db2e add hide-general-settings builtin option
https://github.com/rustdesk/rustdesk-server-pro/issues/1001

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 10:35:05 +08:00
RustDesk dc95b4f401 Merge pull request #586 from fxd0h/feature/wayland-output-transform
feat: carry the wl_output transform in WaylandDisplayInfo
2026-08-18 09:58:21 +08:00
Mariano Abad a3e27b2742 test: pin the rotation contract and the missing-field default
all eight wl_output variants against their degrees, and a snapshot
serialized without the field deserializing to 0, so an older probe
child never forces a lockstep upgrade.
2026-08-17 21:49:50 -03:00
Mariano Abad 3d18e61afa feat: carry the wl_output transform in WaylandDisplayInfo
the mode keeps its unrotated dimensions and logical_size arrives already
swapped, so without the transform a rotated output is indistinguishable
from a scaled one. degrees (0/90/180/270), flipped variants map to their
rotation, serde default keeps older serialized snapshots deserializing.
needed by rustdesk#15886 (drm capture ships a rotated scanout sideways).
2026-08-17 20:54:57 -03:00
RustDesk b2b1ac453d Merge pull request #584 from fufesou/refact/remove-linux-headless
refact: remove linux headless
2026-08-14 21:18:07 +08:00
fufesou fe929d1139 refact: remove linux headless
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-08-13 22:14:08 +08:00
RustDesk 3ed938544f Merge pull request #582 from rustdesk/fix-texture-lifetime
add texture-render-health internal option key
2026-08-13 09:56:46 +08:00
rustdeskandClaude Fable 5 d19ce39e51 add texture-render-health internal option key
Written by the texture-render watchdog / startup probe in the main repo;
a failed record forces texture rendering off until it is cleared (probe
pass or an explicit toggle of use-texture-render).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 09:53:50 +08:00
RustDesk f124c0a5d4 Merge pull request #580 from fxd0h/feature/wayland-socket-fallback
fix(linux): find the compositor socket when WAYLAND_DISPLAY is not set
2026-08-12 14:30:02 +08:00
Mariano Abad 8ec37ed531 fix: bound the wait and the pipe reads against a group-escaping descendant
Two P3 hardening items from the review: a descendant that changes its
own process group escapes the deadline's group kill, and could then leak
or block the parent.

- The deadline path now also sends a pid-targeted SIGKILL to the direct
  child, so child.wait() is bounded even if the child left the group and
  the group kill missed it.

- The normal-exit path drains stdout and stderr non-blocking instead of
  read_to_string: the child has exited so its output is already
  buffered, but an escaped grandchild holding a write end would keep the
  pipe from EOF and hang a blocking read. The drain is capped so a
  descendant that keeps writing cannot spin it. first_buffered_line now
  shares that drain.

Verified: a probe child whose grandchild setpgid-escapes and holds the
pipe returns in 25 ms instead of hanging, and a direct child that
escapes and blocks is bounded to the deadline instead of its full
sleep.
2026-08-12 02:39:28 -03:00
Mariano Abad 6e27672774 refactor: move the socket probe to its own file behind a feature
Addresses the maintainer's two requests on #580: linux.rs was crowded,
and the new fallback should not touch the base Wayland path.

All the socket-probe machinery moves to src/platform/linux/wayland_probe.rs
- the child entry point, the runtime-dir scan, the privilege drop, the
process-group probe and its buffered-line inspection - leaving
WaylandDisplayInfo and get_wayland_displays in linux.rs. The module and
the fallback call in get_wayland_displays are gated on a new off-by-default
feature 'wayland_probe'; without it get_wayland_displays returns the
connect error exactly as it did before this fallback existed, so a
consumer that does not build the DRM login-screen backend compiles none
of this. The DRM build turns the feature on through scrap/drm.
2026-08-12 01:48:27 -03:00
Mariano Abad f69648753a fix: contain the probe in a process group, drop its privileges, and format
Addresses fufesou's #580 re-review:

- The child runs in its own process group and the deadline kills the
  whole group, so a hung loginctl or a leaked grandchild cannot survive
  the child or keep the pipes open past the reads. Verified: a
  deliberately leaked sleep grandchild no longer outlives the probe.

- The probe parses compositor-controlled data, so before touching the
  socket the child drops to the runtime directory's owner and refuses
  to probe at all if the drop fails, rather than parsing untrusted
  bytes as root. loginctl still runs as root first, since finding the
  seat needs it.

- first_buffered_line now distinguishes an inspected-but-empty pipe
  (WouldBlock) from an uninspectable one (fcntl or read error): only a
  real absence of the handshake latches the binary unsupported, an
  inspection failure does not.

- rustfmt-clean.
2026-08-12 00:17:25 -03:00
Mariano Abad 6c86f88e10 fix: latch an unwired consumer on the timeout path too
A binary that does not dispatch the probe argument runs its normal
startup; a long-running one outlives the deadline and was killed before
the handshake check could run, so PROBE_UNSUPPORTED never latched and
every enumeration cycle spawned a full consumer process again. The
timeout path now judges the child by what it already wrote: a real
probe prints the magic line first and flushes, so its absence after a
whole deadline means this is not a probe. Only the buffered bytes are
read, non-blocking, because an EOF-seeking read could hang on a
grandchild that inherited the write end of the pipe.

Also report the exit status when a failed child left stderr empty,
which panic=abort and signals do, and name the malformed-list case in
the deserialization error.
2026-08-10 12:36:31 -03:00
Mariano Abad abca153be9 fix: isolate the socket probe in a subprocess, and bound the seat0 lookup
Two review findings:

- The release profile builds with panic=abort, so the in-thread isolation
  was an illusion: an sctk panic on malformed bytes from a scanned socket
  aborted the whole server before ProbeBusyGuard could run. The probe now
  runs in a child process spawned from the current executable, which the
  consumer binary dispatches to wayland_display_probe_child_main before
  any other startup work. A panic there kills only the child, and the
  deadline now kills the child instead of leaking a blocked thread.

- seat0_runtime_dir ran loginctl through Command::output with no bound,
  before the worker and its timeout existed, while the caller held the
  DISPLAYS lock. The lookup now runs inside the child, under the same
  two second deadline as everything else.

A binary that does not dispatch the probe arg fails a magic line
handshake and the probe latches off for the process lifetime, so the
fallback degrades to the pre-fallback behavior instead of spawning a
full consumer process per enumeration cycle. The consumer wiring is one
early dispatch in core_main:

    #[cfg(target_os = "linux")]
    if std::env::args().nth(1).as_deref()
        == Some(hbb_common::platform::linux::WAYLAND_DISPLAY_PROBE_ARG)
    {
        hbb_common::platform::linux::wayland_display_probe_child_main();
    }
2026-08-10 09:56:12 -03:00
Mariano Abad cccfb8af87 log which socket answered when nothing named one
The fallback was silent on success, so a host where it engaged looked identical to one
where it was never reached. Debug level: it runs on a cache miss, not per frame.
2026-08-08 15:48:09 -03:00
Mariano Abad 4220302d40 fix: bound the socket fallback, and take the runtime dir from the uid
Answers the review on this PR. The fallback kept three states that could not
self-heal, and two of them came from the same place: it trusted the environment and
it had no deadline.

B1, no deadline on any part of the new path while the caller holds a process-wide
lock: `connect(2)` parks on a full backlog and sctk's roundtrip polls with no
deadline, so a socket that accepts and never speaks wayland stalls the display
service, the six drm_capturer call sites and the flutter_ffi SyncReturn. The probe
now runs on its own thread with a 2 s deadline, under the 3 s the uinput caller
already budgets, and only one probe can be in flight. That also keeps sctk's
`panic!`/`todo!` on malformed output events off the thread holding the lock, so it
can no longer poison that mutex, and a `Builder` is used because `thread::spawn`
panics when a thread cannot be created.

B2, a newly reachable empty `Ok` latched into the caller's cache for the process
lifetime: an empty list is now an error, so the existing no-cache path still
applies and the next poll retries.

B3, the `WAYLAND_SOCKET` half of the guard could not fire, because
`connect_to_env` removes that variable on its success and bad-fd paths and this is
re-entered every ~1.5 s. Both variables are read at the call site before
connecting, and the answer is latched: a consumed variable cannot turn a process
that was pointed at a compositor into one free to look for another.

C4 and S1 have the same fix: the directory is `/run/user/<uid>` of the active seat0
session rather than `XDG_RUNTIME_DIR`, so no environment value reaches a connect in
a process that can be root, and it works in the default build, which is given no
such variable (C3). The candidates are scanned rather than guessed, since
`wl_display_add_socket_auto` takes the first FREE name up to `wayland-32` and a
greeter accumulates leftovers; that drops the `path.exists()` pre-stat and its
TOCTOU window with it (C7), and empty values are no longer read as names (C6).

C2, the first socket that accepted used to win unconditionally: the loop now
carries on to the next candidate when a socket connects but fails the protocol or
reports no outputs.

C8, `registry_handlers!()` was empty, so a `wl_output` advertised between the
registry snapshot and the roundtrip was dropped and never reached `outputs()`.

C5 is real and is not fixable here: `get_primary_monitor` spawning a timeout-less
xrandr inside the held lock is in the consumer, and I will send it there.
2026-08-08 14:26:01 -03:00
Mariano Abad f436f53f0d fix: only fall back when nothing named an endpoint, and validate the directory
Review of the first version, all three correct.

The important one: `connect_to_env()` honours `WAYLAND_DISPLAY` and `WAYLAND_SOCKET`,
and the fallback ignored that choice. If an explicit endpoint was named and failed,
this could attach to a *different* compositor and read output positions from the wrong
display. It now runs only when neither variable is set, which is also the only case it
was ever justified by.

`XDG_RUNTIME_DIR` is read with `var_os` and rejected when relative, since `.` or
`../tmp` would probe against the working directory. And every failed probe is kept
instead of the last one overwriting the first, so an error naming both sockets says
what happened to both.

The comment is three lines now: the reasoning belongs here, in the commit.
2026-08-07 19:29:04 -03:00