Skip to content

Fix Start-Job failing under WDAC Constrained Language audit mode (#25109)#27703

Open
jagilber wants to merge 3 commits into
PowerShell:masterfrom
jagilber:fix/start-job-appcontrol-audit-mode-25109
Open

Fix Start-Job failing under WDAC Constrained Language audit mode (#25109)#27703
jagilber wants to merge 3 commits into
PowerShell:masterfrom
jagilber:fix/start-job-appcontrol-audit-mode-25109

Conversation

@jagilber

Copy link
Copy Markdown

Summary

Allow Start-Job to run under an App Control for Business (WDAC) policy in audit
mode, instead of failing with a terminating language-mode error.

Under an App Control for Business (WDAC) policy in audit mode, PowerShell 7
launches with the banner [Constrained Language AUDIT Mode : No Restrictions]
and then fails any Start-Job with:

Cannot start job. The language mode for this session is incompatible with the system-wide language mode.

Start-ThreadJob and New-PSSession are unaffected. This is issue #25109.

The out-of-process job preflight guard in
StartJobCommand.CreateHelpersForSpecifiedComputerNames() blocks the job whenever
the parent is ConstrainedLanguage and the system lockdown policy is anything other
than Enforce
. That predicate wrongly captures the WDAC audit state
(SystemEnforcementMode.Audit), which is meant to log, not block.

Root cause

SystemEnforcementMode has three states
(src/System.Management.Automation/security/wldpNativeMethods.cs):

public enum SystemEnforcementMode { None = 0, Audit = 1, Enforce = 2 }

The guard (StartJob.cs, ~line 650):

if ((Context.LanguageMode == PSLanguageMode.ConstrainedLanguage) &&
    (SystemPolicy.GetSystemLockdownPolicy() != SystemEnforcementMode.Enforce) &&
    ((ScriptBlock != null) || (InitializationScript != null)))

The guard was written for a two-state world (manual constrain vs. enforce). Its
intent is to stop a ConstrainedLanguage parent from launching a FullLanguage
out-of-process child that would escape the CL security boundary. That is only a
real risk when the constraint is local/manual and the system has no lockdown at
all
(None). Under Enforce, the child process is itself locked down; under
Audit, the child independently calls WldpGetLockdownPolicy, enters the same
constrained-audit state, and audit is log-only. Both are consistent, but the
!= Enforce test lumps Audit in with None and throws.

Why a genuine audit policy reaches this guard at all (the keystone):
Utils.EnforceSystemLockDownLanguageMode (engine/Utils.cs) maps the audit state
into ConstrainedLanguage for the session itself —

case SystemEnforcementMode.Audit:
    switch (context.LanguageMode)
    {
        case PSLanguageMode.FullLanguage:
            // Set to ConstrainedLanguage mode.  But no restrictions are applied in audit mode
            // and only audit messages will be emitted to logs.
            context.LanguageMode = PSLanguageMode.ConstrainedLanguage;
            break;
    }
    break;

So a real system-wide WDAC audit policy — with no manual language-mode change —
is sufficient to satisfy the guard's LanguageMode == ConstrainedLanguage clause
(this is the [Constrained Language AUDIT Mode : No Restrictions] banner). The
out-of-process child pwsh runs this identical path at its own startup, so it
enters the same constrained-audit state and parent/child modes stay consistent —
there is no boundary to escape, which is why allowing the job is correct. (This is
also why no LogWDACAuditMessage branch belongs here: unlike the
CompiledScriptBlock dot-source path, Start-Job with a script block is permitted
under Enforce, so there is no enforcement-restricted operation to audit.)

The enum even carries a maintainer note that any code consuming it assumes
"anything but Enforce means the script is allowed" and that new states require
reviewing all callers — this call site was not updated for the audit state.

Fix

Fire the guard only when the system reports no lockdown at all:

-                (SystemPolicy.GetSystemLockdownPolicy() != SystemEnforcementMode.Enforce) &&
+                (SystemPolicy.GetSystemLockdownPolicy() == SystemEnforcementMode.None) &&
System policy Before After
Enforce allowed (child is locked down) allowed (unchanged)
Audit blocked (bug) allowed / log-only (fixed)
None blocked blocked (manual CL boundary preserved)

The existing regression test that sets ConstrainedLanguage with no system
lockdown and expects CannotStartJobInconsistentLanguageMode
(ConstrainedLanguageRestriction.Tests.ps1 ~line 221) still passes, because that
scenario is None.

Tests

Added an audit-mode regression test asserting Start-Job succeeds when the system
lockdown policy resolves to Audit (simulated via the __PSLockdownPolicy debug
hook with the UMCI audit flag 0x8). See the patch.

Note: the existing Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode
helper simulates enforce; there is no audit switch. The added test uses the
documented __PSLockdownPolicy machine env-var debug hook that
GetDebugLockdownPolicy maps to SystemEnforcementMode.Audit. Maintainers may
prefer to extend the testing-support cmdlet with an explicit audit switch.

Related / out of scope

The sibling guard CannotCreateRunspaceInconsistentState
(RemotingErrorIdStrings.resx) uses the same two-state assumption on the runspace
creation path and likely warrants the same treatment. Flagged for maintainer
direction; not changed here to keep this fix focused on #25109.

Files changed

  • src/System.Management.Automation/engine/remoting/commands/StartJob.cs — guard predicate + comment.
  • test/powershell/Modules/Microsoft.PowerShell.Security/ConstrainedLanguageRestriction.Tests.ps1 — audit-mode regression test.

Bug impact

Impact documentation for this PR, following the format used for case 2607140010002081.

Component: PowerShell 7 (Microsoft.PowerShell.Commands.StartJobCommand, out-of-process job creation)
Affected versions: Confirmed 7.5.5 and 7.6.3; issue #25109 also reports 7.5.0. Present on current master.
Trigger: Any active WDAC / App Control for Business policy that resolves to audit mode (WLDP_LOCKDOWN_UMCIAUDIT_FLAG), which forces interactive sessions into ConstrainedLanguage while system enforcement is Audit.
Defect class: Functional regression / correctness — audit mode incorrectly enforced. Terminating error, not silent.

Customer-visible impact

  • Start-Job { ... } fails 100% of the time with CannotStartJobInconsistentLanguageMode on any machine where WDAC is in audit mode. Audit mode is specifically the state organizations use to evaluate an App Control policy before enforcing it — so the break hits precisely the fleets doing safe, staged WDAC rollouts.
  • The failure is a hard terminating error at job-preflight, before the job script block runs. { "Hello" } and any real workload fail identically; script complexity is irrelevant.
  • Any automation, module, scheduled task, CI step, or interactive workflow that relies on Start-Job breaks. Background parallelism via Start-Job is unavailable on affected devices.
  • Misleading diagnosis: the error text points at "language mode … incompatible with the system-wide language mode," steering customers toward Intune/AppLocker/WDAC policy troubleshooting even though the policy is behaving as designed (audit = log-only). Time is lost chasing a policy misconfiguration that does not exist.

What is and is not affected

Behavior Assessment
Start-Job (out-of-process) under WDAC audit Broken — terminating error
Start-Job under WDAC enforce Works (child process is locked down)
Start-Job with no lockdown, session manually set to CL Correctly blocked (intended security boundary)
Start-ThreadJob (in-process) Not affected
New-PSSession / remoting Not affected
Audit logging / WDAC policy evaluation itself Not affected; policy still audits as designed
Security boundary Not weakened by the fix; None case still blocked, Enforce/Audit children remain constrained

Blast radius

Proven

  • Reproduced/reported on PowerShell 7.5.0 / 7.5.5 / 7.6.3.
  • Windows 10 (19045, per Constrained Language Audit Mode blocking actions #25109) and Windows 11 24H2 (customer, case 2607080030006081).
  • Deterministic: fails whenever system lockdown resolves to Audit and a ScriptBlock/InitializationScript is supplied.

Potential but not individually measured

  • Every managed device in a staged WDAC audit-mode deployment (typically large Intune/co-managed fleets during policy piloting).
  • Any product or script that internally calls Start-Job on such devices.

Severity interpretation

Scenario Practical impact
Interactive users on audit-mode devices Medium — a core cmdlet is unusable; Start-ThreadJob is a partial, non-equivalent workaround
Automation/CI depending on Start-Job High — pipelines/tasks fail on affected devices
Orgs piloting WDAC in audit before enforce High friction — the "safe evaluation" mode breaks tooling, discouraging correct staged rollout
Security posture None from the bug; and the fix does not relax the real (None) boundary

Not a confidentiality/privilege/exploit issue and not data loss — it is a functional availability defect for Start-Job on audit-mode devices, plus a diagnosis-misdirection cost.

Workarounds (until fixed)

  1. Use Start-ThreadJob where in-process isolation semantics are acceptable (not a security-equivalent drop-in for Start-Job).
  2. Use remoting (New-PSSession / Invoke-Command) where a real process/session boundary is required.
  3. For controlled diagnosis only, confirm that setting the session to FullLanguage restores Start-Job (demonstrates audit-as-enforce); do not present manual language-mode mutation as a production workaround.
  4. Do not weaken or remove an intended WDAC policy solely to restore Start-Job.

Fix disposition


PR Checklist

  • PR has a meaningful title — "Fix Start-Job failing under WDAC Constrained Language audit mode" (present tense, imperative; describes the change, not the issue).
  • Summarized changes — first sentence states the end-user benefit (for changelog); body explains root cause and fix.
  • Copyright headers — no new .cs/.ps1/.psm1 files added; both edited files already carry the correct header. N/A.
  • This PR is ready to merge — not WIP. (Open as a normal PR; use a Draft PR only if further iteration is expected.)
  • Breaking changes
    • None — behavioral fix to an internal preflight guard; no change to any cmdlet parameter, public API, or protocol. Only the previously-thrown CannotStartJobInconsistentLanguageMode under Audit is removed (a bug), so no Public Contract surface is broken.
  • User-facing changes
    • Not Applicable — no new/changed parameters or about_* topics; Start-Job behavior is restored to what documentation already implies for audit (log-only) mode. (If a Maintainer disagrees, they will add Documentation Needed; we can file a PowerShell-Docs issue on request.)
  • Testing - New and feature
    • Added a new test — audit-mode regression test in ConstrainedLanguageRestriction.Tests.ps1 asserting Start-Job succeeds when system lockdown resolves to Audit (simulated via the __PSLockdownPolicy UMCI audit flag 0x8). The existing None-case denial test is unchanged and still passes.

Start-Job threw CannotStartJobInconsistentLanguageMode when the runspace was in ConstrainedLanguage solely because the system-wide App Control (WDAC) policy is in audit mode. Audit mode enforces no restrictions and the child process re-evaluates policy independently, so there is no boundary to protect.

Track whether ConstrainedLanguage was audit-induced via ExecutionContext.LanguageModeWasSetByAppControlAudit (set in Utils.EnforceSystemLockDownLanguageMode) and allow Start-Job in that case, while still blocking explicitly configured/JEA ConstrainedLanguage sessions. Adds positive and negative tests, including an InitialSessionState-configured ConstrainedLanguage runspace that must remain blocked under audit.
Copilot AI review requested due to automatic review settings July 20, 2026 19:06
@jagilber
jagilber requested review from a team, TravisEz13 and daxian-dbw as code owners July 20, 2026 19:06
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

Copilot AI left a comment

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.

Pull request overview

Fixes Start-Job incorrectly throwing CannotStartJobInconsistentLanguageMode when PowerShell is running under WDAC/App Control audit mode (where the session reports ConstrainedLanguage but restrictions are intended to be log-only). The change threads through an execution-context flag to distinguish audit-induced constrained language from explicitly configured constrained language, and uses that to relax the Start-Job preflight guard only for the audit-induced case.

Changes:

  • Track whether ConstrainedLanguage was applied due to WDAC/App Control audit mode via ExecutionContext.LanguageModeWasSetByAppControlAudit.
  • Update StartJobCommand.CreateHelpersForSpecifiedComputerNames() guard to allow Start-Job when constrained language is audit-induced.
  • Add Pester regression coverage for Start-Job behavior under simulated audit policy, including explicit constrained-language scenarios remaining blocked.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
src/System.Management.Automation/engine/remoting/commands/StartJob.cs Adjusts the preflight guard for out-of-proc jobs to exempt audit-induced constrained language.
src/System.Management.Automation/engine/Utils.cs Sets the new execution-context flag when audit policy forces FullLanguage → ConstrainedLanguage.
src/System.Management.Automation/engine/ExecutionContext.cs Introduces LanguageModeWasSetByAppControlAudit to carry audit-induced CL provenance.
test/powershell/Modules/Microsoft.PowerShell.Security/ConstrainedLanguageRestriction.Tests.ps1 Adds regression tests to ensure Start-Job succeeds under audit-induced CL and remains blocked under explicitly configured CL.

Comment thread src/System.Management.Automation/engine/remoting/commands/StartJob.cs Outdated
Comment thread src/System.Management.Automation/engine/ExecutionContext.cs
jagilber and others added 2 commits July 20, 2026 15:51
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
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.

2 participants