Skip to content

M5: threat-model review, service-hosted WebDAV attach, double-mount diagnosis - #3

Merged
FacuM merged 9 commits into
mainfrom
claude/m5-service-56e842
Nov 1, 2026
Merged

FacuM merged 9 commits into
mainfrom
claude/m5-service-56e842

Conversation

@FacuM

@FacuM FacuM commented Oct 30, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • Threat-model review (docs/design/05-threat-model.md §12): what M5 actually implements, surface by surface (pipes, loopback WebDAV, Dokany requestor check, update signatures, DPAPI, Event Log, service inputs). Each claimed property gets a file:line and a verdict. It finds eight claims with no code behind them. The most serious: the host compiles in no update roots (update_ipc.c:30-31), so the complete verifier is never reachable, and there is no crash-dump code. It proposes TM-R40–TM-R45.
  • Global WebDAV attach (02 §7.7). The service hosts the export without mapping it. Each user's agent maps the UNC returned by the new service method mount.attach_info, at logon and through mount.attach, and only members of service_access get it. The mechanism goes through svc_gw_decide: auto resolves to attach, and a system or link verdict is refused rather than served as attach.
  • sd_webdav service mode. The per-connection owner check yields to the per-request sd_vfs check, and token groups now reach it, so group entries in service_access match. Anonymous and Guest callers get 403. LOCAL SERVICE and NETWORK SERVICE match only by an explicit SID.
  • Attach survives per-user mounts. The agent's own mount-start cleanup keeps the attached service port (sd_webdav_preserve_port). Without this, a later per-user WebDAV mount would have cancelled the global drive.
  • The second-mount heap corruption from m5-notes Part 10 was a double free in the test harness, already fixed by json_drop in e2c3df5. This PR records it and verifies it.
  • Lab procedure for the live global Dokany mount (docs/dev/lab-results.md appendix). It has not been run: the Win10 VM was busy with M3 acceptance.

Test plan

  • unit.svc: attach point reported for a WebDAV global mount, nothing to attach for Dokany, access_denied outside service_access, double-mount regression
  • unit.webdav: a service server admits a member and refuses a non-member (real SSPI, real loopback server)
  • tools\ci-local.ps1 -Pipeline: x64-modern, x64-xp, x86-ia32, x86-sse2 and x64-analysis all pass (only the expected WOW64 dokany.mount skips)
  • repro.x64-modern
  • Live global Dokany mount on sd-win10-ltsc21-x64, two users, mount at boot (procedure in lab-results.md)
  • G2 lab run with two users mapping each other's UNC; G1, G3 and G5

Details and open items: docs/dev/m5-notes.md Part 14.

Live global Dokany mount on the Win10 lab VM (2026-10-30)

  • 58d6b68 service: open the service with SERVICE_START to set its restart actions. The live run found
    that service install could never succeed. ChangeServiceConfig2(SERVICE_CONFIG_FAILURE_ACTIONS) with
    SC_ACTION_RESTART needs SERVICE_START on the handle, so the install rolled back with ACCESS_DENIED, and
    the CLI reported it as "needs an elevated console" (exit 9), even as SYSTEM.
  • f853144 docs: record the M5 live global mount run. These passed on the run: install and start as
    SYSTEM, machine profile and host key, the global mount (Event 1100), standard-user batch and console
    logons, and two live sessions at once (G1). G5 passed: 1 GiB written in 74.6 s and read back in 17.6 s
    with a matching SHA-256. G7 passed: the read-only member is denied, and the drop-member mutation takes
    effect. G3 passed: service stop/start and VM save/resume. Uninstall passed, and Event 1200 records every
    change with the user SID.
    Gaps to triage are in m5-notes.md Part 14 "Left":
    • autostart = boot is not applied by the service.
    • A standard user's service list exits 8 from a batch session.
    • The unknown-profile message has an empty name.
    • scm_error maps every ACCESS_DENIED to "needs an elevated console".

Local gate on f853144: all five presets pass.

Gap fixes, checked by a second live run (2026-10-31)

  • 5780848 service: mount autostart=boot profiles when the service starts. After a reboot the drive is
    mounted before logon, 33 s and 35 s after boot on the two phase-B reboots, so G6 holds for Dokany.
  • 7f4538c cli: name the profile in service errors and tell SCM denials from missing elevation.
  • b478b45 cli: report a refused service-pipe connect as access denied, not "not running". The pipe
    stays interactive-only, as 03 §2.2 specifies. A batch session now gets exit 9 "Access to the ShellDrive
    service is denied.", and exit 8 means the service is really absent.
  • 9b26bfc service: let a new mount replace a failed record of the same profile. The live run found it:
    the first autostart after install fails until the host key exists, and that failed record then blocked
    service mount with exit 10. This commit also fixes a use-after-free in the wait_state test helper.
  • 3b9f032 tests: keeps a 32 KiB smoke result off the stack (C6262 under /analyze).
  • 0ec4afa docs: updates m5-notes.md Part 14.

Local gate on 9b26bfc: all five presets pass. Live run on the Win10 lab VM: 45 steps OK, SDM5 DONE.
Three final-phase steps failed because the lab script did not yet expect the autostart (already_mounted
after a service restart). The script is fixed; the product behaviour is correct.

FacuM added 9 commits October 30, 2026 17:53
…iagnosis

- docs/design/05-threat-model.md section 12: the M5 implementation review.
  Per surface (pipes, loopback WebDAV, Dokany requestor check, update
  signatures, DPAPI, Event Log, service inputs) the claimed property, the
  code that carries it (file:line) and a verdict. Claims with no code: the
  host compiles in no update roots, no crash-dump code, no Event 3100, no
  FUS/RDP session list, no --elevated-view. Proposed TM-R40 to TM-R45.
- Global WebDAV attach (02 7.7): the service hosts the export and maps
  nothing (no_map); each user's agent maps the UNC it gets from the new
  service method mount.attach_info, at logon and through mount.attach.
  Only members of service_access get the UNC. The mechanism goes through
  svc_gw_decide (auto; system/link verdicts are refused, not served as
  attach). backend = webdav service profiles load again.
- sd_webdav service mode: the per-connection owner check yields to the
  per-request sd_vfs check, and the token's enabled groups reach sd_vfs so
  group entries of service_access match. Anonymous and Guest callers get
  403; LOCAL/NETWORK SERVICE match only by an explicit SID.
- The agent's own mount-start cleanup keeps the attached service port
  (sd_webdav_preserve_port); the explicit start/stop cleanups still remove it.
- The Part 10 heap corruption on a second mount was a test double free,
  fixed by json_drop in e2c3df5; recorded and verified.
- docs/dev/lab-results.md appendix: the procedure for the live global
  Dokany mount on the Win10 VM (not run: the VM was in use).

Tests: unit.svc (attach point reported; attach refused outside
service_access), unit.webdav (service server admits a member and refuses
a non-member, real SSPI). Local gate: five presets pass; repro.x64-modern
passes.
ChangeServiceConfig2 with SERVICE_CONFIG_FAILURE_ACTIONS needs SERVICE_START
on the handle when an action is SC_ACTION_RESTART. scm_set_failure_actions
opened the service with SERVICE_CHANGE_CONFIG only, so every install failed
with ERROR_ACCESS_DENIED after CreateService, rolled the service back, and
the CLI reported it as "needs an elevated console" (exit 9), even as SYSTEM.
Found by the live global mount run on the Win10 lab VM: with this change the
service installs, starts and mounts the global drive.
Machine profiles of scope service with [mount] autostart = boot were never
mounted by the service (03 3.4.1): the live lab run found every reboot with
an empty service list until a manual service mount. The body of the mount
handler becomes svc_mount_queue, shared by the IPC "mount" request and the
new svc_autostart_boot_mounts, which runs after the profiles are loaded at
start, on config reload and after a data-folder repair. The SCM start is not
blocked: the mounts go through the control thread's queue with sd_mount's
reconnect backoff, so a network that is not up yet only delays them. An
already mounted profile is left alone, so a reload never double-mounts.
…sing elevation

service mount, unmount and status now pass the requested name to fail_ipc,
so "There is no profile named {name}." shows the profile instead of an
empty placeholder (m5-notes known defect). scm_error splits
ERROR_ACCESS_DENIED by cli_is_elevated: a caller that is not elevated
keeps the elevation message and exit 9 (new cli.service.manage_need_admin,
"Managing the ShellDrive service ...", more accurate for install,
uninstall, start and stop), while an elevated console whose SCM call was
refused gets cli.service.scm_failed with the Win32 code and exit 1, as
03 6.1 assigns. SHELLDRIVE_TEST_SCM=denied drives both branches without
touching the real SCM; a smoke test and an e2e test cover them and the
named profile.
… running'

The service pipe's DACL grants read/write to interactive logons only
(03 §2.2, carried in the threat model), so a member of service_access in
a batch logon is denied at CreateFileW. open_pipe() folded that refusal
into SD_E_NOT_FOUND and connect_service() printed 'not running or not
installed' with exit 8. open_pipe() now returns SD_E_ACCESS_DENIED on
ERROR_ACCESS_DENIED, connect_service() returns the sd_err, and the CLI
reports access_denied with exit 9 through a new localized message in all
six catalogs. Unit tests pin the service pipe DACL to the design and the
new error mapping; a smoke test covers the CLI exit codes and JSON.
run_result is 32 KiB; as a local it trips C6262 under /analyze (x64-analysis), like the other smoke tests it is static.
The live lab run found the boot autostart mount failing (its host key was
added later) and then `service mount` of the same profile answering
already_mounted (exit 10): the duplicate check matched the final failed
record, so the drive could not come up without an unmount first. A final
failed record holds no live drive (03 1.11: failed is terminal), so a new
mount of its profile now detaches it under g.lock and queues its release to
the control thread, the way the tick reaps an unmounted record; the new
mount gets the same letter. A live record still answers already_mounted.

Tests: a failed autostart mount is replaced by a new mount (new id, same
target) and a live one is still refused. wait_state compared the state
string after freeing the reply it pointed into (it only bit with a NULL
`last` in Debug); it now compares first. The boot autostart tests register
their own host-key trust so they also pass when run alone.
The four gaps of the first live run (boot autostart, the batch-session service list report, the unnamed profile, the SCM access-denied message) are fixed and checked live on 2026-10-31, as is the failed-record replacement that run found.
@FacuM
FacuM merged commit 97636ed into main Nov 1, 2026
8 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