Fix intermittent Windows ESP Account setup hang (user-scope 405 race)#49250
Draft
lukeheath wants to merge 2 commits into
Draft
Fix intermittent Windows ESP Account setup hang (user-scope 405 race)#49250lukeheath wants to merge 2 commits into
lukeheath wants to merge 2 commits into
Conversation
The release path wrote the user-scope ServerHasFinishedProvisioning Replace unconditionally, but the user-scope DMClient Provider node was only created by the Pending/hold phase Adds gated on host_uuid == "". When host_uuid was linked (orbit enroll, DevDetail/SMBIOS reply, or osquery backstop) before the first hold session ran, the node was never created and the release Replace returned SyncML 405 -- hanging Autopilot devices on the ESP Account setup screen until the 3-hour timeout. Recreate the user-scope Provider node and its FirstSyncStatus child inline in buildESPReleaseCommands, ordered before the user-scope Replace, so the release is self-sufficient regardless of the host_uuid linking race. The Adds are idempotent (418 Already Exists when the node exists). Closes #49134
Follow-up to the ESP user-scope 405 fix. The two user-scope container-node Adds that buildESPReleaseCommands now persists (for the dropped-response retry path) are found by GetWindowsMDMCommandsForResending when the device returns 418 (Already Exists) in the common case where the node already exists. Without a guard, handleResendingAlreadyExistsCommands would rewrite those format=node Adds into Replace commands on the container URI -- an undefined command the device rejects -- so every successful finalization emitted spurious failing container Replaces post-ESP and error rows in windows_mdm_command_results. Skip container-node Adds (format=node, no <Data>) in the rewrite; leaf-value Adds (profiles, DMClient settings) still convert to Replace. Add a focused unit test for the guard, an end-to-end assertion in the ESP release integration test (which already simulates the host_uuid-linked-before-hold race), and refresh the stale persist-retry doc comment. Refs #49134
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #49250 +/- ##
==========================================
- Coverage 68.08% 68.08% -0.01%
==========================================
Files 3784 3784
Lines 239240 239244 +4
Branches 12575 12575
==========================================
- Hits 162893 162892 -1
Misses 61571 61571
- Partials 14776 14781 +5
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Related issue: Resolves #49134
Background for reviewers new to Windows MDM
Expand for a plain-language primer on ESP, node scopes, and SyncML Add/Replace
What's the ESP? When a Windows device is set up through Autopilot (the out-of-box "Account setup" flow you see on a brand-new work laptop), Windows shows an Enrollment Status Page (ESP) — the "Working on it…" spinner — and blocks the user from reaching the desktop until the MDM server (Fleet) says setup is done. There are two sub-phases: Device setup (before anyone logs in) and Account setup (after the user's profile starts loading).
How does Fleet tell Windows "you're done"? Windows MDM is a protocol called SyncML/OMA-DM. The device holds a tree of configuration nodes (think of a filesystem of settings). Fleet sends commands to that tree:
Add= create a node (likemkdir).Replace= set a node's value (like writing to a file).To release the ESP, Fleet sets a node called
ServerHasFinishedProvisioningtotrue. Crucially, this node exists in two separate places in the tree:./Device/...) — completes the "Device setup" phase. This node exists by default../User/...) — completes the "Account setup" phase. This node does not exist by default; Fleet has to create it first withAdd.What went wrong? Fleet created the user-scope node only during an early "hold" phase, and only while it hadn't yet matched the device to a Fleet host record (
host_uuid). Once fleetd installs during setup, that match happens — sometimes before the hold phase ever ran. In that case theAddwas skipped, the user-scope node was never created, and when Fleet later sent theReplaceto release the device, it hit a node that doesn't exist. The device answered with SyncML status 405 ("not allowed" / no such target), the "Account setup" phase never completed, and the machine sat on "Working on it…" until a 3-hour timeout. Because it was a timing race, it happened only sometimes — which is what made it hard to pin down.The fix in one sentence: send the
Addagain as part of the release itself, right before theReplace, so the node is guaranteed to exist regardless of timing. Re-adding a node that already exists is harmless — the device just replies 418 ("Already Exists"), which SyncML ignores.Summary
Windows Autopilot / Entra OOBE enrollments intermittently hung on the Enrollment Status Page at "Account setup → Working on it…" until the 3-hour ESP timeout. Fleet had run the ESP lifecycle to completion, but the user-scope release command (
./User/.../DMClient/Provider/Fleet/FirstSyncStatus/ServerHasFinishedProvisioning) failed on-device with SyncML 405, so the user ("Account setup") phase never got its completion signal.Root cause (a race, latent since the ESP feature shipped in 4.86.0): the user-scope DMClient Provider node is created by
Addcommands sent only whilehost_uuid == ""in the Pending/hold phase (handleESPHoldOrTransition). The release phase (buildESPReleaseCommands) then writes the user-scopeReplaceunconditionally, with noAdd. Whenhost_uuidis linked — by orbit enroll, the DevDetail/SMBIOS reply, or the osquery backstop — before the first Pending-phase hold session runs, theAdds are skipped, the user node is never created, and the releaseReplacelands on a nonexistent node → 405 → hang.The intermittency ("sometimes a host goes through, sometimes it doesn't") is whichever wins between the ~1-minute ESP hold poll and
host_uuidlinking. The window has been progressively narrowed by changes that linkhost_uuidearlier (#46268 DevDetail-at-first-session linking in 4.87.0; fleetd install-ordering changes in the 4.89.0 RC), which is why it now surfaces as a P1.Fix
buildESPReleaseCommands: prepend two idempotentAdds for the user-scope Provider node and itsFirstSyncStatuschild, ordered before the user-scopeReplace. SyncML processes commands in message order, so the node exists by the time theReplaceruns; when it already exists theAdds return a harmless418(Already Exists). This makes the release self-sufficient regardless of who wins thehost_uuidlinking race.handleResendingAlreadyExistsCommands: skip container-node Adds (format=node, no<Data>) in the 418→Replace rewrite. The release now persists two such Adds for its dropped-response retry; without this guard, the common-case418(node already exists) would rewrite them into undefined containerReplacecommands that the device rejects, emitting spurious failures post-ESP. Leaf-value Adds (profiles, DMClient settings) still convert to Replace.Deferred as a follow-up (#49252): gating
awaiting_configuration = Noneon the user-scope Replace acking200, so an unrelated future failure of that command can't silently strand a device. Not required for this P1.Checklist for submitter
Changes file added for user-visible changes in
changes/.Input data is properly validated,
SELECT *is avoided, SQL injection is prevented (using placeholders for values in statements), JS inline code is prevented especially for url redirects, and untrusted data interpolated into shell scripts/commands is validated against shell metacharacters.Timeouts are implemented and retries are limited to avoid infinite loops
Testing
TestPBT_HandleESPRelease(property-based): asserts the release path includes the user-scope node Adds and that they are ordered before the user-scopeServerHasFinishedProvisioningReplace.TestHandleResendingAlreadyExistsCommands(unit): container-node Adds are skipped in the 418 rewrite while leaf-value Adds still convert to Replace.TestWindows...ESPintegration test (already simulates thehost_uuid-linked-before-hold race): asserts the node Adds reach the wire in the release session.For unreleased bug fixes in a release candidate, one of: