Repository navigation
Constrained Language Mode: one failed [pscustomobject] conversion breaks every later one in the process, including in trusted modules and FullLanguage runspaces #28128
Description
Prerequisites
- Write a descriptive title.
- Make sure you are able to repro it on the latest released version
- Search the existing issues.
- Refer to the FAQ.
- Refer to Differences between Windows PowerShell 5.1 and PowerShell.
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 } # failsB. 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 # failsC. 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 # failsWithout 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).
- Every
[pscustomobject]cast goes through one binder shared by the whole process. The compiler builds a hashtable literal as anOrderedDictionary(Compiler.cs#L6121-L6129, the "pure laziness" comment). It then routes the cast through thePSCustomObjectConvertersingleton (Compiler.cs#L6152-L6158). - The binder decides based on the current language mode, then caches that decision for every language mode.
PSCustomObjectConverter.Bind(Binders.cs#L1794-L1828) maps anOrderedDictionaryorHashtabletoInternalPSCustomObjectand callsLanguagePrimitives.FigureConversion.- Under CLM,
FigureConversionreturnsConvertNotSupportedConversion, becauseInternalPSCustomObjectis not inCoreTypes(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.
- 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. - Nothing invalidates it. On the first switch to CLM, the
ExecutionContext.LanguageModesetter invalidates seven binders; on every switch, it rebuilds the conversion cache (ExecutionContext.cs#L275-L328).PSCustomObjectConverteris 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 anOrderedDictionary(every[pscustomobject]@{ ... }literal and[pscustomobject][ordered]@{ ... }) or of aHashtable([pscustomobject]$hashtable). Each source type is poisoned independently: a failed hashtable cast breaks later hashtable casts but not literals. Casts of other values targetPSObject, 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), throughGet-WindowsArchiveEntryPathValidationResult. Version 1.2.5, bundled with 7.6.0, has no literal. Get-Error: itsPSExtendedErrorview 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-FileHashandExpand-Archive. - The bundled
-
Not a bypass. Checked in both orders, in-session and across runspaces.
Suggested fix
- In
PSCustomObjectConverter.Bind, mergeBinderUtils.GetLanguageModeCheckIfHasEverUsedConstrainedLanguage()into the rule's restrictions, asPSConvertBinderdoes 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. - Don't cache language-mode-dependent results in the process-wide
s_converterCache. ReturnConvertNotSupportedConversionwithout 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. - Consider resolving
ConstrainedLanguagemode:[pscustomobject]literals cannot be used, even though the type is white-listed, and an equivalentNew-Objectcall succeeds #20767 so that nothing depends on the mode. Allow this conversion under CLM: its only result is aPSCustomObject, which CLM already creates withNew-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 insidetry/catch. UseNew-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]$hashtablecast 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
ConstrainedLanguagemode:[pscustomobject]literals cannot be used, even though the type is white-listed, and an equivalentNew-Objectcall succeeds #20767 (open):[pscustomobject]literals fail under CLM although[pscustomobject]is an allowed type. It notes that the error message depends on whether a literal ran earlier in FullLanguage. This issue shows the other direction: the CLM failure breaks FullLanguage code.- Conversion delegate cache cause conversion failures in the default session after using 'restricted language' in another runspace #5281 (closed as "No Activity", still reproduces): a conversion refused in a CLM runspace stays refused in the default runspace, because of the static conversion cache.
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
ConversionSupportedOnlyToCoreTypesActual 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.0PS> $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.0Visuals
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