Skip to content

Commit 8597c19

Browse files
committed
Analyze ForEach-Object parallel child runspaces
1 parent 5ce0c32 commit 8597c19

45 files changed

Lines changed: 9254 additions & 163 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

IMPLEMENTATION_PLAN.md

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,7 @@ priorities.
350350
delivered for ordinary, adjacent, quoted, here-string, redirect, standalone,
351351
call-operator, dynamic-identity, and host-wrapper positions, with
352352
current-scope state propagation and bounded expression rejection pinned
353-
by the 372-entry executable corpus.
353+
by the 383-entry executable corpus.
354354
- [ ] Deliver typed PowerShell script-block execution regions before calling
355355
tasks 7.5-7.7 complete. The corrected contract adds an execution-region
356356
syntax node with independent origin, phase, timing, and cardinality rather than a
@@ -425,8 +425,27 @@ priorities.
425425
proved child-process isolation while their body analysis stays fail
426426
closed; an explicit alternate `-PSVersion` remains visible but incomplete
427427
because it falls outside the pinned PowerShell 7 runtime model.
428-
Child runspace jobs and parallel
429-
blocks; deferred breakpoint/event/completion actions; then unknown
428+
`ForEach-Object -Parallel` now publishes a concurrent per-input child-
429+
runspace region, inherits the captured caller location, starts without
430+
caller bindings or alias mutations, and isolates runspace-local child
431+
exit from the host continuation, including `-AsJob`. Pooled-runspace command-
432+
resolution mutation is joined across later activations; `-UseNewRunspace`
433+
keeps activations independent. Live PowerShell probes pin both behaviors,
434+
including the process-wide environment-provider exception: a possibly
435+
escaping child mutation invalidates later host binding, command-
436+
resolution, and location facts and also poisons later child activations
437+
under `-UseNewRunspace`, which resets runspace-local but not process-wide
438+
state. Runspace-global variables, functions, aliases, and location remain
439+
isolated. The analyzer retains a bounded, case-insensitive set of exact
440+
alias/function names whose identity changed and treats only matching later
441+
invocations as possible process escapes; an ambiguous mutation or set
442+
overflow fails closed to every unproved command name. This includes a
443+
mutator such as `Set-Alias` being rebound to `Set-Item` before a later
444+
authored `Set-Alias Env:...` invocation.
445+
The generator-owned executable corpus now supports per-entry initial-
446+
state mode and includes the promoted Parallel design case. Pinned child
447+
runspace jobs and remote/session invocation remain before deferred
448+
breakpoint/event/completion actions; then unknown
430449
receiver and nested/adversarial matrices. Preserve script blocks proved
431450
to be data as opaque values, expose ambiguous bodies with incomplete
432451
facts, and fail atomically when any potentially executable interior is

SPEC.POWERSHELL.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -529,7 +529,7 @@ The version-pinned PowerShell 7 catalog covers:
529529
| `ForEach-Object -Begin` | Begin | Synchronous | Once | current runspace |
530530
| `ForEach-Object -Process` / `-RemainingScripts` | Process, with binder-assigned Begin/End where applicable | Synchronous | OncePerInputObject | current runspace |
531531
| `ForEach-Object -End` | End | Synchronous | Once | current runspace |
532-
| `ForEach-Object -Parallel` | Process | Concurrent | OncePerInputObject | child runspace; exit isolated |
532+
| `ForEach-Object -Parallel` | Process | Concurrent | OncePerInputObject | child runspace; runspace-local exit isolated; process-wide effects conservative |
533533
| `Where-Object -FilterScript` | Filter | Synchronous | OncePerInputObject | current runspace |
534534
| in-process `Invoke-Command -ScriptBlock` | Main | Synchronous | Once | child scope unless `-NoNewScope`; shared location |
535535
| remote/session/SSH/VM/container `Invoke-Command` | Main | proved from complete parameter set | Unknown unless targets are proved | remote/child state; exit isolated |
@@ -540,6 +540,22 @@ The version-pinned PowerShell 7 catalog covers:
540540
| `Set-PSBreakpoint -Action`, event `-Action` | Action | Deferred | ZeroOrMore | trigger-time state Unknown without proof |
541541
| `Register-ArgumentCompleter -ScriptBlock` | Completion | Deferred | ZeroOrMore | completion-time state Unknown without proof |
542542

543+
Parallel child runspaces do not inherit ordinary caller variables or aliases,
544+
and their runspace-local variable and location exit state does not flow into the
545+
host continuation. They do share process-wide state such as the environment
546+
provider. A supported or unknown child mutation that may affect such state
547+
invalidates later host binding, command-resolution, and location facts. It also
548+
invalidates later child-activation facts even with `-UseNewRunspace`, because a
549+
fresh runspace is not a fresh process.
550+
Runspace `global:` variable, function, alias, and location state remains local
551+
to that runspace and does not by itself invalidate the host. Once child command
552+
resolution is mutated, however, an exact changed alias or function name is
553+
retained in bounded case-insensitive state. A later matching invocation whose
554+
identity is no longer proved is a possible process-wide mutation and receives
555+
the same conservative invalidation unless its identity is independently proved.
556+
An ambiguous changed name, wildcard, or candidate-set overflow collapses to all
557+
unproved command names rather than guessing.
558+
543559
The optional inbox `Microsoft.PowerShell.ThreadJob\Start-ThreadJob` follows
544560
the initialization/main child-runspace model only when the caller's pinned
545561
module baseline proves that identity. Otherwise it follows the unknown

openspec/changes/v0-3-structured-shell-analysis/design.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,7 @@ PowerShell 7.6.4 probes demonstrate that these are independent dimensions:
374374
| local `Invoke-Command { ... }` | child scope unless `-NoNewScope` | shared | synchronous / once |
375375
| `Measure-Command { ... }` / `Trace-Command -Expression { ... }` | shared current scope | shared | synchronous / once |
376376
| `Start-Job { ... }` | child process state | inherited initial location; exit isolated | concurrent / once |
377-
| `ForEach-Object -Parallel { ... }` | child runspace state | inherited initial location; exit isolated | concurrent / once per input |
377+
| `ForEach-Object -Parallel { ... }` | child runspace state; process-wide effects may escape | inherited initial location; runspace-local exit isolated | concurrent / once per input |
378378
| event, breakpoint, completion actions | trigger-time state | trigger-time state | deferred / zero or more |
379379

380380
The shell-specific analyzer owns inbound state, exit propagation, phase
@@ -383,6 +383,22 @@ Occurrence facts expose the effective result. This keeps the public structural
383383
API narrow while avoiding the false claim that variables and location always
384384
share one boundary.
385385

386+
An in-process child runspace is not a process boundary. Ordinary variables,
387+
aliases, and PowerShell location are runspace-local, but environment-provider
388+
mutation is process-wide and can affect the host and sibling runspaces. The
389+
analyzer therefore invalidates later host binding, command-resolution, and
390+
location facts after any possibly escaping child effect. The same invalidation
391+
applies to later activations under `-UseNewRunspace`: the switch removes pooled
392+
runspace-local state, but it does not create a separate process.
393+
Runspace `global:` variables, functions, aliases, and location remain
394+
runspace-local and do not themselves poison host facts. Exact alias/function
395+
names changed inside a child are retained in a bounded, case-insensitive set;
396+
only a matching subsequent invocation whose identity is not independently
397+
proved is treated as a possible process-wide mutation. Ambiguous names,
398+
wildcards, and set overflow collapse to all unproved command names. This
399+
prevents both an alias to `Set-Item` and a rebound `Set-Alias` mutator from
400+
hiding an environment-provider write without tainting unrelated names.
401+
386402
`SimpleCommandSyntax.ExecutionRegions` preserves authored script-block order.
387403
The analyzer separately applies semantic phase order. PowerShell's binder can
388404
assign Begin, Process, and End roles to multiple `ForEach-Object` blocks even

openspec/changes/v0-3-structured-shell-analysis/specs/bounded-shell-analysis/spec.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -668,7 +668,19 @@ from the complete parameter set.
668668

669669
`Start-Job` executes initialization before its main block in a child process;
670670
`ForEach-Object -Parallel` and a proved `Start-ThreadJob` execute in child
671-
runspaces. Their exit mutation does not flow into the containing continuation.
671+
runspaces. Runspace-local variable and location exit mutation does not flow into
672+
the containing continuation. In-process child runspaces share process-wide
673+
state such as the environment provider, so a possibly escaping mutation SHALL
674+
invalidate later host binding, command-resolution, and location facts. It SHALL
675+
also invalidate later child-activation facts under `-UseNewRunspace`, which
676+
creates a fresh runspace but not a fresh process.
677+
Runspace `global:` variables, functions, aliases, and location SHALL remain
678+
runspace-local and SHALL NOT by themselves invalidate host facts. After child
679+
command resolution is mutated, each exact changed alias or function name SHALL
680+
be retained in bounded case-insensitive state. A later matching invocation
681+
whose identity is not independently proved SHALL be treated as a possible
682+
process-wide mutation. An ambiguous name, wildcard, or candidate-set overflow
683+
SHALL fail closed to every unproved command name.
672684
Breakpoint actions, event actions, and argument completers are deferred and
673685
may execute zero or more times; their trigger-time cwd and mutable state are
674686
Unknown unless independently proved. Deferred commands remain in the

0 commit comments

Comments
 (0)