Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions Darling/Darling.Tests/DarlingInstallLocationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -841,6 +841,54 @@ function Get-Acl {
Assert.Contains(answers, a => a.StartsWith("text=", StringComparison.Ordinal) && a.Contains("owned by BUILTIN\\Users", StringComparison.OrdinalIgnoreCase));
}

/// <summary>
/// Found live on 2026-09-23, when dev's upgrade refused DARLING01's pre-#4038 install root: the refusal
/// listed "BUILTIN\Users on C:\PerformanceMonitorDarling" twice. A folder made under C:\ inherits
/// BUILTIN\Users as two ACEs (one grants append, one grants write), and two of the four callers did not
/// drop the repeated line. The function now returns each finding once, so every caller names a principal
/// once per path. Same shadowed <c>Get-Acl</c> idiom as
/// <see cref="ThePreLockWritableExtractionCheck_FlagsAnUntrustedOwner_EvenWithAFullyTrustedDacl"/>. The two
/// ACEs get different inheritance flags so the ACL keeps them apart, as Windows did on the box, and the
/// probe counts them first so the test cannot pass on a single merged ACE.
/// </summary>
[Fact]
public void ThePreLockWritableExtractionCheck_NamesAPrincipalOnce_WhenTwoOfItsAcesGrantWrite()
{
var probe = new StringBuilder();
probe.AppendLine(ExtractFunction(InstallScript, "Resolve-DarlingServiceAccountSid"));
probe.AppendLine(ExtractFunction(InstallScript, "Get-LocalAdministratorsDirectMemberSids"));
probe.AppendLine(ExtractFunction(InstallScript, "Get-DarlingPreLockTrustedSids"));
probe.AppendLine("""
$script:usersSid = New-Object System.Security.Principal.SecurityIdentifier([System.Security.Principal.WellKnownSidType]::BuiltinUsersSid, $null)
function Get-Acl {
[CmdletBinding()]
param([Parameter(ValueFromRemainingArguments = $true)] $Rest)
$sec = New-Object System.Security.AccessControl.DirectorySecurity
$sec.SetAccessRuleProtection($true, $false)
foreach ($s in (Get-DarlingPreLockTrustedSids)) { $sec.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule($s, 'FullControl', 'ContainerInherit, ObjectInherit', 'None', 'Allow'))) }
$sec.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule($script:usersSid, 'AppendData', 'ContainerInherit', 'None', 'Allow')))
$sec.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule($script:usersSid, 'WriteData', 'ObjectInherit', 'None', 'Allow')))
$sec.SetOwner((New-Object System.Security.Principal.SecurityIdentifier([System.Security.Principal.WellKnownSidType]::BuiltinAdministratorsSid, $null)))
return $sec
}
""");
probe.AppendLine(ExtractFunction(InstallScript, "Get-UntrustedWriteGrantees"));
probe.AppendLine("""
$usersAces = @((Get-Acl).GetAccessRules($true, $true, [System.Security.Principal.SecurityIdentifier]) | Where-Object { $_.IdentityReference -eq $script:usersSid })
'usersAces=' + $usersAces.Count
$trusted = Get-DarlingPreLockTrustedSids
$found = @(Get-UntrustedWriteGrantees 'C:\does-not-need-to-exist-for-this-probe' $trusted)
'count=' + $found.Count
'text=' + ($found -join ';')
""");

var answers = RunWindowsPowerShell(probe.ToString());

Assert.Contains("usersAces=2", answers);
Assert.Contains("count=1", answers);
Assert.Contains(answers, a => a.StartsWith("text=", StringComparison.Ordinal) && a.Contains("BUILTIN\\Users on C:\\does-not-need-to-exist-for-this-probe", StringComparison.OrdinalIgnoreCase));
}

/// <summary>
/// #4043 round-1 review, M2: the pre-lock check is now a RECURSIVE walk, not just the root and the
/// service exe - a child with an untrusted OWNER, or an EXPLICIT (non-inherited) write grant to an
Expand Down
5 changes: 3 additions & 2 deletions Darling/tools/install-darling.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,9 @@ function Get-UntrustedWriteGrantees([string]$path, [array]$trusted, [switch]$Rec
$result += "$unlisted (its contents could not be listed: $($failure.CategoryInfo.Reason))"
}
}
return @($result)
# Each finding once: a folder made under C:\ inherits BUILTIN\Users as two ACEs (one grants append, one
# grants write), and without this every caller named that principal twice for the same path.
return @($result | Select-Object -Unique)
}

# Every SID that is a DIRECT member of BUILTIN\Administrators right now (round-1 review, #4043, M2): an
Expand Down Expand Up @@ -704,7 +706,6 @@ if (-not $AcceptWritableExtraction) {
# (round-1 review, L3/L4), rather than silently mis-judging the walk below.
$preLockTrusted = Get-DarlingPreLockTrustedSidsForRerun $serviceName $existing
$writable = @(Get-UntrustedWriteGrantees $root $preLockTrusted -Recurse)
$writable = @($writable | Select-Object -Unique)
if ($writable.Count -gt 0) {
$lines = ($writable | Select-Object -First 20 | ForEach-Object { " $_" }) -join "`n"
$more = if ($writable.Count -gt 20) { "`n ...and $($writable.Count - 20) more." } else { '' }
Expand Down
5 changes: 3 additions & 2 deletions Darling/tools/upgrade-darling.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,9 @@ function Get-UntrustedWriteGrantees([string]$path, [array]$trusted, [switch]$Rec
$result += "$unlisted (its contents could not be listed: $($failure.CategoryInfo.Reason))"
}
}
return @($result)
# Each finding once: a folder made under C:\ inherits BUILTIN\Users as two ACEs (one grants append, one
# grants write), and without this every caller named that principal twice for the same path.
return @($result | Select-Object -Unique)
}

# Every SID that is a DIRECT member of BUILTIN\Administrators right now (round-1 review, #4043, M2): an
Expand Down Expand Up @@ -1611,7 +1613,6 @@ else {
# same override.
if (-not $AcceptWritableExtraction) {
$writable = @(Get-UntrustedWriteGrantees $Source $preLockTrusted -Recurse)
$writable = @($writable | Select-Object -Unique)
if ($writable.Count -gt 0) {
$lines = ($writable | Select-Object -First 20 | ForEach-Object { " $_" }) -join "`n"
$more = if ($writable.Count -gt 20) { "`n ...and $($writable.Count - 20) more." } else { '' }
Expand Down
Loading