Skip to content

Refuse scriptblock job values at runtime; tighten Lint Rules 7-9 - #4

Merged
HalRiveria merged 3 commits into
mainfrom
fix/runtime-scriptblock-guard
Oct 28, 2026
Merged

HalRiveria merged 3 commits into
mainfrom
fix/runtime-scriptblock-guard

Conversation

@HalRiveria-Bot

Copy link
Copy Markdown
Collaborator

Follow-up to #3, which was merged before this commit was pushed.

What was wrong

A code review of #3 found gaps in its new Lint Rule 7:

  • Static job-parameter check: it missed [scriptblock]-typed parameters, provider output without return, and the colon, inline and splatted forms of -JobParams.
  • A misfire: it counted any method call on a scriptblock as a scriptblock.
  • Lost coverage: with the old cmdlet ban gone, nothing flags Write-Host/Read-Host in worker code anymore. Workers share the console host, so these bypass the console queue.

What changed

  • Runtime guard. Invoke-ZipParallelProcessing refuses a job whose value is a scriptblock, or holds one directly in an array or hashtable value (new Get-ZipJobScriptBlockKey). The error names the key and points to the workaround: pass the code as a string and build it in the worker with [scriptblock]::Create().
    • A -JobParams list is checked before anything runs.
    • Each -NextJobProvider job is checked as it's produced.
    • This replaces Rule 7's static job-parameter matcher.
  • Lint Rule 7 flags a variable holding a scriptblock, or a [scriptblock] parameter, passed to a .NET method or constructor from runspace-reachable code. Only .GetNewClosure() results count as scriptblocks.
  • New Lint Rule 9 flags host cmdlets (Write-Host, Read-Host and others) on runspace-reachable paths, with the existing Write-LogLineToHost exception.
  • Rules 7-9 share one traversal, and each function's file is recorded once instead of searched for in every rule.

How it was tested

  • Runtime guard: new ZipParallelProcessing harness Scenario H checks four cases that must be refused and one that must still run:

    • refused: a scriptblock value, one inside an array, one inside a hashtable, and one in a provider job
    • still runs: plain data, including code passed as a string

    Against the module without the guard, 5 of its 6 asserts fail.

  • Lint: sample asserts for every shape each rule flags or skips. Planting one violation per rule in a copy of the real modules makes each rule fail with the right file and line, and current main passes them all.

  • All 21 local harnesses pass.

🤖 Generated with Claude Code

Invoke-ZipParallelProcessing now refuses a job whose value is a
scriptblock, or holds one directly in an array or hashtable value, with
a message naming the key and the [scriptblock]::Create() workaround. A
-JobParams list is checked before anything runs, and each
-NextJobProvider job as it's produced. The new Scenario H in the
harness fails without this check.

That replaces the static job-parameter matcher in Lint Rule 7, which
missed several call shapes. Rule 7 now flags a scriptblock variable, or
a [scriptblock]-typed parameter, passed to a .NET method or
constructor, and only treats .GetNewClosure() results as scriptblocks.
The new Rule 9 flags host cmdlets (Write-Host, Read-Host and others) on
runspace-reachable paths. Workers share the console host, so these
would bypass the console queue. The old cmdlet ban used to catch them
by accident. Rules 7-9 share one traversal, and each function's file
is recorded once instead of being searched for in every rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HalRiveria and others added 2 commits October 28, 2026 04:01
Job guard (Invoke-ZipParallelProcessing):
- Looks up to four levels down, through arrays, lists, hashtables and
  [pscustomobject] properties.
- Reads each collection once per call. A collection passed by reference
  to every job is checked with the first job, before any worker can be
  writing to it. A collection that can't be read because something is
  changing it is let through instead of failing the run.
- A refused -NextJobProvider job no longer throws mid-loop and stops
  the workers already running. New jobs stop starting, the running ones
  finish and report, and then the call throws.

Lint:
- Rule 7 also flags a scriptblock stored through an indexer or property
  on a container the code didn't create ($cache[$k] = $sb,
  $slot.Factory = { }).
- Rule 9 resolves aliases (cls, clear, oh) and flags direct
  [Console]:: and $Host.UI use.

module-descriptions.md documents the console rule as a third
worker-runspace rule. The new harness and Lint assertions fail against
the previous code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Job guard (Invoke-ZipParallelProcessing):
- Reads only materialized collections (ICollection, ICollection<T>,
  IReadOnlyCollection<T>). A lazy or consuming enumerable such as
  GetConsumingEnumerable() is no longer drained before the worker gets
  it.
- Looks inside KeyValuePair and DictionaryEntry values, which were
  skipped along with every other value type.
- Reads a [pscustomobject] shared across jobs once per call, like
  collections.
- Lets a collection through only when it reports being modified. Any
  other failure to read a value now throws, naming the key.

Lint:
- One helper decides what counts as an assigned scriptblock literal.
- Rule 7 treats a container as local only if every assignment to it
  creates a new one, so a store through an alias of a shared container
  or through a loop variable is flagged.
- Rule 9 matches module-qualified names.

The harness's enumeration test doubles are PowerShell classes, so the
harness no longer compiles a type with Add-Type. The new harness and
Lint assertions fail against the previous code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@HalRiveria
HalRiveria merged commit 6a893b9 into main Oct 28, 2026
1 check passed
@HalRiveria
HalRiveria deleted the fix/runtime-scriptblock-guard branch October 28, 2026 09:00
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