feat: add elevated execution support to exec messages - #103
Draft
Richard Markiewicz (thenextman) wants to merge 2 commits into
Draft
feat: add elevated execution support to exec messages #103Richard Markiewicz (thenextman) wants to merge 2 commits into
Richard Markiewicz (thenextman) wants to merge 2 commits into
Conversation
`NowExecRunMsg::decode_from_body` discarded the header flags and rebuilt them from payload content, making it the only exec message that did not preserve flags on decode. Every other exec message seeds its flags from `from_bits_retain(header.flags)`. Any flag on NOW_EXEC_RUN_MSG other than DIRECTORY_SET was therefore silently dropped by the decoder, so a server could not observe an option the client had set. DIRECTORY_SET is still derived from the payload rather than trusted from the header, because peers older than v1.1 omit the directory field entirely. No test accompanies this on its own: NOW_EXEC_RUN_MSG has no second flag to assert against yet. Coverage arrives with the first one added. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Updates NOW-PROTO to v1.7, adding the ability for a client to request that an exec session run with elevated privileges. - Add `NOW_EXEC_FLAG_*_ELEVATED` to the run, process, shell, batch, winps and pwsh exec messages. The flag expresses intent only and never names a mechanism, so the server remains free to change how it elevates. It raises the privileges of the identity that would otherwise run the command and carries no credentials; running a command as a different user is a separate concern for a separate field. - Add `NOW_CAP_EXEC_ELEVATE_SHELL`, advertised by servers that elevate through the platform shell (consent prompt, no stdio redirection). It is the only elevation capability defined: a mechanism with different observable properties is expected to arrive as an additional capability rather than by redefining this one, so a client always learns what to expect before it sends. - Refuse an elevated request in `NowClient` unless the negotiated capset advertises elevation. A server older than 1.7 does not know the flag and would execute the command without elevation, and the caller has no way to detect that, so the request is rejected locally rather than downgraded silently. - Specify the mechanism-dependent semantics in a new "Elevated Execution" section: how elevation is requested, what it does and does not mean, that a server must fail rather than silently execute unelevated, that capability negotiation is the only reliable signal because older servers ignore the flag, and what the shell mechanism implies for stdio, result reporting and abort. - Add `NOW_EXEC_FLAG_BATCH_NO_EXIT` (`cmd /K` rather than `/C`), so an elevated batch session, which has no stdio, can still leave its console open for the user to read. Must be ignored when the session redirects stdio. Exposed on `ExecBatchParams` alongside the existing winps and pwsh `NoExit` options. The round-trip tests added here are also the first coverage of the RUN decoder fix in the preceding commit: ELEVATED is the first NOW_EXEC_RUN_MSG flag that would have been dropped by it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot started reviewing on behalf of
Richard Markiewicz (thenextman)
September 9, 2026 19:21
View session
There was a problem hiding this comment.
🟡 Changes recommended
Batch NoExit is not gated against pre-1.7 servers, allowing silent fallback to cmd /C.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds NOW-PROTO 1.7 elevated execution support across Rust and .NET implementations.
Changes:
- Adds elevated-execution flags and shell-elevation capability negotiation.
- Adds batch
NO_EXITsupport and fixes Rust run-flag decoding. - Updates wire-format tests, client APIs, and protocol documentation.
File summaries
| File | Description |
|---|---|
protocols/rust/now-proto-testsuite/tests/proto/exec.rs |
Tests new exec flags. |
protocols/rust/now-proto-testsuite/tests/proto/channel.rs |
Updates version snapshots. |
protocols/rust/now-proto-pdu/src/exec/win_ps.rs |
Adds WinPS elevation. |
protocols/rust/now-proto-pdu/src/exec/shell.rs |
Adds shell elevation. |
protocols/rust/now-proto-pdu/src/exec/run.rs |
Adds elevation and preserves decoded flags. |
protocols/rust/now-proto-pdu/src/exec/pwsh.rs |
Adds pwsh elevation. |
protocols/rust/now-proto-pdu/src/exec/process.rs |
Adds process elevation. |
protocols/rust/now-proto-pdu/src/exec/batch.rs |
Adds elevation and no-exit flags. |
protocols/rust/now-proto-pdu/src/channel/capset.rs |
Adds capability and protocol version. |
protocols/dotnet/Devolutions.NowProto/src/NowProtoVersion.cs |
Updates protocol version. |
protocols/dotnet/Devolutions.NowProto/src/Messages/NowMsgExecWinPs.cs |
Adds WinPS elevation serialization. |
protocols/dotnet/Devolutions.NowProto/src/Messages/NowMsgExecShell.cs |
Adds shell elevation serialization. |
protocols/dotnet/Devolutions.NowProto/src/Messages/NowMsgExecRun.cs |
Adds run elevation serialization. |
protocols/dotnet/Devolutions.NowProto/src/Messages/NowMsgExecPwsh.cs |
Adds pwsh elevation serialization. |
protocols/dotnet/Devolutions.NowProto/src/Messages/NowMsgExecProcess.cs |
Adds process elevation serialization. |
protocols/dotnet/Devolutions.NowProto/src/Messages/NowMsgExecBatch.cs |
Adds batch elevation and no-exit serialization. |
protocols/dotnet/Devolutions.NowProto/src/Capabilities/NowCapabilityExec.cs |
Defines shell-elevation capability. |
protocols/dotnet/Devolutions.NowProto.Tests/src/NowExecStyles.cs |
Tests new .NET wire encodings. |
protocols/dotnet/Devolutions.NowProto.Tests/src/MsgChannel.cs |
Updates version snapshots. |
protocols/dotnet/Devolutions.NowClient/src/NowClient.cs |
Rejects unsupported elevation requests. |
protocols/dotnet/Devolutions.NowClient/src/ExecWinPsParams.cs |
Exposes WinPS elevation option. |
protocols/dotnet/Devolutions.NowClient/src/ExecShellParams.cs |
Exposes shell elevation option. |
protocols/dotnet/Devolutions.NowClient/src/ExecRunParams.cs |
Exposes run elevation option. |
protocols/dotnet/Devolutions.NowClient/src/ExecPwshParams.cs |
Exposes pwsh elevation option. |
protocols/dotnet/Devolutions.NowClient/src/ExecProcessParams.cs |
Exposes process elevation option. |
protocols/dotnet/Devolutions.NowClient/src/ExecBatchParams.cs |
Exposes elevation and no-exit options. |
protocols/dotnet/Devolutions.NowClient/src/AExecParams.cs |
Tracks elevation intent. |
protocols/docs/NOW-spec.md |
Documents NOW-PROTO 1.7 semantics. |
Review details
- Files reviewed: 28/28 changed files
- Comments generated: 1
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+61
to
+64
| public ExecBatchParams NoExit(bool enable = true) | ||
| { | ||
| _noExit = enable; | ||
| return this; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Updates NOW-PROTO to v1.7, adding the ability for a client to request that an exec session run with elevated privileges.
RDM has had a "Run as administrator" checkbox in Execute script via Devolutions Agent since the legacy RDM Jump days, where it worked — the receiving RDM ran
ShellExecutewith therunasverb. The jump transport was removed in RDMW-21914 and the NOW exec path has no way to carry the intent, so the checkbox has silently done nothing since. This PR adds the protocol half.What this adds
NOW_EXEC_FLAG_*_ELEVATEDon the run, process, shell, batch, winps and pwsh exec messages.NOW_CAP_EXEC_ELEVATE_SHELL(0x0080) - advertised by servers that elevate through the platform shell: may prompt the interactive user for consent, and cannot redirect stdio.NOW_EXEC_FLAG_BATCH_NO_EXIT(0x0010) -cmd /Krather than/C. Under shell elevation there is no stdio, so a console left open is the only way the user sees output. winps/pwsh already hadNO_EXIT; batch did not. Must be ignored when the session redirects stdio, where the hidden interpreter would never exit.NowClient: an elevated request is rejected unless the negotiated capset advertises elevation.Design: intent, not mechanism
The part worth reviewing. Three rules, all aimed at letting a better elevation mechanism land later without a breaking change:
ELEVATED, neverRUNAS. A client cannot select a mechanism, so a server is free to change how it elevates without any client change.execCapset, the same wayUNICODE_CONSOLEandIO_REDIRECTIONalready are. A client learns what elevation will cost it before sending.Only one elevation capability is defined. A mechanism with different observable properties (no prompt, stdio preserved) is expected to arrive as an additional capability rather than by redefining this one. An earlier revision of this branch also defined a reserved
NOW_CAP_EXEC_ELEVATE_TOKEN; it was dropped, because a defined-but-meaningless constant invites clients to branch on it as a property proxy — ours already had one doing exactly that, untestably — and reserving a bit protects nothing when eight are free.Two scoping statements in the spec exist to keep future work unblocked:
ELEVATEDraises the privileges of the identity that would otherwise run the command. It selects no identity and carries no credentials, so "run as another user" has clean ground for a separate field.runfirst.Why the client refuses locally
The spec requires a server to fail rather than execute unelevated — but that only binds servers at 1.7 or later. A pre-1.7 server does not know the flag: it ignores the unknown bit and runs the command with normal privileges, with nothing on the wire to distinguish that from success. The server-side MUST cannot protect that case, so
NowClientrefuses the request when no elevation capability was advertised. Silent downgrade is the exact defect this PR exists to remove; it would be perverse to reintroduce it in the client.Bit allocation
Each exec message has its own flag namespace; values avoid every bit already in use.
NOW_EXEC_RUN_MSGNOW_EXEC_FLAG_RUN_ELEVATED0x0002NOW_EXEC_PROCESS_MSGNOW_EXEC_FLAG_PROCESS_ELEVATED0x0008NOW_EXEC_SHELL_MSGNOW_EXEC_FLAG_SHELL_ELEVATED0x0008NOW_EXEC_BATCH_MSGNOW_EXEC_FLAG_BATCH_ELEVATED0x0008NOW_EXEC_BATCH_MSGNOW_EXEC_FLAG_BATCH_NO_EXIT0x0010NOW_EXEC_WINPS_MSG/NOW_EXEC_PWSH_MSGNOW_EXEC_FLAG_PS_ELEVATED0x0800Included bug fix (
c9c788f, separate commit)NowExecRunMsg::decode_from_bodydiscarded the header flags and rebuilt them from payload content, making it the only exec message that did not preserve flags on decode. Any flag added toNOW_EXEC_RUN_MSG— includingELEVATED— was silently dropped, so a server would never have observed the request.DIRECTORY_SETis still derived from the payload, because peers older than v1.1 omit the directory field entirely.Split out so it gets its own Bug Fixes changelog entry: it is a latent bug in released 0.4.4, independent of this feature. It compiles and passes on its own; the round-trip tests in
454f214are its first coverage, sinceELEVATEDis the first RUN flag that would have been dropped.Verified behaviour behind the spec
The section says a shell-elevated session SHOULD still report its real exit code despite having no stdio. That was an assumption, so I measured it rather than shipping it on faith. From a Medium IL (
S-1-16-8192) unelevated caller, aShellExecuteEx/runaschild at High IL (S-1-16-12288):WaitForExit : ok [SYNCHRONIZE granted]
Id : ok [PROCESS_QUERY_LIMITED_INFORMATION granted]
ExitCode : 42 [true value, not a default 0]
MIC's no-read-up rule is enforced when opening a handle; here the handle is created for us by the AppInfo service, so it does not apply at use.
PROCESS_QUERY_LIMITED_INFORMATIONexists for exactly this. The legacy jump path also depended on the wait half in production for years.PROCESS_TERMINATEis untested, which is why abort stays MAY-fail in the spec.Testing
ELEVATEDis asserted for every exec style, andELEVATED/NO_EXITare asserted independently as well as together — the combined0x0018mask alone would stay green if the two constants were swapped.rust fmt,dotnet fmt,check typos,rust tests(62),rust lints,dotnet tests(61),dotnet build,check locks— all green.Open questions
NOW_STATUScarries the failure fine, but there is no NOW-level code for "elevation was requested and could not be granted", so a client must sniff Win32 numbers — brittle, and meaningless on POSIX. Candidates, 0x0009+ being free:NOW_CODE_ELEVATION_DENIED(consent not granted) andNOW_CODE_ELEVATION_UNAVAILABLE(no elevated token for this account — UAC disabled, or a standard user).My recommendation is to add them now. The protocol change is additive at any time, but the cost of deferring lands in clients: anything shipped in the interim hardcodes
ERROR_CANCELLED(1223) and that code outlives the fix. For what it's worth, denial and consent timeout are indistinguishable at the API level — both surface as 1223 — and I think "consent not granted" is the right granularity, but it should be a stated decision.Should
NO_EXITget a capability bit? It currently has none and is implied by version 1.7, whileELEVATEDin the same bump is capability-discovered. Two flags, two discovery rules, no stated principle. Leaving it version-implied matchesDETACHEDexactly (added in 1.4, no bit, gated client-side), which is why I left it — but if you want the rule written down, the honest version is: flags whose support varies by host capability get bits, flags that are pure protocol behaviour are version-implied.Should this reconcile with the
policies/elevation model? This repo already has anElevationenum (Standard/Elevated),RequestedElevationandElevationRequiredunderpolicies/. I did not reconcile the two, so there are now two elevation vocabularies in one repo. "Elevate this exec session, subject to PEDM policy" will eventually want them to compose.Known gaps, accepted deliberately
NOW-spec.pdfnot regenerated. `. The PDF is untracked.supports_exec_elevation()version helper, deviating from the 1.6 precedent that addedsupports_exec_unicode_console(). Elevation must be gated on the capability, not the version: a 1.7 Linux agent cannotrunas, and a 1.7 Windows agent that has not shipped the server-side change cannot either. Both would answertrue. A version helper is the more discoverable API and would invite exactly the bug the capability prevents.NowClientunit test for the refusal path — there is noDevolutions.NowClienttest project, and constructing a client needs a transport.Follow-up work
ELEVATEDinprocess_exec_run(verb selection; the smallest useful increment), advertiseELEVATE_SHELL, and map declined consent to a distinct status.win_api_wrappers::process::shell_executeso elevated sessions still report a real exit code.