Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,34 @@ jobs:
./build-freerdp-static.sh
make rdp-sidecar-static

# Windows ships the same sidecar, built static via vcpkg so it carries no
# loose DLLs. Cache the vcpkg-built FreeRDP so it isn't recompiled every run.
- name: Cache vcpkg FreeRDP (Windows)
if: runner.os == 'Windows'
uses: actions/cache@v4
with:
path: |
C:/vcpkg/installed
C:/vcpkg/packages
# The vcpkg install depends only on the triplet, not on our CMakeLists,
# so key on a manual marker (bump to force a rebuild) rather than file
# hashes — otherwise every CMake tweak recompiles FreeRDP (~9 min).
key: vcpkg-freerdp-x64-windows-static-v1

Comment on lines +59 to +70

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Verify floating action refs in the workflow
rg -nP 'uses:\s*[^@\s]+@v[0-9]+' .github/workflows/release.yml

# Inspect cache usage and key scoping for the Windows vcpkg cache
rg -nP 'actions/cache|key:\s*vcpkg-freerdp' .github/workflows/release.yml -C 2

Repository: Meanski/noxed

Length of output: 815


Pin actions/cache and scope cache key to release refs.

Line 61 uses actions/cache@v4 (floating tag instead of pinned commit SHA), and the cache key at line 66 is not scoped to a specific ref or release tag. This allows the same cache to be reused across different builds and branches, increasing cache-poisoning exposure in a release workflow.

Recommendations:

  • Pin the action to a specific commit: actions/cache@<commit-sha> (not a floating major version)
  • Scope the cache key to the release ref or commit to prevent unintended cache reuse
🧰 Tools
🪛 zizmor (1.25.2)

[error] 61-61: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 61-61: runtime artifacts potentially vulnerable to a cache poisoning attack (cache-poisoning): enables caching by default

(cache-poisoning)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml around lines 59 - 67, The Cache vcpkg FreeRDP
(Windows) step uses a floating tag `actions/cache@v4` instead of a pinned commit
SHA, and the cache key does not include the current ref or release information.
To fix this, pin the actions/cache action to a specific commit SHA instead of
the floating v4 tag, and modify the cache key by appending the GitHub ref or
release tag (such as using ${{ github.ref }}) to ensure the cache is scoped to
the specific release and prevent unintended cache reuse across different
branches and builds.

Source: Linters/SAST tools

- name: Build RDP sidecar (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
& "$env:VCPKG_INSTALLATION_ROOT\vcpkg.exe" install freerdp:x64-windows-static
cmake -S native/rdp-spike -B native/rdp-spike/build `
-DCMAKE_TOOLCHAIN_FILE="$env:VCPKG_INSTALLATION_ROOT\scripts\buildsystems\vcpkg.cmake" `
-DVCPKG_TARGET_TRIPLET=x64-windows-static `
-DCMAKE_BUILD_TYPE=Release
cmake --build native/rdp-spike/build --config Release
$exe = Get-ChildItem -Recurse -Filter rdp-sidecar.exe native/rdp-spike/build | Select-Object -First 1
if (-not $exe) { throw "rdp-sidecar.exe not produced by the build" }
Copy-Item $exe.FullName native/rdp-spike/rdp-sidecar.exe -Force

# Notarization uses an App Store Connect API key. electron-builder reads
# APPLE_API_KEY as a path to the .p8, so materialize it from the secret.
- name: Prepare App Store Connect API key
Expand Down Expand Up @@ -151,7 +179,27 @@ jobs:
Pop-Location
}

# Always expose the built installers on the run itself so branch/dispatch
# builds are downloadable for testing without cutting a release. Grab them
# from the run's "Artifacts" section (Actions tab → this run).
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: noxed-${{ matrix.os }}
path: |
dist/*.dmg
dist/*.zip
dist/*.exe
dist/*.AppImage
dist/*.deb
native/rdp-spike/rdp-sidecar.exe
if-no-files-found: ignore
retention-days: 7

# Only publish on a real tag push. workflow_dispatch on a branch still runs
# the full build (useful for verifying the sidecars) but skips publishing.
- name: Attach artifacts to release
if: startsWith(github.ref, 'refs/tags/')
uses: softprops/action-gh-release@v2
with:
files: |
Expand Down
6 changes: 6 additions & 0 deletions electron-builder.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ mac:
- zip
win:
icon: build/icon.png
# Ship the self-contained RDP sidecar (built via native/rdp-spike/CMakeLists.txt
# with vcpkg's x64-windows-static FreeRDP). Lands at resources/rdp-sidecar.exe —
# see sidecarPath() in ipc/rdp.ts and the win32 check in verify-sidecar.js.
extraResources:
- from: native/rdp-spike/rdp-sidecar.exe
to: rdp-sidecar.exe
target:
- nsis
linux:
Expand Down
48 changes: 48 additions & 0 deletions native/rdp-spike/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Windows build for the RDP sidecar.
#
# macOS builds the sidecar via the Makefile + build-freerdp-static.sh (a trimmed
# from-source static FreeRDP). On Windows the easiest self-contained path is
# vcpkg's static triplet, which ships static FreeRDP 3 plus its transitive deps
# (OpenSSL, zlib, ...). Configure with the vcpkg toolchain so find_package
# resolves everything:
#
# vcpkg install freerdp:x64-windows-static
# cmake -S native/rdp-spike -B native/rdp-spike/build \
# -DCMAKE_TOOLCHAIN_FILE=<vcpkg>/scripts/buildsystems/vcpkg.cmake \
# -DVCPKG_TARGET_TRIPLET=x64-windows-static -DCMAKE_BUILD_TYPE=Release
# cmake --build native/rdp-spike/build --config Release
#
# Produces rdp-sidecar.exe, which electron-builder bundles via win.extraResources.
cmake_minimum_required(VERSION 3.16)
project(rdp_sidecar C)

set(CMAKE_C_STANDARD 11)

# Link the CRT statically so the .exe doesn't depend on the VC++ redistributable.
if(MSVC)
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
endif()

# FreeRDP 3 from vcpkg. The CONFIG packages export imported targets that carry
# their static transitive dependencies, so we don't enumerate OpenSSL/zlib here.
find_package(FreeRDP CONFIG REQUIRED)
find_package(FreeRDP-Client CONFIG REQUIRED)
find_package(WinPR CONFIG REQUIRED)

add_executable(rdp-sidecar sidecar.c)
set_target_properties(rdp-sidecar PROPERTIES OUTPUT_NAME rdp-sidecar)

target_link_libraries(rdp-sidecar PRIVATE freerdp freerdp-client winpr)

# FreeRDP's vcpkg config pulls in transitive static deps (e.g. cjson) referenced
# by bare library name rather than a full path, so make the vcpkg static lib dir
# searchable for the linker.
if(DEFINED VCPKG_INSTALLED_DIR AND DEFINED VCPKG_TARGET_TRIPLET)
target_link_directories(rdp-sidecar PRIVATE
"${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/lib")
endif()

if(WIN32)
# Sockets + crypto/security backends FreeRDP/WinPR pull in on Windows.
target_link_libraries(rdp-sidecar PRIVATE ws2_32 crypt32 secur32 winmm bcrypt)
endif()
179 changes: 174 additions & 5 deletions native/rdp-spike/sidecar.c
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,19 @@
#include <stdlib.h>
#include <string.h>

#ifdef _WIN32
#include <winsock2.h>
#include <io.h>
#include <fcntl.h>
#endif

#include <freerdp/freerdp.h>
#include <freerdp/client.h>
#include <freerdp/gdi/gdi.h>
#include <freerdp/codec/color.h>
#include <freerdp/input.h>
#include <winpr/synch.h>
#include <winpr/thread.h>
#include <winpr/wlog.h>

typedef struct
Expand All @@ -46,6 +54,106 @@ typedef struct
size_t packedCap;
} SidecarContext;

/*
* Input injection. The parent process (src/main/ipc/rdp.ts) writes fixed 8-byte
* little-endian messages to our stdin; a reader thread pushes them onto a small
* ring queue and signals g_inputEvent, which the main event loop waits on
* alongside FreeRDP's own handles. The loop drains the queue and calls the
* FreeRDP input functions on the main thread (the transport isn't thread-safe).
*
* Wire message (8 bytes):
* u8 type 1=mouse, 2=scancode key, 3=unicode key
* u8 _pad
* u16 flags PTR_FLAGS_* (mouse) or KBD_FLAGS_* (keyboard)
* u16 a mouse x, or key code (scancode / unicode unit)
* u16 b mouse y (0 for keys)
*
* Keeping the sidecar generic — it just forwards flags/codes — means all the
* key-mapping lives in the renderer where it's easy to iterate.
*/
typedef struct
{
UINT8 type;
UINT16 flags;
UINT16 a;
UINT16 b;
} InputMsg;

#define INPUT_QUEUE_CAP 512
static CRITICAL_SECTION g_inputLock;
static InputMsg g_inputQueue[INPUT_QUEUE_CAP];
static int g_inputHead = 0;
static int g_inputTail = 0;
static HANDLE g_inputEvent = NULL;

static UINT16 read_u16_le(const UINT8* p)
{
return (UINT16)(p[0] | ((UINT16)p[1] << 8));
}

static DWORD WINAPI input_reader_thread(LPVOID arg)
{
(void)arg;
UINT8 buf[8];
for (;;)
{
/* Block until a full 8-byte message is available; stop on EOF/short read
* (parent closed stdin, i.e. the session is going away). */
if (fread(buf, 1, sizeof(buf), stdin) != sizeof(buf))
break;

InputMsg msg;
msg.type = buf[0];
msg.flags = read_u16_le(buf + 2);
msg.a = read_u16_le(buf + 4);
msg.b = read_u16_le(buf + 6);

EnterCriticalSection(&g_inputLock);
int next = (g_inputTail + 1) % INPUT_QUEUE_CAP;
if (next != g_inputHead) /* drop if full rather than block the reader */
{
g_inputQueue[g_inputTail] = msg;
g_inputTail = next;
}
LeaveCriticalSection(&g_inputLock);
SetEvent(g_inputEvent);
}
return 0;
}

static void drain_input(rdpContext* context)
{
rdpInput* input = context->input;
for (;;)
{
InputMsg msg;
EnterCriticalSection(&g_inputLock);
if (g_inputHead == g_inputTail)
{
LeaveCriticalSection(&g_inputLock);
break;
}
msg = g_inputQueue[g_inputHead];
g_inputHead = (g_inputHead + 1) % INPUT_QUEUE_CAP;
LeaveCriticalSection(&g_inputLock);

switch (msg.type)
{
case 1:
freerdp_input_send_mouse_event(input, msg.flags, msg.a, msg.b);
break;
case 2:
freerdp_input_send_keyboard_event(input, msg.flags, (UINT8)msg.a);
break;
case 3:
freerdp_input_send_unicode_keyboard_event(input, msg.flags, msg.a);
break;
default:
break;
}
}
}

static void write_u32_le(BYTE* p, UINT32 v)
{
p[0] = (BYTE)(v & 0xFF);
Expand Down Expand Up @@ -201,6 +309,23 @@ static int sidecar_entry(RDP_CLIENT_ENTRY_POINTS* pEntryPoints)

int main(int argc, char* argv[])
{
#ifdef _WIN32
/* Windows opens stdout in text mode, which translates every \n to \r\n and
* would corrupt our binary frame stream (silent pixel desync). Force binary
* before any frame is written; bail out if it fails rather than ship garbage. */
if (_setmode(_fileno(stdout), _O_BINARY) == -1)
{
fprintf(stderr, "[sidecar] failed to set stdout to binary mode\n");
return 1;
}
/* stdin carries binary 8-byte input messages; text mode would mangle them. */
if (_setmode(_fileno(stdin), _O_BINARY) == -1)
{
fprintf(stderr, "[sidecar] failed to set stdin to binary mode\n");
return 1;
}
#endif
Comment thread
coderabbitai[bot] marked this conversation as resolved.

if (argc < 4 || argc > 6)
{
fprintf(stderr, "usage: %s <host> <port> <user> [width] [height]\n", argv[0]);
Expand All @@ -220,9 +345,10 @@ int main(int argc, char* argv[])
fprintf(stderr, "[sidecar] failed to read password from stdin\n");
return 2;
}
/* Remove trailing newline */
/* Remove trailing newline (and CR, in case stdin is binary on Windows) */
size_t len = strlen(pass);
if (len > 0 && pass[len - 1] == '\n') pass[len - 1] = '\0';
while (len > 0 && (pass[len - 1] == '\n' || pass[len - 1] == '\r'))
pass[--len] = '\0';

quiet_wlog_to_stderr();

Expand All @@ -236,12 +362,31 @@ int main(int argc, char* argv[])
return 1;
}

#ifdef _WIN32
/* Winsock must be initialized before any name resolution / socket call.
* FreeRDP's own Windows client does this in its global init; without it
* getaddrinfo fails and freerdp_connect reports DNS_NAME_NOT_FOUND even for
* a perfectly valid host. */
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
{
fprintf(stderr, "[sidecar] WSAStartup failed\n");
freerdp_client_context_free(context);
return 1;
}
#endif

rdpSettings* settings = context->settings;
freerdp_settings_set_string(settings, FreeRDP_ServerHostname, host);
freerdp_settings_set_uint32(settings, FreeRDP_ServerPort, port);
freerdp_settings_set_string(settings, FreeRDP_Username, user);
freerdp_settings_set_string(settings, FreeRDP_Password, pass);
freerdp_settings_set_bool(settings, FreeRDP_IgnoreCertificate, FALSE);
/* Internal RDP hosts almost always present a self-signed certificate (mstsc
* just prompts the user to trust it). This is a view-only tool, so accept the
* cert automatically rather than failing the TLS handshake with
* ERRCONNECT_TLS_CONNECT_FAILED (0x00020008). */
freerdp_settings_set_bool(settings, FreeRDP_IgnoreCertificate, TRUE);
freerdp_settings_set_bool(settings, FreeRDP_AutoAcceptCertificate, TRUE);
freerdp_settings_set_uint32(settings, FreeRDP_DesktopWidth, width);
freerdp_settings_set_uint32(settings, FreeRDP_DesktopHeight, height);
freerdp_settings_set_uint32(settings, FreeRDP_ColorDepth, 32);
Expand All @@ -256,32 +401,56 @@ int main(int argc, char* argv[])
goto cleanup;
}

/* Start the stdin input reader now that we're connected. The auto-reset
* event wakes the main loop whenever input arrives so it can be flushed to
* the (single-threaded) FreeRDP transport. */
InitializeCriticalSection(&g_inputLock);
g_inputEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
HANDLE readerThread = NULL;
if (g_inputEvent)
readerThread = CreateThread(NULL, 0, input_reader_thread, NULL, 0, NULL);
else
fprintf(stderr, "[sidecar] input disabled: failed to create event\n");

while (!freerdp_shall_disconnect_context(context))
{
HANDLE handles[64];
DWORD count = freerdp_get_event_handles(context, handles, 64);
/* Reserve one slot for the input event. */
DWORD count = freerdp_get_event_handles(context, handles, 63);
if (count == 0)
{
fprintf(stderr, "[sidecar] failed to get event handles\n");
rc = 1;
break;
}

DWORD status = WaitForMultipleObjects(count, handles, FALSE, INFINITE);
DWORD total = count;
if (g_inputEvent)
handles[total++] = g_inputEvent;

DWORD status = WaitForMultipleObjects(total, handles, FALSE, INFINITE);
if (status == WAIT_FAILED)
{
fprintf(stderr, "[sidecar] wait failed\n");
rc = 1;
break;
}

if (g_inputEvent)
drain_input(context);

if (!freerdp_check_event_handles(context))
break;
}

freerdp_disconnect(instance);
if (readerThread)
CloseHandle(readerThread);

cleanup:
freerdp_client_context_free(context);
#ifdef _WIN32
WSACleanup();
#endif
return rc;
}
Loading
Loading