Kelly Michels 7 min read Debugging

When a Program Update Bricks the App: An MSIX Job-Object Deadlock

0x80070020 named the wrong file. The real culprit was a service inside the package.

A desktop app's auto-updater staged a new version, couldn't register it, and left the app unable to launch — behind a dialog that blamed a locked file. The actual fault was a packaged Windows service holding its own package's job object, a lock no user-mode workaround can break. This is the walk from the misleading symptom to the one command that fixed it.

Read the full, unedited session transcript →

The symptom that lies

The auto-updater staged a new build and then surfaced a single, unhelpful dialog: "Another program is currently using this file." The app would no longer launch. The obvious reading — some process has a handle on a file in the package directory, find it and kill it — sends you hunting for a lock that is real but is not the thing you can act on.

The opening dialog. It names a sharing violation, but not the process — because the holder isn't a process you can reach.

Under the hood the failure code is 0x80070020ERROR_SHARING_VIOLATION — but the message that actually matters shows up one layer down, in the AppModel-Runtime activation log: "Cannot create the Desktop AppX container … because an error was encountered converting the job." That word — job — is the whole case.

Root cause: a service holding its own job object

The app ships a companion Windows service, CoworkVMService, and it is a packaged service (WIN32_PACKAGED_PROCESS): its binary lives inside the MSIX package directory. Every packaged app runs inside a Windows Job Object that the OS uses to bound and clean up the package's processes.

Here is the deadlock. While the packaged service is running, it holds the package's job object open. When you then launch the app, Windows tries to create the Desktop AppX container for it — which means creating (or "converting") that same job — and the create fails with 0x80070020 because the job is already held. The service that is supposed to support the app is the thing preventing the app from ever starting.

Process Explorer made the scope concrete. Searching the package family handle showed container jobs and DaxUseSemaphore objects still held by services.exe — and not just for the current version. They were leaked from three package versions no longer even on disk (1.32885.1.0, 1.34493.0.0, 1.34493.1.0), alongside the package's registry hives still mounted by the kernel.

The handle map: container jobs and semaphores held by services.exe, leaked across package versions that are no longer installed. Account identifiers in the paths are redacted.

Why every mitigation gets reverted

The intuitive fix is to stop the service, or disable it, or delete it — release the job, launch the app. Each of those runs into a wall that is by design:

  • You can't keep it disabled. Setting the service start type to disabled holds only until the next package servicing operation, at which point the deployment handler (PackagedServiceDEH) resets the service registration to its manifest defaults — including AUTO_START. This was observed live, twice: the start type was set to 4 and came back as 2.
  • You can't delete it. sc delete is refused for an elevated administrator and for SYSTEM. The service's security descriptor answers to TrustedInstaller, which sits above both.
  • You can't close the handle. On Windows 11 services.exe runs as a Protected Process Light. Even elevated, even with SeDebugPrivilege, user mode cannot open it to touch the job handle it holds — the same armor that stops malware from gutting the service controller works against you here.
Proof the package fights back: after two attempts to disable it, START_TYPE is back to 2 AUTO_START. The deployment handler restored it.

Every lever a determined admin reaches for is either refused outright or quietly undone by the package itself. That is the point at which "keep debugging" stops being the right call.

The only recovery that worked

One command has enough authority, precisely because it doesn't try to borrow it:

Get-AppxPackage -Name '*Claude*' | Remove-AppxPackage

Removal tears down the package and its service together, through the deployment stack — the one path where TrustedInstaller is working for you instead of against you. Afterward sc qc returns FAILED 1060: the service does not exist. That is the win condition.

FAILED 1060 — package and service both gone, no error, no reboot forced.

Then a reboot, then reinstall. The order matters: reinstalling before the reboot fails with 0x80073CF6, because the package's registry hives are still mounted by the kernel and only a reboot clears them. And there is no shortcut around the packaging: the .exe installer is just an AddPackage bootstrapper — there is no non-MSIX Windows build — so there is no packaging escape hatch to fall back on.

Reinstalling before the reboot: 0x80073CF6. The mounted hives are kernel-held. Reboot first, then the install sails through.

The full recovery, in order: remove the package → reboot → reinstall.

Takeaways

  • When an HRESULT names a generic condition — 0x80070020 is just "sharing violation" — the real diagnosis is usually one layer down. Here it was the AppModel-Runtime activation log's "converting the job" line.
  • A packaged service whose binary lives inside its own package can deadlock that package's launch by holding the job object. This is a structural hazard of shipping a WIN32_PACKAGED_PROCESS alongside the app, not a transient glitch.
  • MSIX's protections — TrustedInstaller-owned descriptors, PPL service host, deployment handler resets — are exactly what make user-mode mitigation impossible. The intended tool is the deployment stack: remove and reinstall, not surgery on the live package.
  • "Reboot fixed it" is not superstition when kernel-held objects are involved. Job objects and mounted hives outlive the processes that created them; only a reboot reliably reaps them.

← Back to Blog