uncoil is pre-release (0.x). Security fixes go into main and the most recent release.
Older builds are not patched; update instead.
| Version | Supported |
|---|---|
main |
yes |
| the latest release | yes |
| anything older | no |
Please report privately. Do not open a public issue, discussion or pull request for a security problem.
- Preferred: GitHub private vulnerability reporting, under the repository's Security tab → Report a vulnerability.
- Or email justin@justinthevoid.com with "uncoil security" in the subject.
Include the version or commit, your Windows version, the steps to reproduce, and what an attacker gains. A proof of concept helps but isn't required. Don't include HID captures that contain your keystrokes.
This is a one-maintainer project, so timelines are best effort: an acknowledgement within a week, an assessment within two, and a fix or mitigation plan agreed with you before anything is disclosed. You'll be credited in the advisory unless you'd rather not be.
uncoil is a daemon that talks raw HID to peripherals, runs all day as the logged-in user and takes commands over a named pipe. By default it runs unelevated; two optional pieces run elevated. That is where the interesting bugs live.
-
What runs elevated.
scripts/install-task.ps1registers the daemon as a logon task with run level Limited:uncoildruns as you, unelevated. Only two optional things run elevated:- the
uncoil-openrgbtask (install-task.ps1 -OpenRgb), which runsuncoild --openrgb-onceat logon: inhardwaremode it runs OpenRGB once and exits; inlivemode it keeps OpenRGB running as an SDK server for as long as OpenRGB runs (see External processes below); - the daemon itself, if it was installed with
-Elevated(the fallback for PCs where it cannot open its devices unelevated; none seen yet).
Both read
%APPDATA%\uncoil\config.json, which any program running as you can write, and the-Elevateddaemon also reads%APPDATA%\uncoil\devices\*.toml. In those two cases everything read from those files is untrusted input to a high-integrity process. Parsing bugs, path handling and anything that turns config into process launches or file writes are in scope. - the
-
Elevated writes into user folders. The daemon writes
status.json,uncoild.logand the onboard-write journalonboard-writes.jsonlunder%LOCALAPPDATA%\uncoil. Unelevated, a junction there can only send those writes where you could write anyway. In-Elevatedmode the daemon turns on the redirection-trust mitigation (ProcessRedirectionTrustPolicy: junctions made by non-administrators are not followed) and refuses to write its log, status or journal into a folder that is a reparse point, through a link, or into a file with other hard links; the refusal is reported once on stderr and the write is skipped. The--openrgb-oncetask writes only%ProgramData%\uncoil\openrgb\OpenRGB.json(live mode), inside the admin-only folder: it merges every Razer detector off and the server's host and port into whatever settings are there, writes a temporary file opened without following links and refused if it has other hard links, then renames it into place. Redirection attacks (junctions, symlinks, hard links) that still turn an elevated write into a write elsewhere are in scope. -
The control pipe. The desktop app and the
uncoilCLI talk to the daemon over\\.\pipe\uncoil(newline-delimited JSON). Its security descriptor isD:P(A;;GA;;;<user SID>)S:(ML;;NWNR;;;ME): full access for the user the daemon runs as and nobody else, with a medium integrity label (no write up, no read up) so that user's unelevated programs can still connect to an-Elevateddaemon. Remote clients are rejected (PIPE_REJECT_REMOTE_CLIENTS). The pipe is created withFILE_FLAG_FIRST_PIPE_INSTANCEand is the daemon's single-instance lock: if anything already holds the name,uncoildlogs it and exits before opening a device. The daemon serves at most 8 clients, takes request lines up to 64 KB and closes a connection that sends nothing for 5 minutes. Clients connect with identification-level impersonation only (the daemon can tell who is calling but cannot act as them), refuse a server whose process does not run as the same user, and accept replies up to 1 MB. Release builds ignoreUNCOIL_PIPE; only builds with thefakefeature let it move the pipe. Note that any program running as you can connect, so it can do what the CLI can, including writes to a device's memory (which need"write": true). Ways past the DACL, parser bugs, and ways to reach something the documented commands don't allow are in scope. Details: docs/ARCHITECTURE.md. -
Raw HID. The daemon and the reference tools send feature reports to devices. Device files are validated before use: vendor id
0x1532only, key map get/set pairs0x8C/0x0Cor0x8D/0x0Donly, known transaction ids, size limits on matrices, layouts and lists, ids of[a-z0-9-], and names without control characters. A file in the user's devices folder is experimental unless it setssupport, so its writes wait for the read-only checks. Every report sent on a read path (gets, dumps, checks, probes) must be a "get" command. Anything that lets an untrusted party choose the device, the command or its arguments (for example a crafted device file aimed at another vendor's hardware, or a pipe request that writes onboard memory without"write": true) is in scope. -
External processes. OpenRGB is off by default (
openrgb.mode, or the olderopenrgb_hardware_rainbow). Both modes run from the elevateduncoil-openrgbtask (or, in-Elevatedmode, from the daemon). The hardware hand-off has no built-in device names: it does nothing untilopenrgb.devicesinconfig.jsonlists some. Eachmatchandmodemust be a plain name: 1 to 64 letters, digits, spaces and-_.()+#&:/, not starting with-or a space and not ending with a space; other entries are skipped. It runsC:\Program Files\OpenRGB\OpenRGB.exe --noautoconnect --config %ProgramData%\uncoil\openrgb -d … -m …, and only if that folder is owned by Administrators or SYSTEM, is not a reparse point and cannot be changed by non-administrators (the installer creates it that way).tasklistandtaskkillare started by full path from the Windows system folder, andtaskkillonly closes an OpenRGB in the current session. The task reports through its exit code (Task Scheduler's last result: 0 ran and exited cleanly, 1 turned off or nothing configured, 2 failed). Search-path or argument injection issues there are in scope.Live mode: the OpenRGB SDK server has no authentication. While it runs, any program on this PC can change the motherboard, RAM and GPU lighting through it (that is OpenRGB's design, not something uncoil can add). uncoil starts it bound to 127.0.0.1 only, both on the command line (
--server --server-host 127.0.0.1 --server-port N, N fromopenrgb.live.port, 1024–65535) and asServer.default_hostin its settings; an OpenRGB too old to know--server-hostrejects the whole command line rather than listening anywhere else. Every Razer detector is turned off, so that OpenRGB never touches the Razer devices. A job object ends OpenRGB when the task stops or exits. The daemon (unelevated) connects to that port as a client and treats the server as untrusted input, because while OpenRGB is not running any local program could listen there: packets are capped at 8 MiB, at most 64 controllers are taken and one with more than 4096 LEDs is refused, names lose control characters, and serial numbers are skipped without being stored. The task marks its server as uncoil's with a named object,Global\uncoil-openrgb-server; a program running as you could fake that name, and the worst it does is hide the "OpenRGB is running" notice. Ways to make the server listen beyond 127.0.0.1, or to turn what a fake server sends into more than wrong colours, are in scope. -
Other programs on the same devices. Around every request to a Razer device the daemon takes the named mutex
Global\RazerLinkReadWriteGuardMutex(OpenRGB'sRazerDeviceGuard.cppuses the same one). uncoil creates it with default security, or opens one another program created, or runs without it. Any program running as you can hold it, which makes uncoil skip frames and fail commands with "another program is talking to … right now"; that is a nuisance, not an escalation. To spot conflicting programs, the daemon takes a process snapshot at every rescan and compares only the image names against a short list (Synapse, the Chroma SDK services, OpenRGB, SignalRGB); only the matching program's name is kept, shown or logged.
The reactive, ripple and audio meter effects read input from Windows. Because the daemon runs all day, these are held to hard rules, and any way around them is in scope:
- Key presses. Only while the active config uses
reactiveorripple(on its own or in an enabled studio layer),uncoildregisters for keyboard Raw Input (RIDEV_INPUTSINK, on a message-only window incrates/uncoil-hid/src/keys.rs). Each event's scan code is turned into a desk position on the spot (apps/uncoild/src/inputs.rs) and then dropped. Which key was pressed is never logged, written to disk, sent over the control pipe or kept as a sequence; memory holds at most 64(x, y, time)entries from the last 5 seconds, and the whole buffer is overwritten with zeros when the listener stops. A position does map back to a key on your layout, so this short buffer is sensitive; it never leaves the daemon's memory. The log records only "key listener on", "off" or "unavailable". The listener is unregistered on the config reload that stops needing it. - Audio. Only while
audio_meteris in use, the daemon reads the default playback device's peak level (IAudioMeterInformation::GetPeakValue, one number 0..1) at frame rate. No audio samples are captured, and nothing about audio is stored.
A change that logs key identities, keeps a key history, exposes presses outside the daemon, or keeps the listener running when no effect needs it is a security bug.
The daemon's logon task runs unelevated (run level Limited). Two install options add elevation:
-OpenRgbregistersuncoil-openrgb, an elevated logon task for OpenRGB (RAM lighting sits on the SMBus, which needs administrator rights), and creates the admin-only%ProgramData%\uncoil\openrgbfolder it requires. Inhardwaremode it is a one-shot; inlivemode it keeps running, with no time limit, as long as OpenRGB's server does, and stopping the task ends that OpenRGB.-Elevatedruns the daemon itself elevated, as installs did before; only for PCs where it cannot open its devices unelevated.
Installing still takes one UAC prompt, because the binary goes to %ProgramFiles%\uncoil, which only
administrators can write, so no program running as you can swap it. The installer checks the copy's SHA-256
against the source after copying and refuses an install path that runs through a junction or link. Builds
before 0.1.0 installed to %LOCALAPPDATA%\uncoil\bin; the installer no longer touches that copy and only
prints a note that it can be deleted.
- Attacks that need administrator rights or physical access to begin with.
- Malicious firmware on the peripheral itself.
- Bugs in Synapse, OpenRGB, OpenRazer or Windows. Report those upstream.
- The reference scripts in
tools/referencedoing exactly what they say (they are developer tools that write to hardware on purpose). - Denial of service that only turns the lights off.