Repository navigation
feat: add elevated execution support to exec messages - #103
Merged
Marc-André Moreau (mamoreau-devolutions) merged 6 commits intoOct 30, 2026
Merged
Conversation
Contributor
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; |
`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>
A server that cannot grant elevation must report it in a form a client can act on without parsing platform error numbers, which mean nothing on POSIX and cannot be told apart from a future policy denial. `NOW_CODE_ELEVATION_DENIED` (0x0009) covers consent that was declined, expired, or denied by policy; `NOW_CODE_ELEVATION_UNAVAILABLE` (0x000A) covers an account with no elevated token or a style the server cannot elevate. Round-trip tests pin both languages to the same encoding. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Rename `NOW_CAP_EXEC_ELEVATE_SHELL` to `NOW_CAP_EXEC_ELEVATION`, following `IO_REDIRECTION` and `UNICODE_CONSOLE`, which name what the host supports rather than how. Clients need to answer two questions, "will it prompt" and "will I get stdio", and a mechanism-named bit forces them to keep a table of mechanisms as more arrive; a property-named baseline plus future bits that each relax one property keeps that logic flat. The spec section now states the baseline properties, when `STARTED` arrives relative to the consent wait so client timers do not fire on a slow user, that an elevated session sends no `EXEC_DATA`, which NOW status code each failure maps to, and that credential-prompt elevation runs the command as the supplied account. The .NET client also gates batch `NoExit` on 1.7, since a pre-1.7 peer would silently run `cmd /C`, and gains tests for both refusals and for the wire shape of an elevated request. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Every request type gains `with_elevated()`. The client refuses it unless the peer negotiated `NOW_CAP_EXEC_ELEVATION`, because a pre-1.7 peer ignores the flag and runs the command unelevated with nothing on the wire to show it. An elevated tracked request omits `IO_REDIRECTION` and rejects inline stdin and a timeout: it has no stdio, and its cancellation may fail, so a timeout could not be enforced. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Richard Markiewicz (thenextman)
force-pushed
the
feat/exec-elevation
branch
from
October 21, 2026 19:27
454f214 to
749b09f
Compare
…d started flag Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Marc-André Moreau (mamoreau-devolutions)
marked this pull request as ready for review
October 28, 2026 18:45
Marc-André Moreau (mamoreau-devolutions)
approved these changes
Oct 30, 2026
Marc-André Moreau (mamoreau-devolutions)
merged commit Oct 30, 2026
4a24d1e
into
master
9 checks passed
Marc-André Moreau (mamoreau-devolutions)
deleted the
feat/exec-elevation
branch
October 30, 2026 13:35
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 and both client libraries.What this adds
NOW_EXEC_FLAG_*_ELEVATEDon the run, process, shell, batch, winps and pwsh exec messages.NOW_CAP_EXEC_ELEVATION(0x0080) — advertised by servers that can run elevated exec sessions. The baseline semantics are the most restrictive a platform offers: it may prompt the interactive user for consent, and stdio is not redirected.NOW_CODE_ELEVATION_DENIED(0x0009) andNOW_CODE_ELEVATION_UNAVAILABLE(0x000A) — NOW-level status codes so a client never has to sniff Win32 numbers to learn why elevation failed.NOW_EXEC_FLAG_BATCH_NO_EXIT(0x0010) —cmd /Krather than/C. Under 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.Devolutions.NowClientand the Rustnow-client: an elevated request is rejected unless the negotiated capset advertises elevation. The .NET client also refuses batchNoExitto a pre-1.7 peer.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 do. A client learns what to expect before sending.ELEVATE_SHELL. Clients only ever need to answer two questions — will it prompt? and will I get stdio? — and a mechanism-named bit makes them keep a table of mechanisms as more arrive (a PEDM token path with no prompt and stdio; macOSosascript … with administrator privilegeswith a prompt and stdio).NOW_CAP_EXEC_ELEVATIONis the conservative baseline; a future bit relaxes exactly one property and says which. That matches the existing capset, where every bit names what the host supports rather than how.Two scoping statements in the spec 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. The spec is honest that credential-prompt elevation (a standard user typing an admin's password) runs the command as that account, because the identity sentence is what "run as" will later be built on.ELEVATION_UNAVAILABLE; a client must handle it per style.Three things the section now pins down because they otherwise become support tickets:
NOW_EXEC_STARTED_MSGis sent after elevation is granted. The UAC consent wait (up to 120 s) happens before it, so clients should start execution timers atSTARTED, not at submission.EXEC_DATAat all, and a server must ignoreIO_REDIRECTIONset alongsideELEVATED, so no client waits on a stream EOF that never comes.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 both clients refuse 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.
The Rust
now-clientadditionally rejects an elevated request carrying inline stdin or a timeout: there is no stdio to write to, and a timeout is enforced by cancellation, which may fail.Bit allocation
Each exec message has its own flag namespace; values avoid every bit already in use.
NOW_CHANNEL_CAPSET_MSG(execCapset)NOW_CAP_EXEC_ELEVATION0x0080NOW_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_ELEVATED0x0800NOW_STATUS(NOW_STATUS_ERROR_KIND_NOW)NOW_CODE_ELEVATION_DENIED0x0009NOW_STATUS(NOW_STATUS_ERROR_KIND_NOW)NOW_CODE_ELEVATION_UNAVAILABLE0x000AIncluded bug fix (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.
Verified behaviour behind the spec
The section says an elevated session SHOULD still report its real exit code despite having no stdio. Measured rather than assumed: from a Medium IL (
S-1-16-8192) unelevated caller, aShellExecuteEx/runaschild at High IL (S-1-16-12288):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_TERMINATEis untested, which is why abort stays MAY-fail in the spec.Testing
ELEVATEDon every exec style,ELEVATED/NO_EXITasserted independently and together, andEXEC_RESULTcarrying each new status code. The .NET byte arrays are the exact encodings the Rust suite asserts.now-client: an elevated request is refused without the capability and nothing reaches the peer; with it, the request carriesELEVATEDwithoutIO_REDIRECTIONand completes with the real exit code; a timeout on an elevated request is rejected.Devolutions.NowClient.Tests: the same refusal, the wire shape of an elevatedNoExitbatch, andNoExitrefused on a 1.6 peer while a plain batch still goes through.rust fmt,rust lints,rust tests,dotnet fmt,dotnet tests(net10),check typos,check locks— all green. (net9 test runs need a runtime not installed on the dev box; CI has it.)Resolved design questions
ERROR_CANCELLED(1223), which outlives the fix. Denial and consent timeout are indistinguishable at the API level, so "consent not granted" is the granularity.NO_EXITdiscovery — version-implied, matchingDETACHED(1.4). The rule as applied: flags whose support varies by host get a capability bit; flags that are pure protocol behaviour are version-implied and gated client-side.supports_exec_elevation()version helper — deliberate. 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 host-side change cannot either.Still open
policies/elevation model? This repo already has anElevationenum (Standard/Elevated),RequestedElevationandElevationRequiredunderpolicies/. Not reconciled here; "elevate this exec session, subject to PEDM policy" will eventually want them to compose, andELEVATION_DENIEDis where a policy denial would land.NOW-spec.pdfnot regenerated. The PDF is untracked.Follow-up work
ELEVATEDfor run, process, batch, winps and pwsh in one release, so "the capability does not cover every style" stays a safety valve and never something RDM users discover by running a script. The existingwin_api_wrappers::process::shell_executeis the launch path; it needsSEE_MASK_NOASYNC, and the blockingrunascall must leave the DVC message loop. MapERROR_CANCELLEDtoELEVATION_DENIEDand advertiseNOW_CAP_EXEC_ELEVATION.