Windows Task Scheduler: sichere temporäre XML-/SDDL-Wiederherstellung nach unbestätigtem Readback

My Music Lab 0 Reputation points
2026-09-14T15:21:08.4633333+00:00

Wir verwenden Windows Task Scheduler über die COM-API für eine einzelne, vorhandene deaktivierte Aufgabe. Vor einer temporären, eng begrenzten Testdefinition sichern wir die Original-XML und SDDL. Nach RegisterTask(Update) kann der anschließende Readback jedoch fehlschlagen oder nicht eindeutig beweisen, ob die temporäre Definition übernommen wurde. Wir möchten keinen Start als Nachweis verwenden und keine Sicherheitsrechte lockern. Welche dokumentierte, atomare bzw. unterstützte Vorgehensweise gibt es, um: 1. zwischen „Update nicht übernommen“, „Update übernommen, Readback fehlgeschlagen“ und „Wiederherstellung nötig“ zu unterscheiden, 2. eine bestehende deaktivierte Aufgabe sicher auf die exakt gesicherte XML-/SDDL-Definition zurückzustellen, und 3. danach den deaktivierten Zustand sowie 0 laufende Instanzen zuverlässig zu bestätigen? Wir nutzen RegisterTask mit einer bestehenden Aufgabe und wollen keine neue Aufgabe erstellen. Gibt es dafür dokumentierte COM-/Task-Scheduler-Semantik oder ein empfohlenes Transaktions-/Readback-Muster?

Windows development | Windows API - Win32

Answer accepted by question author
Taki Ly (WICLOUD CORPORATION) 4,450 Reputation points Microsoft External Staff Moderator
2026-09-15T03:26:39.88+00:00

Hello @My Music Lab ,

I ran your snippet as-is (baseline pre-created as a disabled task, writes enabled, the two placeholders filled). It reproduces what you describe, and it does so every time: it ends with UpdateReadback = UNCONFIRMED and RestoreReadback = RESTORATION_UNCONFIRMED even though the RegisterTask(TASK_UPDATE) call actually succeeded and the candidate definition was applied. So the write and the timing are not the issue here, the comparison is.

The reason is that when you pass an SDDL in the 7th parameter, the service writes it back into the task XML as a <SecurityDescriptor> element under <RegistrationInfo>. Your baseline was registered without an SDDL and your candidate is built by hand, so neither contains that node, while the readback after the update does. That single injected element is enough to make Same-Definition(observed, candidate) return false, which throws CANDIDATE_NOT_CONFIRMED, and then the restore branch matches neither original nor candidate and falls through to RESTORATION_UNCONFIRMED. The diff is exactly this:

candidate: <RegistrationInfo><Description>...</Description><URI>...</URI>...
observed : <RegistrationInfo><SecurityDescriptor>O:BAG:...</SecurityDescriptor><Description>...

So the fix is on the comparison side: compare a readback against another readback rather than against your hand-built candidate, and strip the <RegistrationInfo><SecurityDescriptor> node before comparing (you already verify the descriptor separately through GetSecurityDescriptor), or just compare the specific fields you changed.

On your four remaining questions. There is no documented atomicity or rollback for RegisterTask with TASK_UPDATE; it overwrites the definition in place and does not keep the old one for you, so you should treat it as applied only on a SUCCEEDED HRESULT. Keep in mind a few success codes are not S_OK (for example SCHED_S_SOME_TRIGGERS_FAILED, 0x0004131B), and in PowerShell only a failed HRESULT throws, so your try/catch to UNCONFIRMED is already correct. There is also no version, ETag or conditional-update token on the API, so the gap between inspecting and restoring is a genuine TOCTOU window with no supported guard beyond re-reading right before you restore and staying fail-closed, which is what you do.

For comparison in general, the service canonicalizes the XML on registration: it expands the default Settings, adds URI, drops RunLevel when it is LeastPrivilege, resolves UserId to a SID, and injects the SecurityDescriptor as above. Because of that, only a readback-versus-readback comparison is reliable; that canonical form is deterministic and re-registering it is idempotent. Ordinal comparison of the SDDL string is fine in practice since the descriptor you pass round-trips unchanged, just note that GetSecurityDescriptor(7) includes inherited (ID) ACEs. And if a call hangs instead of throwing, there is no per-call timeout in COM and no API that tells you the previous RegisterTask has committed, so the practical pattern is to run the call on a worker thread with your own timeout and, on timeout, open a fresh ITaskService::Connect and read the current definition to decide rather than restoring blindly.

One thing worth flagging for the "zero running instances" check: GetInstances only returns instances running at or below the caller's security context, so a count of zero is only trustworthy when you run elevated or in the same context as the task principal.

Overall your fail-closed readback approach is the right one, precisely because Task Scheduler gives you no atomic update, no rollback and no version token. The only real defect is the whole-document comparison tripping over the injected SecurityDescriptor. Your flag choice (TASK_UPDATE | TASK_IGNORE_REGISTRATION_TRIGGERS | TASK_DONT_ADD_PRINCIPAL_ACE) is fine and keeps the security state untouched.

If you want to dig into the details, the relevant documentation is ITaskFolder::RegisterTask for the flags and return codes, IRegisteredTask for the full member set (which confirms there is no version or conditional-update token), and IRegisteredTask::GetInstances for the security-context note on counting running instances.

I hope this helps clarify what is happening. If you adjust the comparison as described and still see an unconfirmed result, feel free to share the updated readback and I will be glad to take another look. If you found my response helpful or informative, I would greatly appreciate it if you could follow this guide for your confirmation.

Thank you.

Was this answer helpful?

1 person found this answer helpful.

0 additional answers

Sort by: Most helpful

Your answer

Answers can be marked as 'Accepted' by the question author and 'Recommended' by moderators, which helps users know the answer solved the author's problem.