Skip to content

Darling upgrade: a staging folder with [ or ] in its name no longer copies nothing under a success message (#4745) - #4762

Merged
erikdarlingdata merged 4 commits into
devfrom
fix/4745-upgrade-literal-path
Sep 29, 2026
Merged

erikdarlingdata merged 4 commits into
devfrom
fix/4745-upgrade-literal-path

Conversation

@erikdarlingdata

@erikdarlingdata erikdarlingdata commented Sep 29, 2026 •

Copy link
Copy Markdown
Owner

Fixes #4745

Why

PowerShell reads [ and ] in a -Path value as wildcard characters. The Darling upgrade script copied a folder -Source with Copy-Item -Path (Join-Path $Source '*'), so a staging folder named like build[1] matched nothing, threw nothing, and the script still printed "New build in place." over the old build. The install script's search for older darling.json.bak-* files used -Path the same way and found none to harden under such an install folder.

Measured on Windows PowerShell 5.1.26100 and PowerShell 7.6.6 (identical results), with a folder a[x]b holding two files and a subfolder with one file:

Statement Files that arrive Error
Copy-Item -Path (Join-Path $Source '*') -Destination $dst -Recurse -Force (old) 0 none
Get-ChildItem -LiteralPath $Source -Force | Copy-Item -Destination $dst -Recurse -Force (new) 3 none

The other wildcard-expanding calls under the same roots misread such a name the same way: Get-ChildItem -Path found no backups to harden, Get-Acl -Path and Set-Acl -Path could resolve to a different path than the backup, and a positional Test-Path returned False for a file that exists. Each now takes -LiteralPath.

A destination containing [ is not affected: a copy into an existing d[yz]e landed there and left a same-shaped sibling dye empty.

What changes

Darling/tools/upgrade-darling.ps1

  • The folder copy is now Get-ChildItem -LiteralPath $Source -Force | Copy-Item -Destination $InstallRoot -Recurse -Force. -LiteralPath on Copy-Item alone would make the * literal too, so the listing is piped in and each item binds its own literal path. The comment that quoted the old form is updated.
  • New check between "The copy did not complete." and "New build in place.": for a folder -Source only, it compares the SHA-256 of PerformanceMonitor.Darling.Service.exe in the source and in the install root. If they differ it stops with "The copy reported success, but the installed PerformanceMonitor.Darling.Service.exe is not the new build's, and the service is still STOPPED - re-run this script with the same arguments." A zip source is not checked; it is already read with -LiteralPath.
  • Nothing after the "New build in place." line changed.

Darling/tools/install-darling.ps1: every wildcard-expanding cmdlet that took a path under $root now uses -LiteralPath. Sites changed (line numbers on dev):

  • 771 Test-Path $serviceExe
  • 1036 Test-Path $configPath, 1037 Test-Path $samplePath
  • 1038 Copy-Item $samplePath $configPath, now Copy-Item -LiteralPath $samplePath -Destination $configPath
  • 1160 Get-ChildItem -Path $root -Filter 'darling.json.bak-*' (the reported site)
  • 1186 and 1202 Set-Acl -Path $secretFile; 1200 and 1210 Get-Acl -Path $secretFile
  • 1335 Test-Path $viewerExe

Left alone on purpose: Join-Path and New-Item (they do not expand wildcards); the two Get-ItemProperty -Path 'HKLM:\...' lookups (constant registry paths, not under $Source, $InstallRoot or $root); Split-Path; and uninstall-darling.ps1, whose Test-Path and Remove-Item calls take a shortcut path and the ProgramData folder, not a path under those roots.

Darling/Darling.Tests/DarlingDeployStaleFileTests.cs: the new tests below. RunWindowsPowerShell also now starts Windows PowerShell with the machine-level PSModulePath. Started from pwsh (the CI runner's default shell), Windows PowerShell 5.1 inherited the parent's module directories and could not find Get-FileHash ("The term 'Get-FileHash' is not recognized"); the executable-check test hit this when run from a pwsh window, and the machine-level value is what an operator's own 5.1 session starts with.

New tests (all in DarlingDeployStaleFileTests, which already parses these scripts):

  • TheDeployScript_CopiesAFolderSourceByLiteralPath_NotThroughAWildcard: text pin on the new copy statement; the old form must be gone.
  • TheInstallScript_FindsOlderConfigBackupsByLiteralPath: text pin on the .bak search.
  • TheUpgradeAndInstallScripts_NameEveryPathLiterally: census over the parsed scripts (Windows PowerShell AST). Any of 25 wildcard-expanding path cmdlets (aliases resolved) that is first in its pipeline and has no -LiteralPath, or that has -Path, fails it, so a positional path or a new -Path site is caught. Constant HKLM: paths are exempt.
  • TheDeployScript_ChecksTheInstalledServiceExecutableBeforeItReportsTheNewBuildInPlace: the hash check sits between the $copied check and "New build in place.", is gated on -not $sourceIsZip, and uses -LiteralPath.
  • TheShippedFolderCopy_CarriesEveryFileOutOfAStagingFolderWhoseNameHasBrackets: runs the copy statement the script really has (found by its place in the script, not its wording) against a staging folder a[x]b with a subfolder and a hidden file; every file arrives, a file the install already had is overwritten, and one the new build does not ship survives.
  • TheShippedExecutableCheck_StopsOnlyWhenAFolderCopyLeftADifferentServiceExecutable (3 cases): the shipped check, lifted out and run: same bytes pass, different bytes stop with the message, a zip source is skipped.

Older pins that quoted the old statement text now quote the -LiteralPath forms: DarlingFileSecurityTests.InstallScript_SetsTheOwnerOnTheCurrentDescriptor_SoTheHardenedDaclSurvives, DarlingInstallLocationTests.LocationGuard_RunsBeforeAnythingIsInstalled, DarlingInstallLocationTests.TheInstaller_NeverAclsTheInstallTree_OnlyTheCredentialFiles (its Set-Acl loop now searches Set-Acl -LiteralPath with a 40-character window, and a DoesNotContain for Set-Acl -LiteralPath $root sits beside the -Path one), DarlingInstallLocationTests.TheWritableTreeCheck_RunsBeforeEitherScriptChangesAnything and DarlingRuntimePreflightTests.RuntimeGate_RunsBeforeTheServiceExeIsEverInvoked. Run against dev's unchanged scripts, all five fail (the upgrade script's folder-copy marker also fails on its own when only that script is put back).

Test plan

  • Failed first against dev's scripts: 6 of the 20 tests in DarlingDeployStaleFileTests failed. The census listed exactly the 11 sites above (1 in the upgrade script, 10 in the install script). The behavioral copy test failed with Expected: "new one" Actual: "old one", that is, nothing was copied and nothing threw.
  • DarlingDeployStaleFileTests: 20 of 20 pass after the change, run from a pwsh window (the case where Windows PowerShell inherits pwsh's module directories, which the PSModulePath change handles).
  • dotnet build Darling/Darling.Tests/Darling.Tests.csproj: 0 Warning(s), 0 Error(s), before and after merging origin/dev.
  • The two scripts still parse under Windows PowerShell 5.1 and hold no byte above 127 (existing TheShippedScripts_* tests, part of the 20).
  • Full Darling.Tests suite without DARLING_TEST_PG, after merging origin/dev: Total: 17158, Errors: 0, Failed: 1, Skipped: 1121, Not Run: 1 (1119 of the skips need DARLING_TEST_PG or DARLING_TEST_PGRUNTIME; the other 2 are conditions of the machine the run was on). The one failure, ManagedConfMigrationRunnerTests.RunStepA_Mismatch_RestoresAndListsTheKey (simulated crash between steps), does not come from this change: ManagedConfMigrationStepsTests sets the static ManagedConfMigrationSteps.FailBetweenSteps to throw, neither class carries a [Collection] attribute, and the stack shows that hook firing inside the runner test. ManagedConfMigrationRunnerTests alone: 24 of 24 pass; run together with ManagedConfMigrationStepsTests, a runner test fails again. This PR touches neither class nor ManagedConfMigrationSteps. The eleven classes that read these scripts, run alone first: Total: 209, Failed: 0, Skipped: 0.
  • Live PostgreSQL tests: not run (they need a database; CI runs them).
  • Not exercised: a real upgrade against a stopped Windows service (this needs an elevated machine with the service installed); the executable check was run only through the extracted-block tests above.

CHANGELOG

SECTION: Fixed
ENTRY:

…s the copy or the backup hardening match nothing (#4745)

PowerShell reads [ and ] in a -Path value as wildcard characters. The
upgrade script copied a folder -Source with Copy-Item -Path over
"$Source\*", so a staging folder named build[1] matched nothing, threw
nothing, and the script still printed "New build in place." over the old
build. The install script's search for older darling.json.bak-* files
used -Path the same way and found none, and its Get-Acl/Set-Acl -Path
calls could resolve to a same-named file in a sibling folder.

- upgrade-darling.ps1: the folder copy is now
  Get-ChildItem -LiteralPath $Source -Force | Copy-Item -Destination
  $InstallRoot -Recurse -Force.
- upgrade-darling.ps1: for a folder -Source, the script compares the
  SHA-256 of the service executable in the source and in the install
  root before it says "New build in place.", and stops with a plain
  message if they differ.
- install-darling.ps1: every Test-Path, Copy-Item, Get-ChildItem,
  Get-Acl and Set-Acl call on a path under the install folder now uses
  -LiteralPath.
- Tests: text pins for both statements, a parsed-script census that
  fails on any wildcard-expanding cmdlet given a bare path, the guard's
  place between the copy check and the success message, and behavioural
  runs of the shipped copy statement (staging folder a[x]b) and of the
  shipped executable check.
… scripts now use (#4745)

DarlingFileSecurityTests, DarlingInstallLocationTests and DarlingRuntimePreflightTests
quoted the -Path statement text the scripts had before #4745. Each pin now quotes the
-LiteralPath form: Get-Acl and Set-Acl on $secretFile, Copy-Item of the sample config, and
the folder copy in the upgrade script.

The Set-Acl loop in TheInstaller_NeverAclsTheInstallTree_OnlyTheCredentialFiles searched
"Set-Acl -Path ", which no longer occurs in code, so it counted nothing. It now searches
"Set-Acl -LiteralPath " with a window wide enough for the whole 32-character call, and a
DoesNotContain for "Set-Acl -LiteralPath $root" sits beside the -Path one.
@erikdarlingdata
erikdarlingdata marked this pull request as ready for review September 29, 2026 08:58
@erikdarlingdata
erikdarlingdata merged commit 110b11c into dev Sep 29, 2026
17 of 18 checks passed
@erikdarlingdata
erikdarlingdata deleted the fix/4745-upgrade-literal-path branch September 29, 2026 09:05
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