Skip to content

Update MeshAgent from Fork - #33

Open
ivan-flamingo wants to merge 68 commits into
flamingo-stack:masterfrom
Ylianst:master
Open

Update MeshAgent from Fork#33
ivan-flamingo wants to merge 68 commits into
flamingo-stack:masterfrom
Ylianst:master

Conversation

@ivan-flamingo

@ivan-flamingo ivan-flamingo commented Mar 19, 2026

Copy link
Copy Markdown

Summary by CodeRabbit

  • New Features

    • Multi-monitor KVM on Linux; macOS per-user KVM socket-mode and updated LaunchAgent with immediate bootstrap
  • Bug Fixes

    • Improved macOS mouse handling and helper audit-session joining
    • More robust Windows desktop switching, KVM relay, and helper spawn/restart behavior
  • Improvements

    • Automatic system CA loading and configurable TLS validation
    • Monitor-border auto-start, notify-bar MaxWidth option, updated Windows SDK targets, README options table reformatted

panreyes and others added 15 commits February 12, 2026 17:31
* Removed trying to load Windows system DLLs from user folders

Fixes Ylianst/MeshCentral#7606
Signed-off-by: Simon Smith <simonsmith5521@gmail.com>
Updated CodeQL GitHub Actions to use the latest versions.
Uncomment language selection for CodeQL analysis.
…dded monitor-border.js to the polyfills (#331)

* Added option to limit the width of the desktop notification bar.

* Added monitor-border to the embedded modules in the polyfills. Added auto-start too.
* Fix strcat_s implementation

Old implementation didn't work, it would crash when it tried to dereference and set a null pointer (*x = i). This version works by setting the default size to destinationSize, so it will throw in the later check if no \0 is found. This function is otherwise unused in the code.

* Added function to load certs from the system

Windows doesn't respect the openssl standard, so that needed to be handled manuall.

Since this project uses a statically linked openssl, the default locations may be inaccurate. If that directory doesn't exist, it tries to get it from the system openssl via a popen. If that also fails, it will not be able to load system certs.

* Added system cert loading wherever an SSL context is established

* Modified OnHttpsConnection callback so the binder can pass general data to the callback.

This was necessary to get a handle on the agent during the connection callback.

* Added check for validateWebCert config option

This will cause the connection to fail if the value is set and the agent attempts to connect to a server with an invalid web certificate

* Fixed some handling of rejectUnauthorized in duktape net handler

Before it would override any user defined checks; now it will foil to connect if the system certs don't match, but will still run user checks if it passes those.

* Added validatewebcert arg to meshcmd.js

This will cause rejectUnauthorized to be true if you pass it in

* Updated default recovery core to check for the "validateWebCert" option and use rejectUnauthorized if it is set.
… ARM systems (#224)

* Force alignment of allocated memory. Without this change, I have noticed crashes on ARM based devices which have stricter alignment requirements than x86. Ideally, this code should be rewritten to pad the sizes of all "structs" and to consider all of the different architecture-specific alignment requirements. But in practice, I suspect that rounding up allocation sizes to a multiple of pointer sizes is likely good enough. This is a pragmatic one-line change that shouldn't break existing code and should prevent any of the crashes that I have observed.

* Added a few more memory alignments when using both primary and extra memory in a back-to-back memory allocation.
@ivan-flamingo ivan-flamingo self-assigned this Mar 19, 2026
@ivan-flamingo ivan-flamingo changed the title Update MeshAgent Update MeshAgent from Fork Mar 19, 2026
si458 and others added 9 commits March 24, 2026 16:11
Signed-off-by: Simon Smith <simonsmith5521@gmail.com>
System proxy detection executes the proxy-helper JavaScript path, which can
create short-lived EventEmitter and ChildProcess object graphs during reconnect
attempts. Without an explicit GC pass those objects can remain retained until a
later heap cycle, causing repeated reconnects to drive up peak heap usage.

Run a Duktape GC pass immediately after popping the proxy-helper result so the
temporary proxy-detection objects are collected promptly.
…ut (#342)

When MeshServer_ConnectEx_NetworkError fires (20s connection timeout),
it frees j (==agent->controlChannelRequest) but does not set the pointer
to NULL. ILibWebClient_CancelRequest, called immediately after, executes
synchronously on the chain thread (ILibChain_RunOnMicrostackThread macro
calls the handler inline when already on the chain thread). This triggers
MeshServer_OnResponse(ReceiveStatus_Complete) which sees a non-NULL
controlChannelRequest and calls ILibMemory_Free on the already-freed
pointer, causing a double-free and heap corruption on every retry cycle.

Fix: set agent->controlChannelRequest = NULL right after ILibMemory_Free(j)
so MeshServer_OnResponse safely skips the cleanup block.

Relates to: #110, #151, #281
Relates to: Ylianst/MeshCentral#7407

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…re (#344)

On macOS Tahoe (26.x), the dashboard's Desktop tab shows a black frame
on Apple-Silicon Macs because the user-context -kvm0 helper inherits
the daemon's system-side audit session (asid != console user's gui
session asid). Per-user XPC services like com.apple.replayd are
registered in gui/<console-uid> and unreachable from a different
audit session, so SLSHWCaptureDesktopProxying / ReplayKit returns
empty frames after a 5-second timeout.

This patch joins the console user's GUI audit session at TWO points:

1. Pre-exec, inside SpawnProcessEx4's fork-child while still root:
   does the actual auditon-based discovery and audit_session_join. On
   Tahoe, only root can call audit_session_port() for arbitrary asids
   or enumerate via auditon(A_GETPINFO_ADDR) across audit sessions, so
   discovery has to happen here.

2. Post-exec, in the helper's main() entry for argv[1] == "-kvm0":
   the kernel resets the asid on exec, so the pre-exec join doesn't
   persist on its own. Post-exec re-joins the same session; the
   pre-exec join leaves enough kernel state that auditon visibility
   for the user's gui session works from the dropped-privilege uid.

Discovery picks the LOWEST visible asid belonging to the target uid
(typically 100002, the user's primary gui session, vs higher asids
for transient/per-app sessions where replayd doesn't register). A
fallback brute-force range probe handles the case where Tahoe's
cross-session auditon visibility filter blocks discovery entirely.

Also includes a small zlib build fix in meshcore/zlib/zutil.h: skip
the "fdopen() not available" macro on __APPLE__ because newer Xcode/
Clang treats the resulting macro expansion as a compile error against
the system stdio.h prototype. macOS has fdopen, so the macro shouldn't
fire on Apple platforms anyway.

No behavior change on Linux/Windows. No new dependencies (audit APIs
ship with the macOS SDK).

Tested on macOS Tahoe 26.x, Apple Silicon, MeshCentral 1.x server.
Before patch: 100% black canvas in dashboard Desktop tab. After patch:
full desktop video streaming at native resolution.

Co-authored-by: austin <me@austinrenfroe.com>
Signed-off-by: Simon Smith <simonsmith5521@gmail.com>
Signed-off-by: Simon Smith <simonsmith5521@gmail.com>
…workaround) (#349)

* macOS Tahoe: post mouseMoved CGEvent instead of CGWarpMouseCursorPosition

On Tahoe, CGWarpMouseCursorPosition fires hot-corner watchers when the
warp destination sits in a corner zone — the OS treats the warp as a
cursor 'presence' at the destination rather than silent teleportation.
On every Desktop-tab connect from the MeshCentral browser UI, the
initial mouse coordinate sent by the browser commonly lands in the
top-right of the video pane, the warp puts the OS cursor at the
top-right of the screen, and Tahoe fires Notification Center
(surfacing Calendar + Weather widgets). Top-left maps to Mission
Control / app-spread the same way.

Posting a kCGEventMouseMoved CGEvent goes through the normal HID-event
path. Hot-corner watchers fire on cursor *dwell*, not on synthetic
movement events, so this stops triggering Notification Center /
Mission Control on connect. Subsequent browser-side cursor movement
generates normal CGEvents that move the OS cursor away from any
corner the cursor briefly visits.

This also makes mesh's mouse-move behavior more conformant with what
macOS apps expect from input frameworks — CGWarpMouseCursorPosition
is documented as 'does not generate mouse events,' which means apps
that listen for mouseMoved (screensaver wake, sticky-corner activation,
HID-tracking apps) miss out on cursor activity that's actually
happening from the user's perspective.

* macOS Tahoe: add -kvmagent mode + LaunchAgent plist (Phase 1)

The user-context LaunchAgent runs natively in gui/<console-uid> with
the user's audit session id (typically asid=100002), where
com.apple.replayd registers. This eliminates the cross-session XPC
bootstrap-lookup failure that breaks ScreenCaptureKit screen capture
when the kvm helper is spawned from the system-side LaunchDaemon
(asid=100001).

Architecture matches Splashtop Streamer's: a separate per-user
LaunchAgent with LimitLoadToSessionType = ['Aqua', 'LoginWindow'],
loaded by launchd into the user's domain. The daemon connects to
the agent over a Unix domain socket; same TLV protocol that flowed
over the inherited stdio pipes in the -kvm0 / -kvm1 case.

Phase 1 deliverables:
- mac_kvm.c: convert KVM_Listener_Path from #define to a writable
  variable + setter, so the listener path can be parameterized for
  the per-user agent. Defaults preserve legacy -kvm1 behavior.
- meshconsole/main.c: new -kvmagent mode. Optional argv[2] sets the
  socket path; default is /tmp/meshagent-kvm-<uid>.sock so a non-
  root user can bind it.
- macos/com.meshagent.kvm.plist: LaunchAgent definition. RunAtLoad
  true, KeepAlive on non-clean-exit, ThrottleInterval 30s.

Phase 2 (next): daemon's kvm_relay_setup() in mac_kvm.c connects to
this socket instead of fork-exec'ing a -kvm0 helper.

Phase 3: drop the audit_session_join workaround entirely once the
daemon-side connect path is live and verified.

* macOS Tahoe: daemon connects to user-LaunchAgent socket (Phase 2)

Phase 2 of the launchagent-split refactor. Adds the daemon-side
socket-connect path that pairs with the -kvmagent mode added in
Phase 1.

When MeshCentral initiates a Desktop session, kvm_relay_setup()
now first tries to connect to /tmp/meshagent-kvm-<uid>.sock — the
listener registered by the user-context LaunchAgent. If the agent
is up, the daemon hands the KVM session off to it via the existing
TLV protocol (now flowing over a Unix domain socket instead of
inherited stdio pipes from a fork-exec'd helper). If the agent
isn't reachable, falls through to the legacy fork-exec -kvm0
helper, preserving the existing behavior for fixtures that don't
yet have the LaunchAgent installed.

The agent's audit session is gui/<uid> natively (via launchd's
LimitLoadToSessionType=Aqua loading), so com.apple.replayd is
reachable from there and ScreenCaptureKit screen capture works
without the audit_session_join workaround the daemon-spawn path
needs. Cleanly supersedes PR #344's approach.

Implementation:
- New static state (g_kvmSocketPipe, g_kvmSocketFD, write handler
  + reserved pointer) tracking the socket-mode session.
- kvm_relay_setup tries connect(); on success wraps the FD in
  ILibProcessPipe_Pipe_CreateFromExisting() so the existing pipe
  abstractions for write / pause / resume / free continue to work.
- New kvm_relay_socket_ReadHandler with the
  ILibProcessPipe_Pipe_ReadHandler signature; reuses the same TLV
  parser as kvm_relay_StdOutHandler.
- New kvm_relay_socket_BrokenHandler clears the static state on
  socket disconnect so subsequent kvm_relay_setup calls reconnect.
- kvm_relay_feeddata branches: ILibProcessPipe_Pipe_Write to the
  socket pipe in socket mode; ILibProcessPipe_Process_WriteStdIn
  to the legacy child process in fork-exec mode.

* macOS Tahoe: process key/mouse events locally when in socket mode

Upstream's MNG_KVM_KEY and MNG_KVM_MOUSE cases in kvm_mainloopinput
bail out when KVM_AGENT_FD != -1 — i.e., when the daemon is in the
unused -kvm1 socket-mode (or now our -kvmagent mode). Presumably
intended for an upstream design where the agent forwarded input
messages elsewhere instead of dispatching them locally.

That's wrong for the user-LaunchAgent architecture: the agent IS
the process that should call KeyAction / MouseAction, since it's
the only one in the right audit session and with the TCC permissions
to post CGEvents into the user's session. The system-level daemon
proxies bytes only.

Symptom before fix: with the LaunchAgent up and the daemon connecting
to it via socket, video streamed fine but every keystroke and mouse
motion was silently dropped at the case-break.

After fix: the agent handles input locally, video + keyboard + mouse
all flow end-to-end through MeshCentral's Desktop tab. Verified
empirically 2026-05-06.

* macOS Tahoe: integrate -kvmagent into -fullinstall + harden socket-mode fallback

Two follow-ups from the launchagent-split refactor's first working
deployment:

1. agent-installer.js: -fullinstall now writes the LaunchAgent plist
   with parameters: ['-kvmagent'] (was ['-kvm1'] - never wired up on
   the daemon side) and sessionTypes: ['LoginWindow', 'Aqua'] (was
   ['LoginWindow'] only - meant the agent never loaded for logged-in
   user sessions). After installing the plist, immediately bootstraps
   it into gui/<console-uid> so the agent is available without
   requiring a logout/login cycle.

2. mac_kvm.c kvm_relay_setup hardening: only fall through to the
   legacy fork-exec helper if the LaunchAgent socket file is
   genuinely absent from disk. If the socket exists but connect()
   transiently fails (e.g. agent re-accepting between sessions),
   retry up to 5 times with exponential backoff (50ms..800ms,
   ~1.55s total). Returns NULL if all retries fail rather than
   firing the broken fork-exec path that produces black frames /
   dead input under Tahoe's audit-session isolation. Adds
   <sys/stat.h> for the stat() check.

* macOS Tahoe: regenerate embedded modules with updated agent-installer.js

Run code-utils expand/shrink to refresh the auto-generated block in
microscript/ILibDuktape_Polyfills.c so the agent-installer module
embedded in the binary reflects the LaunchAgent + -kvmagent changes
made in modules/agent-installer.js earlier in this PR (commits
b658692, 38d0120, 4a8a589, d77773f).

Process used (per si458's note):

  ./meshagent_x86-64 -export \
      --expandedPath=modules_expanded \
      --filePath=microscript/ILibDuktape_Polyfills.c
  cp modules/agent-installer.js modules_expanded/agent-installer.js
  ./meshagent_x86-64 -exec \
      'require("code-utils").shrink({
         expandedPath:"modules_expanded",
         filePath:"microscript/ILibDuktape_Polyfills.c",
         modulesPath:"modules"});
       process.exit();'

Diff is bounded to the BEGIN/END AUTO-GENERATED BODY markers. Module
set is unchanged (1 net add: wget is now embedded as a real module
rather than the prior lazy require() property; the corresponding
process.arch lazy-getter remains in place outside the auto-gen
region).

---------

Co-authored-by: austin <me@austinrenfroe.com>
Co-authored-by: Austin Renfroe <austin@earenfroe.com>
PTR-inc and others added 30 commits July 22, 2026 14:26
* Add build files to .gitignore

* Remove build file
Once the log reached maxLogSize it stopped silently and didn't resume until the file was deleted (#357). Keep that as the default but write a single marker line when the cap is hit, and add two opt-in alternatives:
  logRotate=N    rotate to <log>.1 .. <log>.N at maxLogSize, then start fresh
  logTruncate=1  clear the file and keep logging instead of stopping
logRotate takes precedence if both are set.
* fix typo: integragedJavaScriptLen > integratedJavaScriptLen

* Fix -exec arg parameter length
Now uses the real size of the provided script.
ILibScratchPad is a 4096 char buffer and ILibStringCopy with a length arg of 0 also uses iLibScratchPad as a default/max size. If a larger script was provided, it got silently truncated and execution failed.

* Fix -b64exec parameter length
Use the real length of the provided script.
ILibScratchPad2 is a 64k buffer, large, but with base64 text it could still be a reachable limit.
Fix Base64 pad bits for one-byte final blocks

ILibencodeblock read in[1] even when len was 1. This caused an
out-of-bounds read and allowed four adjacent-memory bits to populate
the unused Base64 pad bits.

WebSocket client nonces are 16 bytes, so every Sec-WebSocket-Key
generation exercised this path. Strict RFC 4648 decoders reject the
resulting noncanonical encoding.

Zero the unused bits when len is 1 and add RFC 4648/RFC 6455
regression coverage.

Impact assessment:

This change does not modify the decoded nonce bytes, protocol version,
agent configuration, or server behavior. It only ensures that unused
Base64 pad bits are zero and prevents the one-byte out-of-bounds read.

Co-authored-by: Anthony Cochenour <acochenour@hopliteindustries.com>
* close `HCERTSTORE` and `HCRYPTMSG` handles in `win-authenticode-opus`

* Add missing call to `ILibDestructParserResults`

* Remove redundant `ILibMemory_Free`, already cleared in `ILibLifeTime_Remove` via `DestroyPtr`

* Add another explicit call to Duktape Garbage Collector

* Cache Win32 authenticode check across reconnect cycles

* Clean-up the authenticode cache code
This workflow defines jobs for building the MeshAgent for both arm64 and x86_64 architectures on macOS. It includes steps for checking out the code, building the agent, and uploading the artifacts.
- Introduced `.github/workflows/freebsd-build.yml` to enable FreeBSD binary builds inside a VM on a Linux runner, targeting x86_64 architecture.
- Added `.github/PULL_REQUEST_TEMPLATE.md` for standardized pull request submissions with testing and documentation guidelines.
* Add support for Wayland on Linux

The Wayland support comes from changing the way input and output works:

1. Screen output is captured via DRM (Direct Rendering Manager)
2. Keyboard/Mouse input is injected via libevdev

Both of these methods are lower level than the previous X11 code,
so they're actually capable of becoming full replacements for the X11
mechanisms, should we ever want to do that.

I've tried to make the changes minimally invasive to the existing code,
instead of a big refactor into X11 vs DRM code paths.

Known issues:
1. Logging out back to the greeter breaks the screen scraping.
   This is fixable, just requires work to figure some things out.
2. Cursor state is not reflected in the screen output

To get the greeter working will require some more surgery. I haven't
figured out how all of that session stuff works yet, but it is possible.
It's largely just an issue of the system getting confused between
Wayland/X11 states. One quite simple solution could be that once we
detect Wayland, we remain in that state for the duration of the process
life. I'm not sure if people care about dynamic switching between X11
and Wayland during a single boot - and I suspect not, so I think this
would be a simple and robust solution to get the greeter working.

If cursor state is necessary, then I can look into that. It just hasn't
felt like a priority.

I don't consider this work 100% finished. We still need to get the
greeter working, and more testing/feedback. But if possible, I'd like
to get this on a public branch so that I can start getting feedback.

This work is sponsored by touchsource.com

* Add new makefile linux package list for EGL/DRM

* Fix signature of ValidateMeshServer

This fixes a warning on modern compilers

* Add a Wayland detection mechanism when there's no Xorg/X11 on the system

Previously, we only worked on eg Ubuntu 24.04, where they still shipped
Xorg/X11. Without this change the agent does not advertise desktop
capability, and the "Desktop" tab in MeshCentral is unavailable.

* Make wayland sticky

This prevents us from switching back to X11 mode if we connect at the
greeter screen.

We may still be able to simplify the wayland detection to just the
seat0 read.

* Delete hacked-in polyfills.c code

It was already in monitor-info.js

* Add more logging and error checking to DRM/EGL code paths

Aiming to find the reason why we're failing on i915

* Fix DRM capture breakage on screen blank, fix DRM handle leak

* The screen capture now resumes automatically after a blank screen
* Fixed leaking of framebuffer handle
* Fixed spurious noisy logs on nvidia capture due to changing handles

* Add rudimentary support for rotated wayland displays

Unfortunately I haven't yet found a reliable universal way to detect
display rotation on wayland, so I'm adding an env var override, eg

For "portrait right":

MESH_KVM_ROTATION=270

By setting MESH_KVM_ROTATION to anything besides zero, we force the
treatment of the display as rotated by either 90,180,270 degrees.

I split it into it's own .c file, because I forsee this getting a bit
messy, having to support various compositors.

* Improve DRM Wayland KVM multi-monitor layout handling

Add multi-output DRM capture support and compose active scanout buffers
into a single logical desktop frame. When running under KWin, read the
session's logical output geometry and apply it to the DRM outputs so
capture and input coordinates follow the desktop monitor arrangement,
including scaled displays.

Also make verbose DRM/EGL diagnostics opt-in through MESH_KVM_DRM_DEBUG
to avoid repeated framebuffer logging during normal agent operation.

* Require Wayland client support for Linux KVM builds

Make Wayland client support mandatory for Linux KVM builds so release and CI
builds cannot silently omit the generic xdg-output monitor layout path. The DRM
backend now includes wayland-client directly, and the makefile fails early when
the wayland-client development package is unavailable.

This keeps Wayland monitor positioning and scaling behavior consistent between
build servers and deployed agents.

* Add DRM monitor selection support

Populate the existing monitor list from DRM/Wayland logical outputs and
advertise the normal multi-monitor display list in DRM mode. Allow
MNG_KVM_SET_DISPLAY to select all monitors or an individual monitor,
crop DRM capture to the selected output, and keep evdev absolute input
mapped against the full virtual desktop so pointer control remains
correct across non-origin displays.

* Implement dynamic `dlopen` loading for libdrm, libEGL, and libGLESv2

Enable runtime loading of required libraries (libdrm, libEGL, and libGLESv2) using `dlopen` to allow the agent to function without a pre-installed GL/DRM stack. Refactor the makefile to remove hard library linking while retaining headers for build compatibility. Additionally, update Wayland integration to dynamically load libwayland-client and provide fallbacks for missing libraries.

* Add display wake and broken-pipe handling improvements for DRM and KVM

Introduce logic to nudge sleeping displays (DPMS) awake during DRM desktop layout calculations and retry within a timeout. Enhance KVM pipe cleanup by canceling timers and broken-pipe handlers to prevent errors from delayed memory access. Define `KVM_DRM_DISPLAY_WAKE_TIMEOUT_MS` and implement `kvm_events_evdev_wake` for pointer-based wake functionality.

* Capture all GPUs in DRM KVM, not just one card

Open every DRM card that has an active display and merge their outputs into
one desktop, sorted by position. Each card gets its own EGL context pinned to
its DRM node so a second GPU's frames aren't imported through the wrong driver
and come out black. Also stops two cards with the same connector name (DP-1,
etc.) from landing on the same monitor.

* Update evdev pointer scaling logic to prevent double offset in single-monitor capture mode

* Add Wayland clipboard support and improve evdev CapsLock handling

Clipboard get/set only had the X11/xclip path, which cant reach the clipboard under Wayland. Added a wl-clipboard (wl-copy/wl-paste) backend that's used when a Wayland session is detected. Needs wl-clipboard installed on the target.

The evdev key path dropped the Shift modifier on typed text, so shifted symbols came through wrong ($ became 4, _ became -, etc.), and it ignored Caps Lock so the case followed the remote Caps Lock state. Sync after each transition so Shift is held while the key is sent, and read the Caps Lock LED to keep the requested case for letters.

* Update compressed clipboard module logic and source commit metadata

* Fix DRM KVM black screening from lost GPU device access after a privilege drop

The DRM capture child opens the card nodes as root, then drops to the logged-in user keeping only CAP_SYS_ADMIN. That also drops CAP_DAC_OVERRIDE, so from there access to /dev/dri/* comes down to plain group permissions. libEGL opens the render/card nodes itself during eglInitialize, which happens after the drop, so unless the session user was already in the render/video groups it failed with EACCES and the remote desktop came through black. The old X11 path never hit this because it goes through the X server socket and never touches the DRI nodes.

Add the video and render groups to the session user's supplementary set during the drop, before the setuid, so GPU conversion works regardless of which groups the user is in or how mesa is packaged. Groups the distro doesn't define are skipped.

Also mirror kvm_send_error() to stderr and move the evdev input-failure diagnostics off stdout. These only reached the MeshCentral viewer, or stdout which the systemd unit nulls, so permission failures like this were invisible in the agent's own logs.

* Install libwayland-dev in the Linux build workflow

The KVM builds need wayland-client (headers to compile linux_kvm_drm.c, and the makefile now hard-requires it), but the install step never pulled in libwayland-dev, so every KVM target failed at the wayland-client check before compiling anything. It's dlopen'd not linked, so the 64-bit dev package is enough for the -m32 build too.

* Install libdrm/EGL/GLES dev headers in the Linux build workflow

The DRM KVM path includes <EGL/egl.h>, <GLES2/gl.h> and the libdrm headers directly (they're dlopen'd at runtime but still needed to compile), so the KVM builds failed on EGL/egl.h now that they get past the wayland-client check. Add libdrm-dev, libegl-dev and libgles-dev to both jobs. Headers only, nothing links them, so the 64-bit packages cover the -m32 build too.

* Fix Wayland "Type" shift-drop on GNOME + re-embed stale clipboard modules

The evdev unicode/Type path fired shift→key→release with no gap, and GNOME (via IBus) drops the modifier when events arrive that fast, so shifted chars lost their shift ($→4, _→-, |→\, caps→lowercase). Hold the key ~20ms with Shift held and pace characters out (same as ydotool on Wayland); tunable via MESH_KVM_KEY_DELAY_MS. KWin was unaffected. Also re-embedded the clipboard + monitor-info compressed modules, which were stale and missing the Wayland (wl-clipboard) code entirely.

* Fix Wayland KVM multi-monitor layout (GNOME) and multi-plane (CCS) scanout

- Multi-monitor 0,0 on GNOME: mutter names HDMI type-A "HDMI-<id>" vs our "HDMI-A-<id>", so the exact-strcmp connector match failed and the all-or-nothing xdg-output layout fell back to raw DRM 0,0. Add kvm_drm_connector_name_equal().
- Black screen on Intel CCS: two-plane (data+metadata) scanout were rejected; import all planes through EGL so the GPU decompresses during the blit.

* Refactor Wayland session detection and Wayland greeter fallback

- Updated `user-sessions.js` to handle case-insensitive matching for 'Display Manager', recognizing additional greeters (e.g., `_ldm`).
- Enhanced `monitor-info.js` to detect Wayland sessions by introducing `kvmcheck_wayland_user` and implementing a fallback for greeter sessions (e.g., GDM/LightDM) to ensure detection before user login.
- Re-embedded the updated compressed `monitor-info` and `user-sessions` modules.

* Introduce graceful KVM session recovery during logout/user-switch

- Added runtime-dir checks and grace periods to distinguish between transient display sleep and actual session teardown in the DRM path.
- Implemented auto-recovery logic in capture child to handle unexpected exits, automatically re-derive active sessions, and re-fork capture processes, with burst-rate limiting to prevent fork storms.
- Improved robustness in session user environment handling by avoiding restarts when X server properties are unavailable in Wayland environments.

* Add rotation handling for Wayland DRM outputs and debug frame dumping

- Implemented `kvm_drm_rotation` to account for rotation transforms for Wayland outputs and logical desktop alignment.
- Added `kvm_drm_debug_dump_frame` to debug desktop orientation/layout issues by exporting PPM captures periodically.
- Updated function signatures and Wayland transform handling to support output rotation, ensuring proper compositor synchronization.
- Adjusted Makefile indentation for consistency.

* Implement deadlock-safe writes to slave->master pipe in DRM mode

- Introduced `kvm_drm_slave_write` for non-blocking, deadlock-safe writes, preventing session hangs under backpressure.
- Replaced all direct `write` calls in DRM paths with the new draining writer to ensure stream consistency.
- Added rotation handling for DRM planes via property queries, with fallback strategies for unsupported configurations.
- Improved input message handling with buffered, real-time parsing to prevent interleaving issues.
- Updated DRM initialization flow to enable universal planes and non-blocking pipes, supporting advanced configurations like plane rotation and multi-plane hardware scanouts.

* Add XKB keymap support for Wayland sessions in KVM

- Implemented `linux_kvm_xkb.c` to resolve Unicode codepoints to Wayland XKB keymap keystrokes, enabling input in non-US layouts and scripts.
- Integrated XKB with the evdev input pipeline for seamless handling of Shift, AltGr, and CapsLock modifiers.
- Updated `linux_events_evdev.c` with fallback to ASCII mapping when XKB is unavailable.

* Refactor Wayland KVM implementation for improved compatibility and reliability

- Replaced `wl_proxy_marshal_flags` with array marshallers for compatibility with libwayland < 1.20 on older systems (e.g., Debian 11 / Ubuntu 20.04).
- Added deadline-bounded alternatives to `wl_display_roundtrip` to prevent session input/output stalls in hung compositors.
- Enhanced DRM resource cleanup with `kvm_drm_close_frame_handles` to prevent GEM handle leaks.
- Improved session user privilege drops with full cleanup of supplementary groups for security.
- Introduced logging improvements for debugging Wayland and DRM behaviors.
- Adjusted Makefile to validate required development headers, ensuring smoother build workflows.
- Updated evdev F-key mappings and error output for consistent and readable diagnostic messages.

* Enhance Wayland KVM fallback and input handling

- Added fallback for compositors without `zxdg_output_manager_v1`, leveraging `wl_output` geometry and mode events as a minimal implementation for Greeter environments.
- Improved lock state synchronization for evdev keyboards by adding `kvm_events_evdev_lock_state` and `kvm_events_evdev_wait_lock_state`, ensuring accurate CapsLock, NumLock, and ScrollLock state reporting.
- Integrated Wayland active session UID derivation for display-manager greeters using `kvm_wayland_active_console_uid`.
- Updated XKB keymap handling with group-aware lookup for precise input handling in multi-layout scenarios.
- Introduced bounded timeouts for `wl-paste` and `wl-copy` to handle unresponsive compositor pipelines gracefully.
- Optimized clipboard dispatch with lifetime caching of Wayland session state for reduced probe costs.

* Improve session UID handling and environment inheritance in DRM path

- Set `HOME` to the session user's directory to fix cache warnings in Mesa.
- Trust `logind` for active session UID derivation, avoiding dead session binding.
- Implement session handover detection with console UID mismatch checks.
- Exit and re-fork on sustained UID mismatches during user switches or logouts.

* Improve Wayland socket detection and error logging in DRM/XKB paths

- Added `kvm_drm_wayland_socket_alive` to validate Wayland sockets with `connect()` checks, avoiding stale socket usage.
- Improved error logging for `wl_display_connect` failures with additional context (`errno`, `XDG_RUNTIME_DIR`, `WAYLAND_DISPLAY`, etc.).
- Updated environment preparation to scan for active Wayland sockets and prioritize live connections dynamically.
- Enhanced fallback logic for `WAYLAND_DISPLAY` environment variable handling to improve robustness in session initialization.

* Override inherited session environment variables for sudo scenarios

- Updated `XDG_RUNTIME_DIR` and `DBUS_SESSION_BUS_ADDRESS` logic to prevent reuse of parent session values under sudo, addressing permission issues with root's runtime directory.
- Added runtime directory existence checks to derive accurate session paths while retaining fallback for unsupported setups.

---------

Co-authored-by: Ben Harper <rogojin@gmail.com>
/tmp/master is unguarded. It runs every relay start as root
Guard it like /tmp/slave on line 1653
fs defaulted to "r" and "w", which on windows is a text stream.
Node has no text mode, and on posix text and binary streams are identical, so a mode string without b or t should open binary.

A text stream expands every written \n to \r\n, and a read stops at the first 0x1A byte, which it reads as an end of file.

ILibDuktape_fs_openSyncEx/readFileSync now appends b when the called without b or t.

createReadStream/createWriteStream also default to "rb"/"wb", even though openSyncEx already forces it.
The sigtable array index was used directly as the OS signal number, but sparc/mips linux don't share the generic numbering, so sigaction() hooked the wrong signal (e.g. SIGCHLD silently hooked SIGSTOP on sparc64).

Replaced the string array with a signal map of name/number pairs generated from the target's real <signal.h> macros at compile time, so it's always correct for the compiled target. The Windows table is just the generic Linux numbers used to fill process.SIGTABLE, since
Windows doesn't do POSIX signals.
)

ProcessCommand and BinaryCommand now print the MeshCommands_Binary name next to the number. The enum and the name switch get generated from one #define list, so future changes to the command list stay in sync.
Also cleaned up the debug output:
- trailing newlines removed from .log entries, missing ones added to printf.
- All controlChannelDebug tracemessages now go to both console and log.
- No duplicate output lines
- Fixed one unreachable log line.
…394)

Ensure remote desktop capture works at login screens by running the capture child as root when `consoleUid()` matches the display manager's uid, addressing issues with root-owned X server cookies. Applied in both initial capture and re-fork paths for logout/user-switch scenarios.
…#396)

Under clang/compiler-rt toolchain this failed to link on ppc64le with undefined symbol __gcc_qsub / __gcc_qmul.

ILibDuktape_WebSocket_bytesSent_ratio/bytesReceived_ratio calculated the ratio with long doubles.
Long double is not a strict defined type per platform, making it less portable.
The proper 'long double' precision would be __float128.
But it's just a ratio calculation which doesn't need this high precision at all, and in the end it returns the floored value as a JS number.

This is the only place in the source where long doubles were used.
…393)

Done because MSVC x86 (x64 has no issue) link-time code generation (v143 and v145, v142 verified
unaffected) drops the conditional part (extraLen > 0 ? header + roundup : 0) of the
ILibMemory_Init_Size macro when the sizes are not constants. So the first block
with a non-zero extraLen got 44 bytes instead of 88, and every x86 Release build
died at startup with 0xC0000374 (STATUS_HEAP_CORRUPTION). The same LTCG build also dropped two of the four ILibMemory_Size_Validate checks. Both are now static inline functions for runtime sizes. The macros stay for the constant-size callers.

Other solution would be disabling LTCG, but this costs nothing performance-wise and makes it independent of the LTCG option and prevents future issues.
Replace ((type*)ptr)[n] pointer casts with memcpy-based
ILibUnaligned_Read/Write16/32/64 helpers to avoid SIGBUS on
MIPS/ARMv5/RISC-V/SPARC.
* Add optional mTLS client certificate for the server connection

* Document the ClientCertPem option

* Use the mTLS client certificate for relay tunnels as well
Fixes 'ld-elf.so.1: Shared object "libutil.so.9" not found, required by "meshagent"'
libutil's SONAME is not stable across FreeBSD releases (.so.9 on
14.x vs .so.10 on 15.x), so a binary linked against one fails to
load on the other.
Meshagent needed libutil for only one symbol: forkpty()
- Reimplement forkpty() locally from libc POSIX primitives instead of linking -lutil.
ARCHID 33 set no CC, so it fell through to the default "CC = gcc" and the
agent was compiled against glibc.

Every other musl target already names its compiler explicitly; this one was
the exception. With CC set the same way the OpenSSL archive is built, the
agent comes out native musl:

-idirafter rather than -I is deliberate and required: musl-gcc drives the host
gcc with -nostdinc, so kernel headers are not on the include path and
<linux/limits.h> is not found. Appending the system directories after musl's
lets musl win for everything it provides while linux/* and asm/* come from the
host; -I would shadow musl's headers with glibc's.
Added musl-tools to the list of installed packages for the build process.
…ew feedback + working macOS CI (#380)

* SafeDuktape_CallMethod: guard pcall against stale stream heapptrs

When a heap-stashed object (e.g. state->writeStream) has been closed,
GC'd, or replaced between the moment its heapptr was captured and the
moment we call one of its methods, the old code went:

    duk_get_prop_string(ctx, -1, "write");   // returns undefined
    ...
    duk_pcall_method(ctx, N);                 // TypeError: ... of undefined

Because the failure originates from a native callback, the error cannot
be reached by a JS try/catch. Duktape then promotes it to a fatal, and
ILibDuktape_ScriptContainer_Engine_fatal exits the process with
ILIBCRITICALEXITMSG(254, ...).

This is the dominant crash mode on macOS 26 (Tahoe) when the KVM
LaunchAgent (`meshagent-launchagent`) restarts faster than the daemon
can clean up the relay socket (see issue #359).

Add ILibDuktape_SafePcallMethod: a thin wrapper that:
- Verifies the callable is actually a function (the Duktape pcall_method
  layout puts the callable at -(nargs + 2) and `this` at -(nargs + 1)).
- If not, drops the (callable, this, args) from the stack and routes to
  Process_UncaughtExceptionEx so the engine keeps running.
- Otherwise behaves exactly like duk_pcall_method, but also routes any
  pcall error to Process_UncaughtExceptionEx (consistent with the
  existing idiom used 100+ times in this codebase).

Apply at the four hot spots that have caused fatal-promotion crashes
in the field: ILibDuktape_HttpStream.c (server response write paths,
including chunked transfer encoding) and ILibDuktape_ReadableStream.c
(pipe chain dispatch).

(cherry picked from commit f2bfaf2)

* mac_kvm: validate peer + cap retries on Tahoe LaunchAgent socket (fix #359)

After PR #349 (a20c31f) introduced the user-LaunchAgent relay path
(/tmp/meshagent-kvm-<uid>.sock) for macOS Tahoe, the daemon's
kvm_relay_setup() wraps an AF_UNIX fd in ILibProcessPipe_Pipe and
returns it to the JS engine. The engine then takes the JS-side
stream's heapptr, calls .write() on it later from a native callback,
and if anything has closed the underlying pipe since the connect(),
the next .write() raises 'cannot read property "write" of undefined'
on the C side. That error originates from a native callback, so no
JS try/catch can catch it, and Duktape promotes it to a fatal that
calls ILibDuktape_ScriptContainer_Engine_fatal -> ILIBCRITICALEXITMSG(254).
Every 1-12 minutes the agent exits 254, launchd relaunches it, and
Remote Desktop never renders. Companion SIGSEGV (KERN_INVALID_ADDRESS
at 0x40) is the same family with the heapptr dereferenced unchecked.

The actual root cause: stat() reports the socket file as present
even after a LaunchAgent crash leaves a stale entry in /tmp, and
connect() can return 0 briefly when the agent is mid-shutdown. We
then wrap a phantom pipe.

Two fixes, layered:

1. Peer credential validation (the actual root cause fix).
   Immediately after a successful connect(), use
   getpeereid() to check the peer's uid matches the one we were
   told to connect as, and getsockopt(SOL_LOCAL, LOCAL_PEERPID) to
   fetch the peer's pid (Apple-specific since 10.4; a small
   fallback define guards older SDKs). If the peer doesn't look
   like the LaunchAgent, close the fd, treat as transient, and
   retry. The path that caused #359 was a peer that was about to
   die — its pid was still resolvable but its pipe would not
   survive the first .write().

2. Retry hardening.
   - maxRetries 5 -> 8 (total budget ~3.65s vs ~1.55s; the LaunchAgent
     can take a couple of seconds to rebind after a session change).
   - Replace 'usleep(50000 << attempt)' with a small static
     backoff table capped at 800ms. On 32-bit int, the shift
     would silently wrap to small sleeps past attempt=4. Not
     the proximate cause of #359, but the symptom was that
     high-attempt retries were happening back-to-back with
     no real backoff, increasing the chance of connecting to
     a phantom peer.
   - Add explicit 'giving up' log with the last connect() errno
     so future debugging doesn't have to reverse-engineer why
     the socket path returned NULL.
   - Treat ILibProcessPipe_Pipe_CreateFromExisting returning NULL
     as a transient retry rather than a hard failure that would
     fall through to a half-initialized pipe.

NOT changed (deliberately): the 'socket exists but unreachable ->
return NULL' policy from PR #349. Falling back to fork-exec on
Tahoe lands the helper in the wrong audit session and produces
black-screen / dead-input. Returning NULL and letting the caller
surface the error to MeshCentral is the correct behavior; the
improvement here is that, with peer validation, we no longer
return NULL *after* having wrapped a phantom pipe.

Bump MESH_VER 194 -> 195 (makefile:195) per the protocol-visible-
change policy. The peer-validation step is local to the daemon and
does not change the wire format, but the LaunchAgent fallback
semantics and retry budget do affect the agent's lifecycle behavior
and are worth flagging on the wire.

Files:
  - meshcore/KVM/MacOS/mac_kvm.c: includes, new helper
    kvm_relay_socket_validate_peer(), modified kvm_relay_setup().
  - makefile: MESH_VER 194 -> 195.

Verification: read-only review on Linux host. Build for macOS
('make macos ARCHID=16|29') requires Xcode SDK and cannot run
on this host. Reviewer must run a Mac build + a Tahoe KVM
session-replay test before merge.

Companion commit: f2bfaf2 (ILibDuktape_SafePcallMethod) which
turns the fatal-promotion crash from a hard exit(254) into a
soft uncaughtException on the rare paths where the heapptr
goes stale anyway.

(cherry picked from commit 2c68429)

* ci: add macOS build smoke test for #359

Triggered on push to the fix branch and on PRs to master that
touch any of the files modified by the fix. Builds ARCHID=16
(x86_64) on macos-13 and ARCHID=29 (arm64) on macos-14 with
'make macos', uploads the binaries as artifacts. Intentionally
narrow: it does not start the agent or attempt to reproduce the
runtime crash; the build itself is the evidence that the new
kvm_relay_socket_validate_peer() helper, the modified
kvm_relay_setup() retry loop, and the B-side
ILibDuktape_SafePcallMethod changes compile and link on real
Apple toolchains.

(cherry picked from commit eb44059)

* ci: switch x86_64 macos runner to macos-13-large (macos-13 queue saturated)

The previous macos-13 job for the x86_64 build remained queued for
30+ minutes after the arm64 (macos-14) job had already finished in
under a minute. macos-13 is the last Intel-based image and its
runner pool is more constrained than the Apple Silicon pool. Switch
to macos-13-large (same OS, more resources, often a different
runner pool) to unblock the workflow.

(cherry picked from commit 4100161)

* ci(macos): use GitHub-hosted macos-13 (not macos-13-large)

macos-13-large is a self-hosted-only label; jobs requesting it on a
GH-hosted fork stay queued forever (observed: 30+ min on x86_64
job of run 2, runner_id never assigned). Switch back to the standard
macos-13 GitHub-hosted label.

If the standard macos-13 queue is ever saturated in the future, the
only valid alternative for x86_64 is to switch to a self-hosted
runner (documented inline).

(cherry picked from commit b02635c)

* ci(macos): address review on #362 and unblock the x86_64 job

Two changes, both CI-only. No functional change to the fix itself.

1. Review feedback from @si458 on #362: drop `paths` and the branch-pinned
   `push` trigger, leaving just `pull_request: branches: [master]`, so the
   workflow matches the other build workflows in this repo and Actions
   rebuilds a binary on every push to a PR (the `pull_request` event
   re-fires on `synchronize`).

2. The x86_64 job could never have produced that binary: it targeted
   `macos-13`, the last Intel image, which GitHub is retiring. Jobs
   requesting it are not assigned a runner and get cancelled by the 24h
   queue timeout -- in @fsmw's fork that job was cancelled exactly 24h
   after it started, and upstream this PR has zero check runs to this day.

   It now cross-compiles from an Apple silicon runner, which the makefile
   already supports: ARCHID=16 builds with `CC = gcc -arch x86_64`, and
   both third-party dependencies are checked in per architecture
   (lib-jpeg-turbo/macos/osx-x86-64/libturbojpeg.a and
   openssl/libstatic/macos/osx-x86-64), so nothing in the build depends on
   the runner's own architecture. The previous comment in the workflow
   said a self-hosted runner was the only alternative; it is not, and the
   comment is corrected.

* Remove macOS PR build/test workflow per reviewer request (#380)

---------

Co-authored-by: Fernando <fsanmartin@iternova.cl>
Only setImmediate was pinned in the heap stash, so setTimeout/setInterval objects the caller didn't retain/reference could be GC'd and silently
cancelled by their finalizer. This made unreferenced setTimout/setIntervals unreliable.
Fix: Root all timers at init and release on elapse/clear.
* Build the system TLS trust store once per process, lazy where possible, shared with every SSL_CTX. Robust certpath lookup instead of shelling out to openssl for every context.

The vendor OpenSSL's compiled-in path is /usr/local/ssl, which no distribution normally uses. Every util_load_system_certs() call therefore found X509_get_default_cert_dir() pointing at a missing directory and fell back to popen("openssl version -d") — a shell, resolved through $PATH, once per SSL_CTX. The original code even carried a comment calling this a security risk.

macOS now reads the Keychain anchors directly (Security.framework), just like Windows reading the system ROOT store (default anchor set only; admin trust overrides are not consulted).

Linux has no standard mechanism, so the unofficial one is implemented:

SSL_CERT_FILE/SSL_CERT_DIR env vars first

well-known rehashed cert directories, validated, registered lazily

well-known CA bundles (loaded in full)

last resort: 'openssl version -d' via execve from absolute paths - no shell, minimal env, 3s timeout - then OpenSSL's own defaults.

Side note: stock agent to server TLS never consults this store (the server is pinned; meshcore.js disables chain verification everywhere), so even total failure isn't fatal. It only matters for custom scripts verifying against public CAs.

The real gain is one store, built once, shared by reference. On Windows (lazy by-subject ROOT-store lookup) and Linux with a rehashed dir it stays empty until a handshake requests an issuer. MacOS and bundle-only systems load all, once.

Previously every SSL_CTX built its own: a full system-store copy on Windows, the shell-out on Linux/macOS.

* Make macos 10.12 (Sierra) the minimum
Needed for functions used
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.