@@ -254,7 +254,7 @@ export class K8ClientPod implements Pod {
254254 // WSL2 has issues with kubectl port-forward when binding to localhost, binding to all interfaces will trigger
255255 // a permission prompt which if hidden behind the terminal can cause the port-forward command to fail.
256256 if ( ! isWindows ) {
257- cmdArguments . push ( localBindAddress , '&' ) ;
257+ cmdArguments . push ( localBindAddress ) ;
258258 }
259259 } else {
260260 cmd = constants . KUBECTL ;
@@ -280,16 +280,14 @@ export class K8ClientPod implements Pod {
280280 }
281281 }
282282
283- // Don't use shell on Windows when doing persist mode to avoid argument parsing issues
284- const useShell : boolean = isWindows && persist ? false : true ;
285-
283+ // shell:false to eliminate shell-injection; ShellRunner's detached spawn handles backgrounding.
286284 await new ShellRunner ( ) . run ( cmd , cmdArguments , {
287285 verbose : true ,
288286 detached : true ,
289287 environmentVariablesToAppend : {
290288 PATH : `${ this . kubectlInstallationDirectory } ${ path . delimiter } ${ process . env . PATH } ` ,
291289 } ,
292- useShell,
290+ useShell : false ,
293291 } ) ;
294292
295293 return availablePort ;
@@ -338,6 +336,15 @@ export class K8ClientPod implements Pod {
338336 return matchedProcesses ;
339337 }
340338
339+ // Best-effort: signal the process group led by pid (detached forwards are group leaders); ignore if not a leader.
340+ private static killProcessGroup ( pid : number , signal : NodeJS . Signals ) : void {
341+ try {
342+ process . kill ( - pid , signal ) ;
343+ } catch {
344+ // not a group leader (or already exited); the direct process.kill(pid) call handles it
345+ }
346+ }
347+
341348 public async stopPortForward ( port : number ) : Promise < void > {
342349 if ( ! port ) {
343350 return ;
@@ -364,6 +371,8 @@ export class K8ClientPod implements Pod {
364371 // Extract PIDs and kill the processes
365372 for ( const pid of matchedProcesses . map ( ( p : ProcessInfo ) : number => p . pid ) ) {
366373 try {
374+ // Signal the whole detached process group so no orphaned kubectl child keeps the local port. #4953
375+ K8ClientPod . killProcessGroup ( pid , 'SIGTERM' ) ;
367376 process . kill ( pid , 'SIGTERM' ) ;
368377 this . logger . debug ( `Successfully sent SIGTERM to PID: ${ pid } ` ) ;
369378
@@ -376,6 +385,7 @@ export class K8ClientPod implements Pod {
376385 this . logger . debug (
377386 `Process with PID ${ pid } is still running after SIGTERM, attempting to kill with SIGKILL` ,
378387 ) ;
388+ K8ClientPod . killProcessGroup ( pid , 'SIGKILL' ) ;
379389 process . kill ( pid , 'SIGKILL' ) ;
380390 }
381391 } catch ( killError ) {
0 commit comments