Skip to content

--export-viewer-config: sweep all three files on a failed export; document the ancestor-junction limit of the directory guards #1973

Closed

Description

@erikdarlingdata

Two low-grade review findings from #1968's final review round, deferred at merge time only because the merge was reboot-blocking; both are ~15-minute mechanical fixes in Darling/PerformanceMonitor.Darling.Service/DarlingCliCommands.cs.

1. A failed export sweeps only the secret file, leaving a half-finished folder

The catch around the three WriteExportFileAsync calls deletes darling.json on any failure (correct - it is the secret-bearing file, and the message says so). But if the failing write was server.crt or README.txt, the files written BEFORE the failure stay behind - and because each write deletes the PREVIOUS export's copy first, the folder can end up as a mix of new and stale files with no config, unnamed in the error. No secret exposure; it just cuts against the verb's premise of refusing to hand over a partial setup.

Fix: on failure, best-effort delete all three (the cert and README are always safe to remove), and name in the error exactly what was removed and what, if anything, was left.

2. Directory guards do not see a reparse point on an ANCESTOR of the destination

The self-destruct guard (lexical Path.GetFullPath compare) and the junction guard (DirectoryInfo.Attributes on the leaf) both miss a junction earlier in the chain - e.g. destination C:\Data\subfolder where C:\Data is a junction. GetFullPath is lexical and leaf attributes do not reflect intermediate links.

Analysis that downgrades this (verified against the code, record it in whatever fix ships): the catastrophic case the reviewer named - the export resolving back into the service's config directory and destroying the service's own darling.json - is ALREADY blocked one layer down. TryCheckExportTarget runs per FILE through the OS's real path resolution (File.GetAttributes / File.ReadAllText follow the whole junction chain), and the service's darling.json does not carry the export marker, so the verb refuses with "not written by --export-viewer-config" before anything is deleted. The residual exposure is only "files land in an unexpected directory the junction-planter controls" - and someone who can plant junctions in the ancestor chain of the operator's chosen destination already controls that destination's fate. The exported secret is still ACL'd wherever it lands, and the harden result is confirmed, not assumed.

Fix: at minimum a comment on the two guards recording the limitation and naming the marker guard as the backstop (the reviewer's own suggested floor); optionally resolve the chain (Directory.ResolveLinkTarget walk or Path.GetFullPath on a FileInfo.LinkTarget-resolved base) and refuse when the RESOLVED destination differs from the lexical one - but do not let that replace the marker guard, which is the layer that actually holds.

Both were dispositioned on the PR before merge; review context: #1968

Activity

  1. added a commit that references this issue on Aug 1, 2026
  2. erikdarlingdata commented on Aug 1, 2026

    @erikdarlingdata
    OwnerAuthor

    Shipped in #1974. A failed export now sweeps all three files through one helper - each removal independent, each survivor named with the OS reason, the darling.json line keeping its live-password framing. The existence probe uses File.GetAttributes for the same dangling-symlink reason TryCheckExportTarget does. The test forces the failure where it bites (FileShare.Read hold on a marker-carrying server.crt: readable for the pre-flight guard, undeletable for the production write) and was watched red against the old behavior. Both directory guards now carry the ancestor-junction limitation comment naming the per-file marker guard as the backstop that actually holds. The review bot's TOCTOU note was verified wrong on its stated mechanism (File.Delete is a documented no-op on a missing file) and dispositioned on the PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions