From ee3ed05923a7ae8a0be8aeec5f5f3ff7b7a9b2f7 Mon Sep 17 00:00:00 2001 From: Julian Noble Date: Thu, 23 Jul 2026 04:27:16 +1000 Subject: [PATCH] punk-runtime: dependency-free sha1 hashing in the powershell payload (Get-FileHash field failure) Field report (jcross1, Windows PowerShell 5): Get-FileHash not recognized during list -remote while Write-Host/Invoke-WebRequest worked. Root cause class: Get-FileHash is a SCRIPT-defined function in PS 5.1's Microsoft.PowerShell.Utility, resolved via PSModulePath autoload - a damaged PSModulePath (pwsh7 path pollution, overridden env, redirected user module dirs) removes it while the module's compiled cmdlets keep working. Effects were wrong results, not just noise: spurious UPDATE AVAILABLE rows (null local hash) and fail-closed fetch verification. New Get-PunkFileSha1 computes sha1 via .NET (SHA1.Create over a stream, lowercase hex) at all three former Get-FileHash sites; works on any PowerShell edition/state incl FIPS, and emits a one-time informational note when the damaged condition is detected (chosen over detect-and-offer-install/env-repair: nothing to install once the dependency is gone, and persistent environment mutation from a fetch tool is disproportionate). Bash payload already probes sha1sum/shasum/sha1/openssl - unchanged. bin/AGENTS.md records the no-script-module-cmdlets payload rule. Verified: hash parity with Get-FileHash under powershell 5 and pwsh 7; list -remote via the rewrapped .cmd (correct Same-version rows); damaged-PS simulation (probe forced unresolvable): note printed once, zero errors, hashing correct; scriptwrap roundtrip pin runtimecmd_roundtrip PASS (payload + regenerated bin output committed together). CHANGELOG bullet added under the in-flight 0.19.0 entry (rides the user's pending release-notes commit). Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com --- bin/AGENTS.md | 8 +++++ bin/punk-runtime.cmd | 47 +++++++++++++++++++++++++---- src/scriptapps/bin/punk-runtime.ps1 | 47 +++++++++++++++++++++++++---- 3 files changed, 90 insertions(+), 12 deletions(-) diff --git a/bin/AGENTS.md b/bin/AGENTS.md index 4bb815b6..7c8ce01a 100644 --- a/bin/AGENTS.md +++ b/bin/AGENTS.md @@ -101,6 +101,14 @@ The first argument to a built punk shell may be a dash-delimited package mode co `bin/runtime//` (retrieved from the punkbin artifact repo with sha1 verification against its `sha1sums.txt` - both the powershell payload on windows and the bash payload on unix verify; a fetch whose checksum fails leaves only a `.tmp`). +Hashing is deliberately dependency-free in both payloads: the powershell payload computes +sha1 via .NET (`Get-PunkFileSha1`) rather than `Get-FileHash`, because Get-FileHash is a +script-defined function in Windows PowerShell 5.1 resolved through PSModulePath and +vanishes on machines with a damaged PSModulePath while compiled cmdlets still work +(field-observed; a one-time note flags the condition); the bash payload probes +sha1sum/shasum/sha1/openssl. Keep new payload code free of PS 5.1 +script-module-defined cmdlets (`Get-FileHash`, `New-TemporaryFile`, `New-Guid`, +`Import-PowerShellDataFile`, ...) for the same reason. `list -remote` compares local runtimes against the server's sha1sums (Same version / UPDATE AVAILABLE / not-listed, plus remote-only entries; cached sha1sums.txt fallback with a warning when the server is unreachable). It also surfaces the selection state: diff --git a/bin/punk-runtime.cmd b/bin/punk-runtime.cmd index d61612b4..d548299d 100755 --- a/bin/punk-runtime.cmd +++ b/bin/punk-runtime.cmd @@ -2513,6 +2513,41 @@ function Get-PunkRuntimeRootName { } return $name } +#Dependency-free SHA1 (robustness fix 2026-07-23): Get-FileHash is a SCRIPT-defined +#function in Windows PowerShell 5.1's Microsoft.PowerShell.Utility, resolved via +#PSModulePath autoloading - on machines with a damaged PSModulePath (pwsh7 module-path +#pollution, overridden env vars, redirected user module dirs) it vanishes while the +#module's COMPILED cmdlets (Write-Host, Invoke-WebRequest) still work, observed in the +#field ("Get-FileHash ... not recognized" under PS 5). Hashing here goes through .NET +#directly so fetch verification and list -remote comparison work on any PowerShell +#edition/state, with a one-time informational note when the damage is present. +#Returns lowercase hex (sha1sums.txt convention; -eq/-ne comparisons are +#case-insensitive so case never matters for verification). +$script:punkFileHashNoteShown = $false +function Get-PunkFileSha1 { + param([string] $Path) + if (-not $script:punkFileHashNoteShown) { + $script:punkFileHashNoteShown = $true + if ($PSVersionTable.PSVersion.Major -le 5 -and -not (Get-Command Get-FileHash -ErrorAction SilentlyContinue)) { + Write-Host "note: Get-FileHash is unavailable in this Windows PowerShell (PSModulePath on this machine may be damaged) - punk-runtime uses built-in .NET hashing and is unaffected" + } + } + #Resolve-Path: .NET's current directory is not PowerShell's location, so hand + #[System.IO.File] an absolute provider path + $fullpath = (Resolve-Path -LiteralPath $Path).ProviderPath + $sha1 = [System.Security.Cryptography.SHA1]::Create() + try { + $fs = [System.IO.File]::OpenRead($fullpath) + try { + $hashbytes = $sha1.ComputeHash($fs) + } finally { + $fs.Dispose() + } + } finally { + $sha1.Dispose() + } + return (($hashbytes | ForEach-Object { $_.ToString("x2") }) -join "") +} #G-103 artifact metadata: a runtime may carry a .toml beside it (emitted #by the buildsuite kit-family-artifacts step / fetched from punkbin). Returns a #short "[variant=... tcl=... rN ...]" summary for list output, "" when absent. @@ -2787,8 +2822,8 @@ function psmain { if (Test-Path -Path $output -PathType Leaf) { Write-Host "Runtime already found at $output" Write-Host "Checking sha1 checksum of local file versus sha1 of server file" - $file_sha1 = Get-FileHash -Path "$output" -Algorithm SHA1 - if (${file_sha1}.Hash -ne $stored_sha1) { + $file_sha1 = Get-PunkFileSha1 -Path "$output" + if ($file_sha1 -ne $stored_sha1) { Write-Host "$runtime on server has different sha1 hash - Download required" $need_download = $true } @@ -2812,8 +2847,8 @@ function psmain { } Write-Host "comparing sha1 checksum of downloaded file with data in sha1sums.txt" Start-Sleep -Seconds 1 #REVIEW - give at least some time for windows to do its thing? (av filters?) - $newfile_sha1 = Get-FileHash -Path "${output}.tmp" -Algorithm SHA1 - if (${newfile_sha1}.Hash -eq $stored_sha1) { + $newfile_sha1 = Get-PunkFileSha1 -Path "${output}.tmp" + if ($newfile_sha1 -eq $stored_sha1) { Write-Host "sha1 checksum ok" Move-Item -Path "${output}.tmp" -Destination "${output}" -Force Write-Host "Runtime is available at ${output}" @@ -3045,8 +3080,8 @@ function psmain { #local runtimes in the comparison $dircontents = Get-PunkRuntimeCandidates $archfolder foreach ($f in $dircontents) { - $local_sha1 = Get-FileHash -Path $(${f}.FullName) -Algorithm SHA1 - $localdict[$f.Name] = ${local_sha1}.Hash + $local_sha1 = Get-PunkFileSha1 -Path $(${f}.FullName) + $localdict[$f.Name] = $local_sha1 } } diff --git a/src/scriptapps/bin/punk-runtime.ps1 b/src/scriptapps/bin/punk-runtime.ps1 index 090ffffa..ddcc9d5d 100644 --- a/src/scriptapps/bin/punk-runtime.ps1 +++ b/src/scriptapps/bin/punk-runtime.ps1 @@ -199,6 +199,41 @@ function Get-PunkRuntimeRootName { } return $name } +#Dependency-free SHA1 (robustness fix 2026-07-23): Get-FileHash is a SCRIPT-defined +#function in Windows PowerShell 5.1's Microsoft.PowerShell.Utility, resolved via +#PSModulePath autoloading - on machines with a damaged PSModulePath (pwsh7 module-path +#pollution, overridden env vars, redirected user module dirs) it vanishes while the +#module's COMPILED cmdlets (Write-Host, Invoke-WebRequest) still work, observed in the +#field ("Get-FileHash ... not recognized" under PS 5). Hashing here goes through .NET +#directly so fetch verification and list -remote comparison work on any PowerShell +#edition/state, with a one-time informational note when the damage is present. +#Returns lowercase hex (sha1sums.txt convention; -eq/-ne comparisons are +#case-insensitive so case never matters for verification). +$script:punkFileHashNoteShown = $false +function Get-PunkFileSha1 { + param([string] $Path) + if (-not $script:punkFileHashNoteShown) { + $script:punkFileHashNoteShown = $true + if ($PSVersionTable.PSVersion.Major -le 5 -and -not (Get-Command Get-FileHash -ErrorAction SilentlyContinue)) { + Write-Host "note: Get-FileHash is unavailable in this Windows PowerShell (PSModulePath on this machine may be damaged) - punk-runtime uses built-in .NET hashing and is unaffected" + } + } + #Resolve-Path: .NET's current directory is not PowerShell's location, so hand + #[System.IO.File] an absolute provider path + $fullpath = (Resolve-Path -LiteralPath $Path).ProviderPath + $sha1 = [System.Security.Cryptography.SHA1]::Create() + try { + $fs = [System.IO.File]::OpenRead($fullpath) + try { + $hashbytes = $sha1.ComputeHash($fs) + } finally { + $fs.Dispose() + } + } finally { + $sha1.Dispose() + } + return (($hashbytes | ForEach-Object { $_.ToString("x2") }) -join "") +} #G-103 artifact metadata: a runtime may carry a .toml beside it (emitted #by the buildsuite kit-family-artifacts step / fetched from punkbin). Returns a #short "[variant=... tcl=... rN ...]" summary for list output, "" when absent. @@ -473,8 +508,8 @@ function psmain { if (Test-Path -Path $output -PathType Leaf) { Write-Host "Runtime already found at $output" Write-Host "Checking sha1 checksum of local file versus sha1 of server file" - $file_sha1 = Get-FileHash -Path "$output" -Algorithm SHA1 - if (${file_sha1}.Hash -ne $stored_sha1) { + $file_sha1 = Get-PunkFileSha1 -Path "$output" + if ($file_sha1 -ne $stored_sha1) { Write-Host "$runtime on server has different sha1 hash - Download required" $need_download = $true } @@ -498,8 +533,8 @@ function psmain { } Write-Host "comparing sha1 checksum of downloaded file with data in sha1sums.txt" Start-Sleep -Seconds 1 #REVIEW - give at least some time for windows to do its thing? (av filters?) - $newfile_sha1 = Get-FileHash -Path "${output}.tmp" -Algorithm SHA1 - if (${newfile_sha1}.Hash -eq $stored_sha1) { + $newfile_sha1 = Get-PunkFileSha1 -Path "${output}.tmp" + if ($newfile_sha1 -eq $stored_sha1) { Write-Host "sha1 checksum ok" Move-Item -Path "${output}.tmp" -Destination "${output}" -Force Write-Host "Runtime is available at ${output}" @@ -731,8 +766,8 @@ function psmain { #local runtimes in the comparison $dircontents = Get-PunkRuntimeCandidates $archfolder foreach ($f in $dircontents) { - $local_sha1 = Get-FileHash -Path $(${f}.FullName) -Algorithm SHA1 - $localdict[$f.Name] = ${local_sha1}.Hash + $local_sha1 = Get-PunkFileSha1 -Path $(${f}.FullName) + $localdict[$f.Name] = $local_sha1 } }