Describe the bug
On Windows, the self-update flow uses the relative path update.finished and starts the generated update script without setting ProcessStartInfo.WorkingDirectory.
When the runner is hosted by Windows Service Control Manager, its inherited current directory can be C:\Windows\System32. As a result, the update marker is resolved as:
C:\Windows\System32\update.finished
This caused a self-hosted runner running as NT AUTHORITY\NETWORK SERVICE to fail an otherwise successful update because it could not delete a marker previously created at that shared System32 path.
The behavior was observed while updating runner 2.334.0 to 2.336.0. The same relative-path logic is present in 2.336.0 and in current main at commit 99e01149303b194e08778cdc9c03fa2703d03fb2b.
To Reproduce
- Configure an x64 Windows self-hosted runner to run as a Windows service under a least-privilege service identity such as
NT AUTHORITY\NETWORK SERVICE.
- Start the runner service with the normal Service Control Manager environment, where the process current directory is
C:\Windows\System32.
- Trigger a runner self-update while an
update.finished marker exists in that inherited current directory and is not deletable by the runner service identity. This can occur when multiple runner services with different identities share the same inherited System32 working directory.
- The runner downloads and validates the official update package, extracts the candidate version, and generates
_work\_update.cmd.
- The update then fails when
SelfUpdater resolves the relative marker path against the process current directory.
The underlying issue can also be reproduced without System32 by starting the listener with a protected current directory that differs from the runner root and contains a marker the listener cannot delete.
Expected behavior
The update marker and update script process should be scoped to the individual runner root, independently of the host process current directory:
<RunnerRoot>\update.finished
The generated update script should run with <RunnerRoot> as its WorkingDirectory.
Actual behavior
The marker path and generated update script inherit the Windows service process current directory. In the observed service configuration, this resolves to C:\Windows\System32 and causes multiple runner services to share the same marker location.
Runner Version and Platform
- Update attempted:
2.334.0 to 2.336.0
- The affected logic is also present in runner
2.336.0 and current main commit 99e01149303b194e08778cdc9c03fa2703d03fb2b
- OS: Windows 10 Pro 22H2, build
19045.6332, x64
- Hosting mode: Windows service, automatic delayed start
- Service identity:
NT AUTHORITY\NETWORK SERVICE
- Runner installation path: local non-system volume; exact repository and runner names redacted
What's not working?
The listener diagnostic log contains:
System.UnauthorizedAccessException:
Access to the path 'C:\Windows\system32\update.finished' is denied.
Before this exception, the diagnostic log confirms that the runner successfully:
- detected the
2.336.0 update;
- downloaded the official runner package;
- validated the package hash;
- extracted the candidate version;
- copied the candidate files; and
- generated
_work\_update.cmd.
The relevant implementation currently uses a relative marker path and does not set the update process working directory:
src/Runner.Listener/SelfUpdater.cs
src/Runner.Listener/SelfUpdaterV2.cs
src/Misc/layoutbin/update.cmd.template
SelfUpdater.cs and SelfUpdaterV2.cs delete:
string flagFile = "update.finished";
IOUtil.DeleteFile(flagFile);
The Windows update template creates the same relative marker:
type nul > update.finished
The process used to launch the generated update script does not explicitly set WorkingDirectory.
A proposed fix is to resolve the marker from WellKnownDirectory.Root and set the generated update process working directory to that same runner root:
string runnerRoot = HostContext.GetDirectory(WellKnownDirectory.Root);
string flagFile = Path.Combine(runnerRoot, "update.finished");
IOUtil.DeleteFile(flagFile);
Process invokeScript = new();
invokeScript.StartInfo.WorkingDirectory = runnerRoot;
A patch implementing the fix for both update flows has been prepared against current main.
- Patch SHA-256:
F4FF72115317745806A93D3BAC4883944BFAD3CC6B6804138161EDBE6C291155
git diff --check: passed
- Patch application check against current
main: passed
- Targeted build for the affected runner projects: passed with 0 warnings and 0 errors
- L0 regression tests: 2 passed, 0 failed
The two L0 tests separately exercise the production preparation methods in SelfUpdater and SelfUpdaterV2. They cover:
- a runner root containing spaces;
- deletion of the marker from the runner root;
WorkingDirectory set to the runner root; and
- preservation of quoting for an update script path containing spaces.
The patch can be provided if maintainers would like to review or adopt it. No custom runner binary is being used in production.
Job Log Output
Not applicable. The failure occurs in the runner listener self-update flow, outside an individual workflow step.
Runner and Worker's Diagnostic Logs
Relevant redacted listener diagnostic excerpt:
Runner update in progress, do not shutdown runner.
Downloading 2.336.0 runner
Download latest runner and unzip into runner root.
Waiting for current job finish running.
All running job has exited.
Generate and execute update script.
System.UnauthorizedAccessException:
Access to the path 'C:\Windows\system32\update.finished' is denied.
Runner update process finished.
Repository names, runner names, machine names, tokens, credentials, and exact local installation paths have been removed from this report.
Describe the bug
On Windows, the self-update flow uses the relative path
update.finishedand starts the generated update script without settingProcessStartInfo.WorkingDirectory.When the runner is hosted by Windows Service Control Manager, its inherited current directory can be
C:\Windows\System32. As a result, the update marker is resolved as:This caused a self-hosted runner running as
NT AUTHORITY\NETWORK SERVICEto fail an otherwise successful update because it could not delete a marker previously created at that shared System32 path.The behavior was observed while updating runner
2.334.0to2.336.0. The same relative-path logic is present in2.336.0and in currentmainat commit99e01149303b194e08778cdc9c03fa2703d03fb2b.To Reproduce
NT AUTHORITY\NETWORK SERVICE.C:\Windows\System32.update.finishedmarker exists in that inherited current directory and is not deletable by the runner service identity. This can occur when multiple runner services with different identities share the same inherited System32 working directory._work\_update.cmd.SelfUpdaterresolves the relative marker path against the process current directory.The underlying issue can also be reproduced without System32 by starting the listener with a protected current directory that differs from the runner root and contains a marker the listener cannot delete.
Expected behavior
The update marker and update script process should be scoped to the individual runner root, independently of the host process current directory:
The generated update script should run with
<RunnerRoot>as itsWorkingDirectory.Actual behavior
The marker path and generated update script inherit the Windows service process current directory. In the observed service configuration, this resolves to
C:\Windows\System32and causes multiple runner services to share the same marker location.Runner Version and Platform
2.334.0to2.336.02.336.0and currentmaincommit99e01149303b194e08778cdc9c03fa2703d03fb2b19045.6332, x64NT AUTHORITY\NETWORK SERVICEWhat's not working?
The listener diagnostic log contains:
Before this exception, the diagnostic log confirms that the runner successfully:
2.336.0update;_work\_update.cmd.The relevant implementation currently uses a relative marker path and does not set the update process working directory:
src/Runner.Listener/SelfUpdater.cssrc/Runner.Listener/SelfUpdaterV2.cssrc/Misc/layoutbin/update.cmd.templateSelfUpdater.csandSelfUpdaterV2.csdelete:The Windows update template creates the same relative marker:
The process used to launch the generated update script does not explicitly set
WorkingDirectory.A proposed fix is to resolve the marker from
WellKnownDirectory.Rootand set the generated update process working directory to that same runner root:A patch implementing the fix for both update flows has been prepared against current
main.F4FF72115317745806A93D3BAC4883944BFAD3CC6B6804138161EDBE6C291155git diff --check: passedmain: passedThe two L0 tests separately exercise the production preparation methods in
SelfUpdaterandSelfUpdaterV2. They cover:WorkingDirectoryset to the runner root; andThe patch can be provided if maintainers would like to review or adopt it. No custom runner binary is being used in production.
Job Log Output
Not applicable. The failure occurs in the runner listener self-update flow, outside an individual workflow step.
Runner and Worker's Diagnostic Logs
Relevant redacted listener diagnostic excerpt:
Repository names, runner names, machine names, tokens, credentials, and exact local installation paths have been removed from this report.