Skip to content

Commit a78f404

Browse files
committed
Make re-arm safe under Constrained Language Mode
Addresses Copilot review on PR #662. __ShellInteg_Rearm compared prompt identity with [object]::ReferenceEquals. Re-arm runs unconditionally at script load and from PSConsoleHostReadLine, and under Constrained Language Mode a static method call is blocked. Confirmed: Cannot invoke method. Method invocation is supported only on core types in this language mode. and the failure ABORTS the enclosing script even from inside try/catch, so it would break profile sourcing or the input path. This is the same hazard the script already guards elsewhere via $Global:__ShellInteg_CanInspectErrors. Rather than no-op under Constrained Language Mode, identity is now compared with -eq. ScriptBlock does not override Equals, so -eq IS reference identity (verified: two scriptblocks with identical text compare False, the same object compares True), and the operator is permitted in Constrained Language Mode. Re-arming therefore keeps working there instead of being disabled. Verified end-to-end under ConstrainedLanguage on Windows PowerShell 5.1 and PowerShell 7: sourcing the script, rendering the prompt with marks, and the input path all succeed with no exception. Note re-arm still has no per-command driver in that mode because the PSConsoleHostReadLine wrapper is gated behind CanInspectErrors - pre-existing v6 behaviour, unchanged here - so integration degrades gracefully rather than self-healing. Also fixes the repo's forbidden-spelling rule ('re-entrancy' -> 'reentrancy') in the new tests, and adds an assertion that re-arm contains no static method call at all, so this cannot regress. 104/104 ShellIntegrationTests pass.
1 parent 1aad0a3 commit a78f404

2 files changed

Lines changed: 20 additions & 5 deletions

File tree

src/cascadia/UnitTests_TerminalCore/ShellIntegrationTests.cpp

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -515,7 +515,11 @@ void ShellIntegrationTests::PowerShell_ScriptContent_ReArmsFromReadLineBoundary(
515515
// Detection is by object identity against our own scriptblock — never by
516516
// text matching, which a renderer could accidentally satisfy.
517517
const auto rearmFunc = script.find("function Global:__ShellInteg_Rearm");
518-
const auto identityCheck = script.find("[object]::ReferenceEquals($current, $Global:__ShellInteg_Wrapper)", rearmFunc);
518+
// -eq, NOT [object]::ReferenceEquals: re-arm runs at script load and from the
519+
// readline boundary, and a static method call under Constrained Language Mode
520+
// aborts the enclosing script even from inside try/catch.
521+
const auto identityCheck = script.find("if ($current -eq $Global:__ShellInteg_Wrapper) { return }", rearmFunc);
522+
const auto noStaticCall = script.substr(rearmFunc).find("[object]::ReferenceEquals");
519523
const auto adopt = script.find("$Global:__ShellInteg_OriginalPrompt = $current", identityCheck);
520524
const auto reinstall = script.find("Set-Item Function:\\global:prompt $Global:__ShellInteg_Wrapper", adopt);
521525

@@ -531,6 +535,8 @@ void ShellIntegrationTests::PowerShell_ScriptContent_ReArmsFromReadLineBoundary(
531535
L"The wrapper scriptblock must be captured after prompt is defined");
532536
VERIFY_IS_TRUE(rearmFunc < identityCheck && identityCheck < adopt && adopt < reinstall,
533537
L"Re-arm must detect replacement by object identity, adopt the newcomer, then reinstall the wrapper");
538+
VERIFY_ARE_EQUAL(std::string::npos, noStaticCall,
539+
L"Re-arm must not invoke static methods - blocked in Constrained Language Mode, and the failure aborts the script even from inside try/catch");
534540
}
535541

536542
void ShellIntegrationTests::PowerShell_ScriptContent_ReArmIsBestEffortAndSkipsWithoutWrapper()
@@ -606,9 +612,9 @@ void ShellIntegrationTests::PowerShell_ScriptContent_GuardsAgainstChainingPrompt
606612
VERIFY_ARE_NOT_EQUAL(std::string::npos, clearFlag);
607613
VERIFY_ARE_NOT_EQUAL(std::string::npos, retainPrev);
608614
VERIFY_IS_TRUE(promptStart < reentryGuard && reentryGuard < servePrev,
609-
L"Re-entry must be detected before any rendering work and serve the displaced prompt");
615+
L"reentry must be detected before any rendering work and serve the displaced prompt");
610616
VERIFY_IS_TRUE(setFlag < delegation && delegation < clearFlag,
611-
L"The re-entrancy flag must be set around the delegation and cleared in finally");
617+
L"The reentrancy flag must be set around the delegation and cleared in finally");
612618
}
613619

614620
void ShellIntegrationTests::PowerShell_ScriptContent_RendersFailSafe()

src/cascadia/inc/PowerShellShellIntegration.h

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -613,17 +613,26 @@ function Global:__ShellInteg_Rearm {
613613
# the next shell, which sources only this version.
614614
if ($null -eq $Global:__ShellInteg_Wrapper) { return }
615615
616+
# Identity is compared with -eq rather than a static reference-equality
617+
# method. This runs unconditionally at script load and from
618+
# PSConsoleHostReadLine, and under Constrained Language Mode a static method
619+
# call raises "Method invocation is supported only on core types", which
620+
# aborts the enclosing script even from inside try/catch - breaking profile
621+
# sourcing or the input path. ScriptBlock does not override Equals, so -eq is
622+
# reference identity here (verified: two scriptblocks with identical text
623+
# compare False), and the operator is permitted in Constrained Language Mode.
624+
# That keeps re-arming working there instead of disabling it.
616625
$current = $function:prompt
617626
if ($null -eq $current) { return }
618-
if ([object]::ReferenceEquals($current, $Global:__ShellInteg_Wrapper)) { return }
627+
if ($current -eq $Global:__ShellInteg_Wrapper) { return }
619628
620629
# Best-effort. This runs from PSConsoleHostReadLine, so a throw here would
621630
# break the input path, and at script load, where it would break profile
622631
# sourcing. `prompt` can legitimately be ReadOnly or Constant in a hardened
623632
# profile, which makes Set-Item raise SessionStateUnauthorizedAccessException.
624633
# Shell integration is a convenience; never let it take the shell down.
625634
try {
626-
if (-not [object]::ReferenceEquals($current, $Global:__ShellInteg_OriginalPrompt)) {
635+
if ($current -ne $Global:__ShellInteg_OriginalPrompt) {
627636
# Retain the renderer being displaced, in case the newcomer chains
628637
# back into us.
629638
$Global:__ShellInteg_PrevPrompt = $Global:__ShellInteg_OriginalPrompt

0 commit comments

Comments
 (0)