Hi @A W ,
Thank you for your patience. I went through the documentation you referenced and a few related pages. Based on what is documented, I could not find a supported user-mode mechanism that meets all of your constraints. Below is what the docs establish and where the gap is.
1. The SCM start path does not return a process handle
StartServiceW returns only a BOOL. Per its remarks, the SCM spawns the service process if needed and returns once the service control dispatcher reports that the ServiceMain thread was created. No process handle is passed back to the caller at creation time.
2. The reported PID is not bound to the activation through early exit
According to QueryServiceStatusEx, the dwProcessId value is valid only while the service is in SERVICE_RUNNING, SERVICE_PAUSE_PENDING, SERVICE_PAUSED, or SERVICE_CONTINUE_PENDING. It may not be valid during SERVICE_START_PENDING or SERVICE_STOP_PENDING, and it is never valid once the service is SERVICE_STOPPED.
The SERVICE_STATUS_PROCESS documentation also notes that dwProcessId is zero when the service is not running.
If the process exits before the controller opens it, the docs provide no guarantee that ties a later PID to that activation.
3. Opening the PID depends on the process DACL
OpenProcess checks the requested access against the security descriptor of the target process. If the caller has enabled SeDebugPrivilege, the requested access is granted regardless of the security descriptor. The same access model is described in Process Security and Access Rights. With the existing DACL and privileges unchanged, ERROR_ACCESS_DENIED for SYNCHRONIZE | PROCESS_QUERY_LIMITED_INFORMATION is consistent with this documented behavior.
4. Handle guarantees apply only after a handle is obtained
Per Process Handles and Identifiers, a process handle stays valid until closed, even after the process terminates, while a PID is valid only until the process terminates. The controller owns the handle and must close it with CloseHandle, as stated in the OpenProcess remarks. PROCESS_QUERY_LIMITED_INFORMATION is not supported on Windows Server 2003 or Windows XP, per Process Security and Access Rights.
The open question is how to acquire the handle in the first place, and the SCM path does not document a way to do that.
Supported alternatives (each changes one of your constraints)
Use service-level evidence: Starting a Service shows a service control program tracking the service through SERVICE_STATUS_PROCESS returned by QueryServiceStatusEx. That structure reports dwWin32ExitCode and dwServiceSpecificExitCode, which are service-reported values, not the process exit code.
Change the launcher: if the controller creates the process itself with CreateProcess, it receives a process handle with PROCESS_ALL_ACCESS at creation, and that handle remains valid until closed (Process Handles and Identifiers, Process Security and Access Rights). The process would then no longer be an SCM-managed service.
Change the trust boundary: a process's security descriptor can be changed with SetSecurityInfo (Process Security and Access Rights). The service could use this to grant the controller the required rights on its own process object. This changes the DACL and still leaves a window before the change takes effect.
Change the privileges: enable SeDebugPrivilege or use a privileged broker. You have already ruled these out.
If you need an official confirmation of internal SCM behavior beyond what is documented, I'd recommend contacting Windows Developer Support, where engineers can investigate at that level.
If you found this helpful, you can refer to this guidance to confirm it and help others with similar questions.
Thank you.