Expose gigl vars as env vars for the custom luancher#642
Expose gigl vars as env vars for the custom luancher#642kmontemayor2-sc wants to merge 4 commits into
Conversation
| shell_line = " ".join([command, *(shlex.quote(a) for a in args)]) | ||
| logger.info(f"Launching {component.name} via subprocess: {shell_line!r}") | ||
| subprocess.run(shell_line, shell=True, check=True) | ||
| subprocess.run(shell_line, shell=True, check=True, env=env) |
There was a problem hiding this comment.
Semgrep identified an issue in your code:
The subprocess.run() call uses shell=True on user-controlled command strings, allowing shell injection attacks that could execute arbitrary commands with the process's privileges.
More details about this
The subprocess.run() call executes shell_line with shell=True, which spawns a shell process to interpret the command. This is dangerous because if resolved_command or any element in resolved_args comes from untrusted input (e.g., user-provided configuration, external data), an attacker can inject arbitrary shell commands.
For example, if an attacker controls custom_resource_config.command and sets it to id; rm -rf /, the shell will execute both id and the destructive rm -rf / command. Even though shlex.quote() escapes individual arguments, it doesn't protect against injection in the command itself—only in the args list. An attacker who controls the command field can bypass this protection entirely.
Exploit scenario:
- Attacker provides
custom_resource_config.command = "echo test; cat /etc/passwd #" - After joining with args,
shell_linebecomes something like"echo test; cat /etc/passwd #" - When
subprocess.run()executes this withshell=True, the shell interprets the semicolon as a command separator - Both
echo testandcat /etc/passwdexecute, leaking sensitive system files - If the process runs with elevated privileges, the attacker can exfiltrate or modify sensitive data
The shell inherits environment variables and settings from the parent process, which further expands the attack surface. Using shell=False would treat the entire string as a single command name rather than allowing shell metacharacters to be interpreted.
To resolve this comment:
💡 Follow autofix suggestion
| subprocess.run(shell_line, shell=True, check=True, env=env) | |
| subprocess.run(shell_line, shell=False, check=True, env=env) |
View step-by-step instructions
- Replace
subprocess.run(shell_line, shell=True, check=True)with an invocation that does not useshell=True. - Change the code to directly pass the command and arguments as a list, rather than joining into a string. Use:
subprocess.run([resolved_command] + resolved_args, check=True). - Remove any uses of
shlex.quotewhen building the argument list, since quoting is only necessary when passing a string to the shell. - Ensure that
resolved_commandand every item inresolved_argsare unquoted strings representing the command and its arguments.
Passing the command and arguments as a list with shell=False (the default) is safer, because it avoids any interpretation by a shell, preventing command injection vulnerabilities.
💬 Ignore this finding
Reply with Semgrep commands to ignore this finding.
/fp <comment>for false positive/ar <comment>for acceptable risk/other <comment>for all other reasons
Alternatively, triage in Semgrep AppSec Platform to ignore the finding created by subprocess-shell-true.
You can view more details about this finding in the Semgrep AppSec Platform.
There was a problem hiding this comment.
Might be good opportunity to also consolidate existing VAI launchers to use env vars too instead of injected runtime args:
GiGL/gigl/src/common/vertex_ai_launcher.py
Lines 256 to 264 in 454b54b
Adopting one distinct pattern will make it easier for us to maintain the different code paths, and I am happy to adapt the env variables given prior discussed arguments as the golden path, but lets also old our old injection paths in existing launchers.
|
|
||
| GIGL_APPLIED_TASK_IDENTIFIER_ENV_KEY: Final[str] = "GIGL_APPLIED_TASK_IDENTIFIER" | ||
| GIGL_TASK_CONFIG_URI_ENV_KEY: Final[str] = "GIGL_TASK_CONFIG_URI" | ||
| GIGL_RESOURCE_CONFIG_URI_ENV_KEY: Final[str] = "GIGL_RESOURCE_CONFIG_URI" |
There was a problem hiding this comment.
We have RESOURCE_CONFIG_OS_ENV already defined in code.
See: gigl/env/pipelines_config.py
Some opportunity to consolidate / refactor.
There was a problem hiding this comment.
Hmmm, how about a new gigl/env/constants.py? And then migrate the existing RC one in as a follow up?
I think there's two things here tho:
- These env vars aren't always defined (though we can also add them to the VAI path I suppose which is probably a good idea?)
- now we have both
dep_constantsandconstantsthat may get confusing.
WDYT? I'm fine with adding constants and then moving in the existing var in a future PR (in additional to exposing in our VAI jobs)
There was a problem hiding this comment.
This plan sounds good to me.
One small suggestion; should we export these variables @ the start of each component too where possible?
i.e. currently we have initialize_metrics(task_config_uri=task_config_uri, service_name=args.job_name) being called at the start of every component. We could refactor this to initialize_gigl_env which can both init the metrics and make available these env variables. Having the same behaviour throughout will bring down the long term complexity of the codebase.
There was a problem hiding this comment.
It would also simplify our code below for "passing the env variables to subprocesses" as those env variables would already be available everywhere.
But open to thoughts.
There was a problem hiding this comment.
One small suggestion; should we export these variables @ the start of each component too where possible?
Yes! I can look into setting this up as a follow up :)
There was a problem hiding this comment.
I'd prefer to have this PR in first as the other changes may be somewhat complicated still (and I'm not sure we need all of these things to be exposed globally)
| GIGL_RESOURCE_CONFIG_URI_ENV_KEY: Final[str] = "GIGL_RESOURCE_CONFIG_URI" | ||
| GIGL_PROCESS_COMMAND_ENV_KEY: Final[str] = "GIGL_PROCESS_COMMAND" | ||
| GIGL_CPU_DOCKER_URI_ENV_KEY: Final[str] = "GIGL_CPU_DOCKER_URI" | ||
| GIGL_CUDA_DOCKER_URI_ENV_KEY: Final[str] = "GIGL_CUDA_DOCKER_URI" |
There was a problem hiding this comment.
Could you remind me what does cpu and cuda docker images mean in this case?
There was a problem hiding this comment.
they're the containers we launch on e.x vai for custom training.
| GIGL_APPLIED_TASK_IDENTIFIER_ENV_KEY: Final[str] = "GIGL_APPLIED_TASK_IDENTIFIER" | ||
| GIGL_TASK_CONFIG_URI_ENV_KEY: Final[str] = "GIGL_TASK_CONFIG_URI" | ||
| GIGL_RESOURCE_CONFIG_URI_ENV_KEY: Final[str] = "GIGL_RESOURCE_CONFIG_URI" | ||
| GIGL_PROCESS_COMMAND_ENV_KEY: Final[str] = "GIGL_PROCESS_COMMAND" |
There was a problem hiding this comment.
could you remind me what is the process command in this case?
Adding comments could help.
There was a problem hiding this comment.
This is like python -m my.launcher.code I guess we can skip this one?
| self.assertNotIn(key, os.environ) | ||
|
|
||
| @patch("gigl.src.common.custom_launcher.subprocess.run") | ||
| def test_dispatch_preserves_inherited_env(self, mock_run: MagicMock) -> None: |
There was a problem hiding this comment.
cant this test and the first test be both combined to reduce verbosity?
| process_command="echo", | ||
| process_runtime_args={}, | ||
| cpu_docker_uri=None, | ||
| cuda_docker_uri=None, |
There was a problem hiding this comment.
In our code we usually default these
cuda_docker_uri or DEFAULT_GIGL_RELEASE_SRC_IMAGE_CUDA
Could you remind me the reason we need to deviate here?
There was a problem hiding this comment.
No reason, we can use the default ones if you prefer.
Do this so custom launchers can get the task config, etc.
The alternative approach here is that we can use omegaconf but as we discussed offline that may be more complicated as the resolution would change often.