Skip to content

fix: move the updater body out of cmd's 8,191 character command line - #7

Merged
StaticHumStudio merged 1 commit into
mainfrom
fix/update-bat-cmd-line-limit
Aug 6, 2026
Merged

fix: move the updater body out of cmd's 8,191 character command line#7
StaticHumStudio merged 1 commit into
mainfrom
fix/update-bat-cmd-line-limit

Conversation

@StaticHumStudio

Copy link
Copy Markdown
Owner

The blocker

update.bat built its entire PowerShell body from caret-continued double-quoted fragments. cmd joins those into one logical line and truncates it at 8,191 characters. The block had reached 17,182.

cmd kept the first 8,191 and discarded 8,991. The cut landed mid-word inside function Start-HumWatch:

kept  [8140:8191]: "tch service did not stop safely' \"  \"}; \"  \"f"
cut   [8191:8242]: "unction Start-HumWatch { \"  \"  if (Test-Path -Li"

PowerShell got a script with unclosed braces and failed at parse time. cmd then tried to execute the discarded remainder as a command. Exit 9009, no _update_status.txt, updater completely non-functional on every install.

It failed safely (the parse error lands well before Stop-HumWatch, so nothing was ever mutated) but v2.0.1 could not ship a working one-click updater.

The block was 5,193 characters from v0.9.5 through v1.0.1, so no published install is stranded. It outgrew the limit during the v2.0.x hardening work.

The fix

The body moves verbatim to scripts/update-core.ps1, invoked with -File. No update logic changed. update.bat keeps the banner, the fltmc elevation gate, and nothing else.

Three related fixes while the file was open:

  • The invocation is now the last line cmd reads. The release swap replaces update.bat mid-run, and cmd resumes a batch file by byte offset, so any following line would be read out of the new file at the old offset. exit /b rides the same already-buffered logical line, and the result banner moved into the PowerShell script. Verified that PowerShell keeps executing the parsed original after its own script file is overwritten, so mirroring scripts\ mid-transaction is safe.
  • %errorlevel% in the elevation block expanded at block-parse time under EnableDelayedExpansion. Demonstrated on Windows: in that block shape %errorlevel% reads 0 where !errorlevel! reads the child's real 77. It only propagated correctly before because exit /b with an empty argument preserves the current errorlevel.
  • A failed update now prints the underlying error and waits instead of closing the elevated console before the operator can read anything. Guarded by UserInteractive and IsInputRedirected so scripted runs never block.

Tests

The caret-block reconstruction helper is gone. In its place:

  • test_batch_entrypoints_stay_under_the_cmd_command_line_limit bounds every cmd logical line across all four batch entrypoints.
  • test_update_batch_is_only_a_launcher_for_the_powershell_core pins the launcher shape, the trailing exit /b, and the absence of %errorlevel%.
  • test_update_core_is_a_real_script_not_a_cmd_command_string keeps the body from moving back.
  • The updater contract tests read both halves. The ACL contract harness now cuts Set-HardenedAppAcl out of the real script instead of reconstructing it from cmd fragments.

The old #-comment regression test was correct and passing. It reconstructed the joined -Command source and never measured it, which is why CI missed a 17KB line.

Verification on Windows 11 (26100, PowerShell 5.1)

Check Result
update-core.ps1 parses under Windows PowerShell 5.1 PASS
Byte-faithful probe of update.bat through cmd (only the fltmc gate stubbed, same byte length, CRLF preserved) reaches the release check, clean error, exit 1
Real unmodified update.bat under an admin token, from the built v2.0.1 archive same, exit 1, no _update_status.txt
gh present (gh branch) PASS
gh off PATH (anonymous Invoke-RestMethod fallback, previously unexercised) PASS
Success path, staged version pinned to the published 1.0.1 writes marker, reads it back, prints "HumWatch is already current.", deletes marker, exit 0
PowerShell surviving replacement of its own script file mid-run PASS
build-release.ps1 with scripts\update-core.ps1 added to the required entries exit 0, sidecar matches
Full suite, Linux 239 passed, 5 skipped
Windows-gated suite 45 passed

The clean error in every run is The named release or its SHA256 manifest is missing, which is correct: the latest published release is v1.0.1 and it has no .zip/.sha256 assets.

Not covered

The UAC self-elevation branch was not re-run end to end here, since the SSH session already holds an admin token so fltmc succeeds. That branch was confirmed working on this hardware before this change, the Start-Process -Verb RunAs line is untouched, and the exit-code propagation it depends on is tested in isolation above.

A full successful update remains impossible to exercise until v2.0.1 assets exist.

🤖 Generated with Claude Code

update.bat built its whole PowerShell body from caret-continued
double-quoted fragments. cmd joins those into one logical line and
truncates it at 8,191 characters. The block had reached 17,182, so cmd
kept the first 8,191, cut mid-token inside `function Start-HumWatch`,
handed PowerShell a script with unclosed braces, and then tried to run
the discarded remainder as a command. Every install failed at parse time
with exit 9009 and never wrote _update_status.txt. Verified end to end on
Windows 11 under real UAC elevation against the built v2.0.1 archive.

The body now lives in scripts/update-core.ps1, invoked with -File. The
logic is unchanged. update.bat keeps the banner, the fltmc elevation
gate, and nothing else.

Three related fixes while the file was open:

- The invocation is now the last line cmd reads. The release swap
  replaces update.bat mid-run and cmd resumes a batch file by byte
  offset, so any following line would be read out of the new file at the
  old offset. `exit /b` rides the same buffered logical line, and the
  result banner moved into the PowerShell script.
- The elevation block read %errorlevel% and %UPDATE_EXIT% under
  EnableDelayedExpansion, so both expanded at block-parse time. It
  propagated correctly only because `exit /b` with no argument preserves
  the current errorlevel.
- A failed update now prints the underlying error and waits, instead of
  closing the elevated console before the operator can read anything.

Tests: the caret-block reconstruction helper is gone, replaced by a bound
on every cmd logical line across all four batch entrypoints, plus a shape
test pinning update.bat as a launcher. The updater contract tests read
both halves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@StaticHumStudio
StaticHumStudio merged commit 6b37f3b into main Aug 6, 2026
7 checks passed
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.

1 participant