Repository navigation
Darling install scripts refuse a writable-by-others install tree before the lock (#4043) - #4050
Conversation
…eady write to (#4043) The #4038 lock only stops writes from the moment it runs; it cannot undo one made before it existed. A folder made directly under C:\ inherits Authenticated Users: Modify from the volume root the instant it is extracted, so a local user could swap the service exe, a DLL, or a pg-runtime binary before install-darling.ps1 ever reaches its lock step, and the lock would never notice. install-darling.ps1 now checks the install root and the service exe's own ACL first (step 1a, before 1b2's lock), naming any principal outside SYSTEM/Administrators/TrustedInstaller/the installing admin that already holds a write grant, and refuses unless -AcceptWritableExtraction is passed for a deliberate dev loop. upgrade-darling.ps1 carries the same check on a folder -Source before it copies that folder's content over the already-locked install root - a folder -Source has no content-hash check the way a zip -Source does, so it is its own pre-lock writable-extraction window. The Authenticode/hash-manifest options from the original issue are withdrawn per the issue's own revised plan: a check run BY this script only catches an attacker who leaves the script untouched, and the same attacker could replace the script itself. README updated to prefer C:\Program Files\PerformanceMonitorDarling over a bare C:\ folder, and to describe the new refusal. Part 2 (narrowing the service account's Modify to pg-runtime only) is not done - see the PR body for the audit and why. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GdmA4ND1wLSqA91ax1m4xv
…#4038 lock's own grant (#4043) Round-1 review found the pre-lock check refused C:\Program Files and C:\Program Files\dotnet (CREATOR OWNER is an inherit-only template, not a real grant) and would false-refuse any re-run or upgrade over a tree #4038 already locked (the service account's own Modify grant read back as a stranger's). Excludes the four CREATOR */SERVER inherit-only template SIDs by SID, adds a direct owner check (covers OWNER RIGHTS, which is not one of those four), and lets Get-DarlingPreLockTrustedSids trust an already-registered service's current logon account. Adds four realistic-ACL test cases, including a red-watch proving the old code refused Program Files. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GdmA4ND1wLSqA91ax1m4xv
Security review, round 1 (#4043 pre-lock ACL check)Scope: HighH1. The zip
MediumM1. The documented upgrade procedure is now refused, and the docs still recommend the writable pattern this issue is about.
M2. Checking only the root and the exe misses the case where the root was re-permissioned after extraction, and the lock then erases the evidence.
M3.
Low
Clean
|
…ot check (#4043 round-1) Addresses round-1 security review findings on the #4043 pre-lock ACL check: H1 (zip -Source hashed and extracted from a protected staging copy), M1 (docs point at a protected staging folder, not C:\staging), M2 (the pre-lock walk is now recursive, with a direct-Administrators-member trust exception), M3 (upgrade-darling.ps1 now refuses a pre-existing writable -InstallRoot before its lock), and L1-L5 wording and message fixes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GdmA4ND1wLSqA91ax1m4xv
…eMonitorDarling, so the #2185 refusal no longer sends people into the #4043 one (#4043) install-darling.ps1's profile-path refusal (#2185) told users to move the install to C:\PerformanceMonitorDarling. A folder made directly under C:\ inherits Authenticated Users: Modify, and this PR's pre-lock check refuses exactly that folder, so following one refusal landed users in the other. The refusal, the script's help text, the service's own install-location message (DarlingInstallLocation.DocumentedInstallDirectory), the README's commands and the retention runbook all name C:\Program Files\PerformanceMonitorDarling now. The refusal also says to extract fresh rather than move, as the #4043 message does. The README's manual sc create quotes the binPath, which the documented path now needs. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GdmA4ND1wLSqA91ax1m4xv
…rling from an elevated shell, so following it no longer ends in the #4043 refusal (#4043) bf2f5bc swept the README, the scripts, the runbook and the service's own messages, but missed docs/uat-onboarding.md, which still called C:\PerformanceMonitorDarling "the documented location and you should use it". Someone following it step by step would reach 1.6 and have install-darling.ps1 refuse the folder the doc told them to make. - 1.2 now says to verify from an elevated PowerShell in the download folder, since 1.3 extracts into Program Files, which only administrators can write. - 1.3 extracts to the Program Files path and says why a folder made directly under C:\ is refused. - 1.4 says to stay elevated so Notepad can save darling.json there. - Every later command quotes the path, and the bare exe invocations gain the call operator (&) that a quoted path needs. - A test comment explaining #1647 names the documented folder, which inherits BUILTIN\Users read and execute from Program Files rather than from the root DACL. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GdmA4ND1wLSqA91ax1m4xv
…ly even when it starts with explicit entries (#4043) CI's elevated runner failed ProtectDarlingStagingFolder_LocksTheFolderToSystemAndAdministratorsOnly with three explicit rules where two were expected. icacls /inheritance:r strips only INHERITED entries. A folder created under a parent that passes nothing on gets its creator's default DACL as EXPLICIT entries, and those survived the strip, so the folder kept a third full-control grant. Protect-DarlingStagingFolder now runs /reset first, which replaces every explicit entry with what the parent passes on; /inheritance:r then removes those, and the two grants are the whole DACL. A new test builds a parent that passes nothing on, so any machine reproduces the case, elevated or not. It asserts the folder starts with explicit entries, then that only SYSTEM and Administrators remain. Red-watch: with /reset removed the new test fails; with it, both staging tests pass. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GdmA4ND1wLSqA91ax1m4xv
Security review, round 2 (final) -- #4043 pre-lock ACL checkReviewed through git at Round-1 findings -- all verified CLOSED
New -- introduced by the round-2 fixesM-a (Medium) -- the recursive walk fails OPEN on a directory it cannot enumerate. L-a (Low) -- sibling docs still steer operators into the folder 1a now refuses. M1 fixed y3 coverage gap -- real but low, and cheaply closableThe SHA256SUMS-sidecar gate ( Checked and clean
Net: functionally sound and a clear improvement over round 1. One fail-open (M-a) worth a follow-up; the rest is Low/doc/coverage. No independently-exploitable High found. |
Security review, round 2 -- addendum:
|
…t skip (#4043 round-2 review) - Get-UntrustedWriteGrantees (install and upgrade, still identical) captures the recursive Get-ChildItem's errors and reports each unlisted folder, so a subtree that refuses listing can no longer hide a swapped binary behind a clean-looking result. - The loader diagnosis names the documented install directory from the one constant, which moves to a platform-neutral DarlingInstallPaths so any platform can name it (CA1416). - The staging-lock test plants its explicit entry directly instead of relying on the creator's default DACL, which CI's runner does not supply. - Wiring pins: SHA256SUMS.txt is read once, only after its folder passes the check and its refusal; each script checks its tree before anything that changes state. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GdmA4ND1wLSqA91ax1m4xv
|
Round-2 disposition: commits 36c6823 and 563d4a4 fix every item from the review and its addendum, instead of filing them.
The CI failure on f2d14d6 came from this PR's own staging test. The test relied on a default DACL that CI's runner does not supply. It now plants its entry directly. Each new test fails with its fix reverted, and the full Darling suite passes locally. The |
…port says to stay elevated (#4043 round-2 addendum) The --configure-firewall, --export-viewer-config and --print-web-token blocks called .\ with no cd of their own, so they only worked in the same shell as an earlier cd. --export-viewer-config writes viewer-config\ beside the service's darling.json inside Program Files, so the step now says to stay elevated. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GdmA4ND1wLSqA91ax1m4xv
…ntries in their sections (#4080) Adds 42 entries and 42 link refs (#3992, #3995, #3996, #3998, #4001, #4002, #4003, #4007, #4010, #4011, #4013, #4015, #4020, #4022, #4025, #4029, #4030, #4031, #4036, #4038, #4039, #4040, #4044, #4047, #4048, #4049, #4050, #4051, #4055, #4061, #4063, #4064, #4065, #4066, #4067, #4068, #4069, #4070, #4071, #4073, #4074, #4078). Each PR's entry was buffered, and this lands every entry whose PR was merged on origin/dev when it ran. #3989 left 26 entries under bare 'Changed' and 'Fixed' lines above '### Added'. They move into '### Changed' and '### Fixed', below the new entries, and one blank line stays under [Unreleased]. Claude-Session: https://claude.ai/code/session_01Ua31ugERL5DmhFVRtf6keQ Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Closes #4043.
Why
#4038's review left two residuals. The issue's last comment withdrew the Authenticode/hash-manifest
option for part 1 and replaced it: a check run BY install-darling.ps1 AFTER the lock only catches an
attacker who leaves the script itself untouched, and the same local user who can replace the service
exe can just as easily replace
install-darling.ps1, which the admin then runs elevated. The trustroot is where the admin extracted the zip, not anything a script reads back from inside that tree.
What changes
Part 1 (implemented, matches the revised plan).
install-darling.ps1step 1a (new, before 1b2's lock) reads the install root's ACL and theservice exe's own file ACL. Any Allow ACE granting write/append/delete/change-permissions/
take-ownership to a principal outside SYSTEM, Administrators, TrustedInstaller and the admin running
the script refuses the install, names the principals, says files may already have been replaced, and
points at
C:\Program Files\....-AcceptWritableExtractionskips it (documented as a dev-loopescape hatch, not for a real install).
upgrade-darling.ps1gets the same check, and it has its own real pre-lock window: a zip-Sourceis covered by the existing SHA256 check regardless of who could write to its folder (thehash is of the zip's content), but a folder
-Sourcehas no content check at all - the scriptonly confirms the service exe's name is present and says outright "this script cannot verify it." So
the folder-
-Sourcebranch now runs the same ACL check (same trusted set, same-AcceptWritableExtraction) before the overlay copies that folder over the already-locked-InstallRoot.Get-UntrustedWriteGrantees,Get-DarlingPreLockTrustedSids) arebyte-identical in both scripts, same idiom as
Lock-DarlingInstallTree- checked by a new xunit testthe same way
DarlingInstallLocationTestsalready checks the lock andGet-DarlingServiceLogonName.C:\Program Files\PerformanceMonitorDarlingover a bare
C:\folder and explains why (Program Files denies ordinary users write by default; afolder directly under
C:\inheritsAuthenticated Users: Modifythe instant it's extracted), anddocuments the new refusal and switch. The scripted-install summary mentions the new step 1a and links
the switch.
Part 2 (audit done; NOT narrowed - leaving Modify as #4038 shipped it).
Static audit of every file/directory write the service process makes, by grepping
AppContext.BaseDirectoryand the runtime-root fields throughDarling/PerformanceMonitor.Darling.Service:pg-runtime\(extract, rescue-copy topg-runtime-prev\,pg-runtime.sha256/.stamp/.blockedstamp files)DarlingManagedPostgres.cs(_runtimeRoot = AppContext.BaseDirectory + "pg-runtime"),DarlingStoreUpgrade.cs(stamp/blocked paths allPath.Combine(runtimeRoot, ...))pg-runtime/pg-runtime-prevdarling.jsonDarlingConfig.ResolveConfigPath/Loadinstall-darling.ps1step 4b and the--configure-networkCLI verb, both running as the elevated admin, not the service accountDarlingInstallLocation.Report, called fromDarlingWorker.cswwwroot/ web content rootMcp/DarlingWebHostService.cs(ContentRootPath = AppContext.BaseDirectory)BringYourOwnDirectoryNamefolder besidedarling.json, i.e. inside the install root by defaultDarlingLogHashKeyFile.DirectoryFor- only whenpostgres.managed = falseAND not running in a container (the non-default, bring-your-own-Postgres case; the default managed path resolves beside the data directory under%ProgramData%, outside the tree)pg-runtimethat the proposed narrowing (Modify onpg-runtime/pg-runtime-prevonly) would break for anyone running BYO-Postgres on Windows outside a containerGiven the BYO-Postgres log-hash-key-directory finding above, and that the issue itself requires
narrowing to be "verified with a real-service install on a test box" - which this lane's hard fences
(never install/start the real Darling service) rule out doing here regardless - Part 2 is left
unchanged. Narrowing needs: (a) resolving the BYO-Postgres write site (either fold it into the grant
or confirm nobody runs that combination on Windows), and (b) an actual install-and-run verification on
a disposable box. Recommend a follow-up lane sized for that, not folded into this one.
Test plan
dotnet build Darling/Darling.Tests/Darling.Tests.csproj -c Debug- Build succeeded, 0 Warning(s), 0 Error(s).[System.Management.Automation.Language.Parser]::ParseFile, no syntax errors).Darling.Tests.exe -class "*DarlingInstallLocationTests*"- Total: 14, Errors: 0, Failed: 0 (13 pre-existing + 2 new: the byte-identity check and the red-watch check; one pre-existing test file, two new[Fact]s).Get-UntrustedWriteGranteestoreturn @()ininstall-darling.ps1(nothing else changed) and re-ran the new test - it failed (1 Failed) with the no-op in place, then reverted and re-ran the full class green (14/14). Confirms the test actually exercises the shipped function, not a tautology.Darling.Testssuite - not run; this lane hit the context/time budget after the targeted class passed. The change only touches two.ps1scripts, one test file, and README prose, so the blast radius outsideDarlingInstallLocationTestsshould be nil, but the coordinator should run the full suite once before arming.Round-1 fixes
The coordinator ran this PR's own
Get-UntrustedWriteGranteesagainst real folders on this machine andfound two verified defects, both fixed here:
False refusal on the very location the refusal message recommends.
CREATOR OWNERonC:\Program FilesandC:\Program Files\dotnetwas flagged as an untrusted grantee. CREATOR OWNER,CREATOR GROUP and their two SERVER twins (
S-1-3-0..3) are inherit-only templates - they grant nothingon an object that already exists, only rights a CHILD inherits when someone later creates one, and
creating that child needs write/create rights on the parent, which the check already tests for every
other principal. Fixed by excluding those four SIDs by value inside
Get-UntrustedWriteGrantees, not byadding them to
$trusted(a caller must never be able to trust them for real by omission the way a namein
$trustedwould).OWNER RIGHTS(S-1-3-4) is different and is not in that exclusion: it redefines what the owner maydo instead of naming a risk on its own, so what matters is WHO the owner is - an owner outside the
trusted set holds
WRITE_DAC/WRITE_OWNERimplicitly and can grant itself anything regardless of thecurrent DACL.
Get-UntrustedWriteGranteesnow also checks the owner of$pathdirectly (the same factLock-DarlingInstallTree's own post-lock walk already acts on,"owned by ..."), which covers this casewithout trying to interpret the ACE.
False refusal on a re-run or upgrade over a tree Install and upgrade lock the Darling install folder, so an ordinary local user can no longer replace the service's binaries (#4034) #4038 already locked. Install and upgrade lock the Darling install folder, so an ordinary local user can no longer replace the service's binaries (#4034) #4038's lock grants the
Darling service account Modify on the install root, and that account was not in
Get-DarlingPreLockTrustedSids's static set - so the very first re-run, repair, or reconfigure over analready-locked install would have refused itself over the grant Install and upgrade lock the Darling install folder, so an ordinary local user can no longer replace the service's binaries (#4034) #4038 itself made.
install-darling.ps1can run over an existing install:$existing/$isUpgradealready drive areal fresh-vs-upgrade branch later in the script.
$existingis now resolved earlier, ahead of step1a instead of only at step 1b, so 1a's pre-lock check can tell a re-run apart from a fresh extraction.
upgrade-darling.ps1's check only ever looks at a folder-Sourceand its exe - never at-InstallRoot- so in the normal flow it does not see the locked installed tree at all. It can stillsee it if
-Sourceitself is (or is derived from) a previously-locked Darling tree, so the same fixwas applied there too for correctness, at negligible cost.
Get-DarlingPreLockTrustedSidsnow takes an optional$existingServiceAccount, normalized exactly theway
Lock-DarlingInstallTreenormalizes it (LocalSystem spelled out, a leading.\read as thiscomputer). Both call sites pass the current service's logon account (via the existing
Get-DarlingServiceLogonName) only when a service by that name is already registered.Tests (
Darling/Darling.Tests/DarlingInstallLocationTests.cs, three new[Fact]s, realistic ACLsinstead of idealized ones):
ThePreLockWritableExtractionCheck_IsSilentOnRealInheritOnlyTemplates_ButStillCatchesARealBroadGrant-(a) a temp folder carrying
C:\Program Files' own captured DACL must PASS; a pre-round-1 copy of thefunction reproduced inline (not read from git history) is run over the same DACL and must still flag it -
a red-watch built into the pin, not a one-off manual check; (b) a temp folder carrying
C:\'s own DACL(Authenticated Users: Modify) must REFUSE; (c) all four inherit-only template SIDs, granted explicitly,
must be silent regardless of what this box's Program Files happens to carry.
ThePreLockWritableExtractionCheck_TrustsTheAccountTheLockItselfGranted_OnAnAlreadyLockedTree- a treeactually locked via
Lock-DarlingInstallTreewith a stand-in service account (NT AUTHORITY\LOCAL SERVICE, deliberately not one of the four base-trusted SIDs) is flagged without the account and silentwith it.
ThePreLockWritableExtractionCheck_FlagsAnUntrustedOwner_EvenWithAFullyTrustedDacl-Get-Aclisshadowed (the same idiom the existing WMI-unavailable test uses) to hand back a fully-trusted DACL with
only the owner outside the trusted set, since Windows will not let this suite retarget a real object's
owner to an arbitrary SID without elevation.
Manually red-watched beyond the automated case above: ran the shipped (pre-fix)
Get-UntrustedWriteGranteesagainst real
C:\Program FilesandC:\Program Files\dotneton this machine and confirmed both wereflagged; confirmed the fixed version is silent on both while still flagging a real
Authenticated Usersgrant on
C:\itself.dotnet build Darling/Darling.Tests/Darling.Tests.csproj -c Debug- Build succeeded, 0 Warning(s), 0 Error(s).[System.Management.Automation.Language.Parser]::ParseFile).Get-UntrustedWriteGrantees/Get-DarlingPreLockTrustedSidsconfirmed byte-identical between the two scripts.Darling.Tests.exe -class "*DarlingInstallLocationTests*"- Total: 17, Errors: 0, Failed: 0 (14 prior + 3 new).$existingServiceAccounttrust from the shippedinstall-darling.ps1(upgrade script untouched) andre-ran the class - 4 failed (the 3 new Facts plus the byte-identity check, exactly as expected since
only one script was neutered); reverted and re-ran green (17/17).
Darling.Testssuite - Total: 13116, Errors: 0, Failed: 0, Skipped: 631 (unrelated live/DBtests with no PG rig running in this lane).
install/start of the real Darling service here); unchanged from round 1, still recommended before
shipping.
Part 2 (the static write-site audit and the decision to leave the service account's Modify grant
unnarrowed) is unchanged from round 1 below - still handed off, not folded into this lane.
Round-1 security review (disposition)
Findings and comment: #4050 (comment).
Applied on top of the code above, same branch.
-Sourcenot covered regardless of who could write to its folder. Fixed. Beforehashing, the zip is copied into a fresh staging folder the script creates (
New-DarlingProtectedStagingFolderProtect-DarlingStagingFolder, upgrade-only) with an explicit protected ACL: SYSTEM and Administratorsonly, full control, inheritance removed via
icacls /inheritance:r, owner set to Administrators. Thestaged copy is hashed and is the only thing
Expand-Archiveever reads;$Sourceis reassigned to itright after the hash check. The folder is deleted in a
finallythat wraps the rest of the script (seebelow), on every failure path, not just success. Without
-Sha256,SHA256SUMS.txtis trusted only whenits own folder passes the identical recursive check a folder
-Sourcegets (M2); otherwise the scriptrefuses and points at
-Sha256with the hash from the release page. With-Sha256, the sidecar folder isnever even looked at - the hash of the protected copy is the whole proof.
wrapped in
try { ... } finally { remove $zipStagingFolder }, not re-indented, to keep the diffreviewable instead of touching every intervening line. Verified empirically that PowerShell's
exit(which is how
Fail()terminates) still runs an enclosingfinally, whiletrapdoes not catchexit- sofinallyis the only mechanism that actually guarantees cleanup acrossFail()calls betweenstaging and extraction.
Darling/README.md's upgrade section nowextracts to
C:\Program Files\PerformanceMonitorDarling-staging\<ver>from an elevated shell, with onesentence on why (the folder you run the script from is the trust root). The zip+
-Sha256form is stilldocumented as an alternative.
upgrade-darling.ps1's.PARAMETER Sourcehelp, the self-overwrite refusalmessage, and the
.PARAMETER AcceptWritableExtractionhelp all point at the protected folder instead ofC:\staging\<version>.Fixed.
Get-UntrustedWriteGranteesgained a-Recurseswitch: the root itself is still checked for bothexplicit and inherited grants (that is how the
C:\hole shows up), but every descendant is checked for(a) an untrusted owner (never inherited, so a re-permissioned root can't hide who created it) and (b)
an explicit (non-inherited) write grant to an untrusted principal - inherited grants on a child just
repeat what the root already carries, which is checked once, not per file. A junction/link below the root
is reported, not silently skipped (parity with
Lock-DarlingInstallTree's own walk). The "for no benefit"comment is corrected. Trusted principals now also include any principal that is a direct member of
BUILTIN\Administrators (
Get-LocalAdministratorsDirectMemberSids, via WinNT ADSI rather thanGet-LocalGroupMember, which throws on an orphaned SID) - nested/domain membership is not resolved; therefusal names the principal and
-AcceptWritableExtractionis the accepted way past it. If the groupitself can't be enumerated at all, nothing is added (fail closed).
Darling/artifacts/pg-runtime.zip,1,661 files): the recursive walk took 1.6 seconds. Well under the ~10s threshold, so it stays
Get-ChildItem -Recurse | Get-Aclrather than moving to.NETenumeration.upgrade-darling.ps1never checked-InstallRootitself. Fixed. Before its lock, it nowruns the same recursive check on
-InstallRoot(reusing the trust set computed once for both this and thefolder-
-Sourcecheck) and refuses unless-AcceptWritableExtractionis passed. The message says: a treeinstalled before Install and upgrade lock the Darling install folder, so an ordinary local user can no longer replace the service's binaries (#4034) #4038 was writable by the named principals for its whole life so files may have been
replaced; the safe path is a fresh install into a protected folder; the switch accepts the risk; this fires
once, because the lock that follows closes the tree.
(
WriteAttributes,WriteExtendedAttributes) toGet-UntrustedWriteGrantees's mask, matchingLock-DarlingInstallTree's exactly - the comment is now literally true rather than reworded around the gap.-AcceptWritableExtractionhelp and the folder--Sourceinline comment: reworded to "a folder in your ownprofile ... writable by anything already running as you, elevated or not."
finding. Fixed via
Get-DarlingPreLockTrustedSidsForRerun: when the service is registered but its logonaccount can't be read at all, it fails with 1b2's own existing message (naming
sc.exe qc) instead ofreaching the generic writable-folder refusal.
account name is readable but won't resolve to a SID (
Resolve-DarlingServiceAccountSidreturns$null-e.g. a gMSA an unreachable DC can't answer for), the script now fails naming the account and why, instead of
later showing its own lock's grant as a stranger's raw SID.
one sentence to the README next to the staging-folder guidance: trusting the installing admin's account also
trusts that same admin's non-elevated session, since it's the same account. Listed here as accepted, per the
ruling.
Test plan (round-1 fixes)
[System.Management.Automation.Language.Parser]::ParseFile- no errors.Darling.Testsbuild - 0 Warning(s), 0 Error(s).DarlingInstallLocationTests- Total: 21, Failed: 0 (17 prior + 4 new: recursive walk catches a childgrant/owner miss and stays silent on a clean recursive tree and a junction; a direct Administrators
member enumerates;
Get-DarlingPreLockTrustedSidsForRerunfails with a named reason for both L3 and L4instead of silently mis-trusting; the staging folder's ACL is protected).
ThePreLockWritableExtractionCheck_ShipsIdenticallyInTheInstallAndUpgradeScripts)extended to the three new shared functions (
Get-LocalAdministratorsDirectMemberSids,Resolve-DarlingServiceAccountSid,Get-DarlingPreLockTrustedSidsForRerun) and stays green.Get-UntrustedWriteGrantees(if ($false -and $Recurse)) -the new recursion test went red (1 Failed); reverted, rebuilt, back to green (21/21).
pg-runtimetree: 1.6s (see M2 above).Darling.Testssuite - Total: 13120, Errors: 0, Failed: 0, Skipped: 631 (live/PG tests; no rigrunning in this lane - acceptable here since this PR touches scripts, one test class and docs, not the
store).
DARLING_TEST_PGrig was intentionally not stood up for this pass (script/docs/test-class-only change).-InstallRootand staging-folder behavior -still out of this lane's hard fences (never register/start/stop a real Darling service); recommended
before shipping, as round 1 already noted for the rest of this check.
Coverage gaps, said out loud rather than left silent:
function it calls,
Get-UntrustedWriteGrantees -Recurse, IS covered directly). Time-boxed out of this pass;a good five-minute follow-up if anyone wants the belt-and-suspenders coverage.
upgrade-darling.ps1's M3-InstallRootblock itself (it is inlinetop-level script flow, not an extractable function) - covered indirectly through
Get-DarlingPreLockTrustedSidsForRerunandGet-UntrustedWriteGrantees -Recurse, which is the logic itcomposes.
install-darling.ps1(C:\PerformanceMonitorDarling, which 1a itself refuses) is fixed in the coordinator pass below.Coordinator pass (bf2f5bc, 8c00707, f2d14d6)
The staging lock left a third full-control grant on CI's elevated runner (f2d14d6).
ProtectDarlingStagingFolder_LocksTheFolderToSystemAndAdministratorsOnlyfailed there with three explicit rules where two were expected.icacls /inheritance:rstrips only inherited entries. A folder created under a parent that passes nothing on gets its creator's default DACL as explicit entries, and those survived.Protect-DarlingStagingFoldernow runs/resetfirst. A new test builds such a parent so any machine reproduces the case; reproduced locally (3 rules), fixed (2), and red-watched (the new test fails with/resetremoved).The #2185 refusal sent people straight into the #4043 one. The service-account-profile refusal told users to
move the folder to
C:\PerformanceMonitorDarling, and this PR's pre-lock check refuses exactly that folder: afolder made directly under
C:\inherits write for every signed-in user. Every pointer to the install folder nownames
C:\Program Files\PerformanceMonitorDarling:install-darling.ps1(help, comments, and the [BUG] #2185 message, which now says to extract again from an elevatedsession and copy
darling.jsonacross);upgrade-darling.ps1's ImagePath comment;DarlingInstallPaths.DocumentedInstallDirectory(moved out of the Windows-onlyDarlingInstallLocationin36c6823), and the matching comment in
DarlingWorker;Darling/README.md(the location section,upgrade-darling.ps1examples,dotnet publish -o,sc createbinPath with inner quotes,
icacls);docs/retention-hold-runbook.md;docs/uat-onboarding.md(8c00707). 1.2 now verifies from an elevated PowerShell, 1.3 extracts into ProgramFiles and says why a folder directly under
C:\is refused, 1.4 stays elevated so Notepad can save there, andevery later command quotes the path (with
&where a quoted exe is invoked).install-darling.ps1already registers the service with an inner-quoted binPath, so the space in the path issafe. Test fixtures and changelog history that name the old folder are left alone: they model paths that real
installs used.
Verified: Darling.Tests builds with 0 warnings; 860 tests across the touched classes pass locally (94 after
8c00707: DarlingFileSecurityTests and DocCommentHygieneTests); both scripts parse with 0 errors. CI runs the
full suite.
Round-2 review (disposition, 36c6823 and 563d4a4)
The final review found no
High. This PR fixes all three of its items instead of filing them:
Get-UntrustedWriteGrantees(installand upgrade, still byte-identical) now passes
-ErrorVariableto its recursiveGet-ChildItem. Each folderthat the walk cannot list becomes a finding,
<path> (its contents could not be listed: UnauthorizedAccessException).The check refuses on it as it does on a write grant. The new theory
ThePreLockWritableExtractionCheck_ReportsAFolderItCannotList_RatherThanSkippingItcovers both scripts. Itdenies the running account List Folder on one child and trusts that account everywhere else. With the fix
reverted, it fails for both scripts.
C:\PerformanceMonitorDarling.DarlingToolExitCodenowappends
DarlingInstallPaths.DocumentedInstallDirectory. The constant moved out of the Windows-onlyDarlingInstallLocationinto a new platform-neutral class,DarlingInstallPaths. The service also builds forLinux, and CA1416 flagged the new call site. Commit 8c00707 already fixed the UAT-doc half of L-a.
TheSha256SumsSidecarIsReadOnlyAfterItsFolderPassesTheRecursiveCheckpins the order: the check, then itsFailrefusal, then the read. It also pins that every use of$sumsis the path, the existence test, orthat one read.
TheWritableTreeCheck_RunsBeforeEitherScriptChangesAnythingpins each script's tree check ahead of every stepthat changes state:
copy, and the service start
sc create, the config ACL,and the service start
The review's addendum on 8c00707 (comment)
found no High. Commit 563d4a4 fixes its nits in
docs/uat-onboarding.md. The--export-viewer-configstep now says to stayelevated, because it writes into the install folder. The three commands that started with
.\and nocdoftheir own now run the service exe by its full quoted path.
The addendum also noted a pre-existing Low. From UAT step 1.4 until install step 4b,
BUILTIN\Userscan readdarling.json, and under Program Files so canALL APPLICATION PACKAGES. This PR does not change that. After step 4b,darling.jsonkeeps itsNT AUTHORITY\INTERACTIVEread grant by design (#1792), because the viewer reads the file. #1792 accepted thatgrant, and closing either window needs a design change that #1792 considered and did not make.
CI failure on f2d14d6
The failure was this PR's own new test.
ProtectDarlingStagingFolder_StripsExplicitEntriesTheFolderWasCreatedWithdepended on the creator's default DACL to supply an explicit entry, and CI's runner supplies none
(
explicitBefore=0). The test is nowProtectDarlingStagingFolder_StripsExplicitEntriesAlreadyOnTheFolder. Itplants
BUILTIN\Users:(OI)(CI)Mon the folder itself and asserts that the entry is there before the lock runs.With
/resetremoved, the test fails with 3 rules left.Verification
CHANGELOG entry
SECTION: Fixed
ENTRY:
C:\, the folder inherits write access for every signed-in user. The Install and upgrade lock the Darling install folder, so an ordinary local user can no longer replace the service's binaries (#4034) #4038 lock stops writes only from the moment that it runs, so it could never catch a binary that was swapped before then.install-darling.ps1andupgrade-darling.ps1now check every file in the tree, and the existing install, before they change anything. They name who can write to the tree, and each folder that they cannot look inside. Then they refuse, unless you pass-AcceptWritableExtractionfor a deliberate dev loop. An upgrade copies its zip to an administrators-only folder and verifies the zip there before it extracts anything. Every doc and message now namesC:\Program Files\PerformanceMonitorDarlingas the install folder. This includes the refusal for a folder inside a user profile, which used to send people to a folder that this check refuses.REF:
[Darling install scripts refuse a writable-by-others install tree before the lock (#4043) #4050]: Darling install scripts refuse a writable-by-others install tree before the lock (#4043) #4050
Handoff
If a future lane picks it up: resolve the BYO-Postgres log-hash-key directory site first, and add
pg-runtime,pg-runtime-prev, and (if kept) the BYO key directory to the grant, RX on everythingelse, verified with an actual install/upgrade/restart cycle.
Mcp/DarlingWebHostService.cs'sContentRootPathwrite behavior was not independently verified(believed read-only, ASP.NET static content root) - worth a five-minute look before anyone relies on
the audit table as exhaustive.
Darling/README.mdare rewritten too; see the coordinator pass below..git-commit-msg-4043.txtsits untracked in this lane's worktree (used to passthe commit message safely); it was never staged or committed and can be ignored/deleted with the
worktree.
🤖 Generated with Claude Code
https://claude.ai/code/session_01GdmA4ND1wLSqA91ax1m4xv