Skip to content

Constrained Language Mode: one failed [pscustomobject] conversion breaks every later one in the process, including in trusted modules and FullLanguage runspaces #28128

Open

Description

@BenPryor

Prerequisites

Steps to reproduce

Under ConstrainedLanguage (CLM), [pscustomobject]@{ ... } fails (#20767). PowerShell then caches that failure for the whole process. After one failed attempt, even one caught by try/catch, every later [pscustomobject]@{ ... } in the process fails with the same error. That includes code that runs in FullLanguage: script modules that a WDAC policy trusts, and FullLanguage runspaces, including ones created after the failure. The error blames "this language mode", so the trusted command looks broken under CLM.

Under an enforced WDAC policy, untrusted scripts and interactive sessions run in CLM, while trusted modules run in FullLanguage. So one failed literal breaks trusted commands for the rest of the process, such as the bundled Expand-Archive (Microsoft.PowerShell.Archive 1.2.6). It also silences Get-Error, whose format view uses a [pscustomobject] literal. This is not a CLM bypass: if FullLanguage code converts first, CLM literals still fail.

Run each snippet in a fresh pwsh -NoProfile. If a [pscustomobject] literal has already run in FullLanguage in that process, the bug does not appear (see Scope). No WDAC policy is needed.

A. A FullLanguage host with a CLM runspace

$iss = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault()
$iss.LanguageMode = 'ConstrainedLanguage'
$ps = [powershell]::Create($iss)
$null = $ps.AddScript('[pscustomobject]@{ a = 1 }').Invoke()
$ps.Streams.Error[0].FullyQualifiedErrorId   # ConversionSupportedOnlyToCoreTypes, as expected
$ps.Dispose()

$ExecutionContext.SessionState.LanguageMode  # FullLanguage
[pscustomobject]@{ a = 1 }                   # fails

B. Trusted code called from a CLM session. A function defined while the session is still in FullLanguage keeps FullLanguage, just as a policy-trusted module does.

function Get-TrustedObject { [pscustomobject]@{ a = 1 } }   # defined in FullLanguage
$ExecutionContext.SessionState.LanguageMode = 'ConstrainedLanguage'
try { [pscustomobject]@{ a = 1 } } catch { $_.FullyQualifiedErrorId }   # expected failure
Get-TrustedObject                                                       # fails

C. A bundled module command

$txt = Join-Path $env:TEMP 'clmrepro.txt'; $zip = Join-Path $env:TEMP 'clmrepro.zip'
Set-Content -Path $txt -Value 'x'
Compress-Archive -Path $txt -DestinationPath $zip -Force   # loads Microsoft.PowerShell.Archive in FullLanguage
$ExecutionContext.SessionState.LanguageMode = 'ConstrainedLanguage'
try { [pscustomobject]@{ a = 1 } } catch { $_.FullyQualifiedErrorId }   # expected failure
Expand-Archive -Path $zip -DestinationPath (Join-Path $env:TEMP 'clmrepro') -Force   # fails

Without the CLM attempt (the AddScript call in A, the try line in B and C), each snippet succeeds. All three reproduce on 7.6.6 and 7.7.0-preview.5.

Root cause

Links are to v7.6.0; this code is unchanged on master at f5fd484 (2026-09-28).

  1. Every [pscustomobject] cast goes through one binder shared by the whole process. The compiler builds a hashtable literal as an OrderedDictionary (Compiler.cs#L6121-L6129, the "pure laziness" comment). It then routes the cast through the PSCustomObjectConverter singleton (Compiler.cs#L6152-L6158).
  2. The binder decides based on the current language mode, then caches that decision for every language mode.
    • PSCustomObjectConverter.Bind (Binders.cs#L1794-L1828) maps an OrderedDictionary or Hashtable to InternalPSCustomObject and calls LanguagePrimitives.FigureConversion.
    • Under CLM, FigureConversion returns ConvertNotSupportedConversion, because InternalPSCustomObject is not in CoreTypes (LanguagePrimitives.cs#L5683-L5697).
    • The rule it returns is restricted only by the target's type (target.PSGetTypeRestriction(), L1826). It has no language-mode restriction and no version. The comment at L1819-L1820 assumes the conversion is always safe to share.
  3. The first rule wins everywhere. The DLR caches the rule on the binder, where every call site that uses it finds it, and on each call site. So whichever language mode binds first decides the outcome for every [pscustomobject] cast of that dictionary type in the process.
  4. Nothing invalidates it. On the first switch to CLM, the ExecutionContext.LanguageMode setter invalidates seven binders; on every switch, it rebuilds the conversion cache (ExecutionContext.cs#L275-L328). PSCustomObjectConverter is not among the seven and has no version to bump. PSConvertBinder, by contrast, adds a language-mode restriction for non-core types (Binders.cs#L490-L506, L3799). That is why other refused casts, such as [System.Collections.ArrayList]@(1, 2) or [System.Text.StringBuilder]'x', do not poison trusted code within a session.

Across runspaces, a second cache is involved: #5281, which is still unfixed. FigureConversion also stores ConvertNotSupportedConversion in the static s_converterCache (LanguagePrimitives.cs#L4410-L4431). The language-mode setter clears that cache, which protects trusted code called from a CLM session, because each call switches the mode. A runspace that never switches mode keeps the cached refusal. #5281's repro ([System.IO.FileInfo] 'foo' in a CLM runspace, then in the default runspace) still fails on 7.6.6. In snippet A both caches hold the refusal (see Diagnostic evidence).

Why the reverse order is safe. When FullLanguage binds first, the cached rule calls ConvertViaNoArgumentConstructor.Convert. That method checks the language mode at run time and refuses the conversion under CLM (LanguagePrimitives.cs#L3991-L4003). CLM literals therefore still fail, with the "Hashtable-to-Object conversion is not supported" message noted in #20767.

Scope

  • Affected casts. Every [pscustomobject] cast of an OrderedDictionary (every [pscustomobject]@{ ... } literal and [pscustomobject][ordered]@{ ... }) or of a Hashtable ([pscustomobject]$hashtable). Each source type is poisoned independently: a failed hashtable cast breaks later hashtable casts but not literals. Casts of other values target PSObject, a core type, and are unaffected.

  • Order. Whichever language mode binds first decides. After a FullLanguage literal has bound first, later FullLanguage literals keep working, and CLM literals still fail.

  • Trusted code affected in 7.6.6 and 7.7.0-preview.5:

    • The bundled Expand-Archive (Microsoft.PowerShell.Archive 1.2.6), through Get-WindowsArchiveEntryPathValidationResult. Version 1.2.5, bundled with 7.6.0, has no literal.
    • Get-Error: its PSExtendedError view uses a literal, so it prints nothing.

    Windows PowerShell 5.1 behaves the same way. Under an enforced WDAC policy, one failed literal breaks its Get-FileHash and Expand-Archive.

  • Not a bypass. Checked in both orders, in-session and across runspaces.

Suggested fix

  1. In PSCustomObjectConverter.Bind, merge BinderUtils.GetLanguageModeCheckIfHasEverUsedConstrainedLanguage() into the rule's restrictions, as PSConvertBinder does for non-core types. Also give the binder the version-based invalidation that the other binders get on the first switch to CLM. Then a rule bound in one language mode is never reused in another.
  2. Don't cache language-mode-dependent results in the process-wide s_converterCache. Return ConvertNotSupportedConversion without caching it, or key the cache by language mode. This fixes Conversion delegate cache cause conversion failures in the default session after using 'restricted language' in another runspace #5281 for every type.
  3. Consider resolving ConstrainedLanguage mode: [pscustomobject] literals cannot be used, even though the type is white-listed, and an equivalent New-Object call succeeds #20767 so that nothing depends on the mode. Allow this conversion under CLM: its only result is a PSCustomObject, which CLM already creates with New-Object psobject -Property. Alternatively, construct the object directly, as the Compiler.cs comment suggests.

Workarounds

  • Code that may run under CLM: never attempt a [pscustomobject] conversion, even inside try/catch. Use New-Object -TypeName psobject -Property ([ordered]@{ ... }), which works in both modes and poisons nothing.
  • A poisoned process: restart it. Setting the language mode clears only the conversion cache, not the binder's rules.
  • Hosts that run untrusted content in CLM runspaces: run it out of process. As a fragile stopgap, run one [pscustomobject]@{} literal and one [pscustomobject]$hashtable cast in FullLanguage before creating the CLM runspace.
Diagnostic evidence: clearing only the binder's rule cache

In snippet B's setup, clearing only PSCustomObjectConverter's rule cache through reflection restores FullLanguage call sites that have not run yet. Call sites that already cached the bad rule keep failing.

function Clear-PSCustomObjectBinderRules {
    $bf = [System.Reflection.BindingFlags]'NonPublic,Static'
    $ib = [System.Reflection.BindingFlags]'NonPublic,Public,Instance'
    $t = [System.Management.Automation.PSObject].Assembly.GetType('System.Management.Automation.Language.PSCustomObjectConverter')
    $binder = @($t.GetFields($bf) | Where-Object { $_.FieldType -eq $t })[0].GetValue($null)
    $m = @([System.Runtime.CompilerServices.CallSiteBinder].GetMembers($ib) |
        Where-Object { $_.Name -like '*Cache*' -and ($_.MemberType -eq 'Field' -or $_.MemberType -eq 'Property') })[0]
    if ($m.MemberType -eq 'Field') { $c = $m.GetValue($binder) } else { $c = $m.GetValue($binder, $null) }
    if ($c) { $c.Clear() }
}

The functions below were defined before the switch to CLM, so they run in FullLanguage. Output is the same on 7.6.6 and Windows PowerShell 5.1.26100.9444:

1 CLM literal                                  -> ConversionSupportedOnlyToCoreTypes (expected)
2 trusted function A                           -> ConversionSupportedOnlyToCoreTypes
3 Clear-PSCustomObjectBinderRules              -> ok
4 trusted function B (call site never run)     -> ok
5 trusted function A again (same call site)    -> ConversionSupportedOnlyToCoreTypes
6 CLM literal again                            -> Hashtable-to-Object conversion is not supported ...

In snippet A's setup, both caches hold the refusal:

1 CLM runspace: literal                           -> ConversionSupportedOnlyToCoreTypes (expected)
2 s_converterCache[OrderedDictionary->InternalPSCustomObject] = ConvertNotSupportedConversion
3 Clear-PSCustomObjectBinderRules                 -> ok
4 host literal (new call site)                    -> ConversionSupportedOnlyToCoreTypes
5 host sets $ExecutionContext.SessionState.LanguageMode = 'FullLanguage'   (rebuilds s_converterCache)
6 Clear-PSCustomObjectBinderRules                 -> ok
7 host literal (new call site)                    -> ok

Related

Expected behavior

# A: the host's literal outputs the object
ConversionSupportedOnlyToCoreTypes
FullLanguage

a
-
1

# B: Get-TrustedObject outputs the object
ConversionSupportedOnlyToCoreTypes

a
-
1

# C: Expand-Archive extracts clmrepro.txt without error
ConversionSupportedOnlyToCoreTypes

Actual behavior

# A
ConversionSupportedOnlyToCoreTypes
FullLanguage
InvalidArgument:
Line |
   9 |  [pscustomobject]@{ a = 1 }                   # fails
     |  ~~~~~~~~~~~~~~~~~~~~~~~~~~
     | Cannot convert value to type "System.Management.Automation.LanguagePrimitives+InternalPSCustomObject". Only core types are supported in this language mode.

# B
ConversionSupportedOnlyToCoreTypes
InvalidArgument: Cannot convert value to type "System.Management.Automation.LanguagePrimitives+InternalPSCustomObject". Only core types are supported in this language mode.

# C
ConversionSupportedOnlyToCoreTypes
InvalidArgument: Cannot convert value to type "System.Management.Automation.LanguagePrimitives+InternalPSCustomObject". Only core types are supported in this language mode.

Error details

FullyQualifiedErrorId : ConversionSupportedOnlyToCoreTypes
CategoryInfo          : InvalidArgument: (:) [], RuntimeException
Exception             : System.Management.Automation.RuntimeException: Cannot convert value to type "System.Management.Automation.LanguagePrimitives+InternalPSCustomObject". Only core types are supported in this language mode.
 ---> System.Management.Automation.PSInvalidCastException: Cannot convert value to type "System.Management.Automation.LanguagePrimitives+InternalPSCustomObject". Only core types are supported in this language mode.
   at System.Management.Automation.LanguagePrimitives.ThrowInvalidConversionException(Object valueToConvert, Type resultType)
   at System.Management.Automation.LanguagePrimitives.ConvertNotSupportedConversion(Object valueToConvert, Type resultType, Boolean recurse, PSObject originalValueToConvert, IFormatProvider formatProvider, TypeTable backupTable)
   at CallSite.Target(Closure, CallSite, OrderedDictionary)
   at System.Dynamic.UpdateDelegates.UpdateAndExecute1[T0,TRet](CallSite site, T0 arg0)
   at System.Management.Automation.Interpreter.DynamicInstruction`2.Run(InterpretedFrame frame)
   at System.Management.Automation.Interpreter.EnterTryCatchFinallyInstruction.Run(InterpretedFrame frame)
   --- End of inner exception stack trace ---

Get-Error prints nothing in an affected process (see Scope). The block above shows the error record's properties after snippet A, on 7.6.6.

Environment data

PS> $PSVersionTable

Name                           Value
----                           -----
PSVersion                      7.6.6
PSEdition                      Core
GitCommitId                    7.6.6
OS                             Microsoft Windows 10.0.26200
Platform                       Win32NT
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0…}
PSRemotingProtocolVersion      2.4
SerializationVersion           1.1.0.1
WSManStackVersion              3.0
PS> $PSVersionTable

Name                           Value
----                           -----
PSVersion                      7.7.0-preview.5
PSEdition                      Core
GitCommitId                    7.7.0-preview.5
OS                             Microsoft Windows 10.0.26200
Platform                       Win32NT
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0…}
PSRemotingProtocolVersion      2.4
SerializationVersion           1.1.0.1
WSManStackVersion              3.0

Visuals

No response


Transparency note: this report was generated with AI assistance (GitHub Copilot CLI) and reviewed before posting. The repros and diagnostics above were run on the versions listed.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions