Replace popen with spawn - #211
Conversation
|
When I had a "Fabulous" LLM review this code, it said: |
|
Many things about this fix seem sensible, but why did popen() become spawn. What was bad about popen(). |
|
|
@Martin-Zeithaml, I'm looking at the code which seems to have evolved from a simpler PR to, well, a one with more moving parts. Does this PR, especially, after 4d9b256, contain changes which could better fit in multiple PRs? I've still not understood the whole PR so I might be wrong, but it feels like several PRs. |
|
479a8c3 to
f70f8b1
Compare
|
With permission from Martin, I've re-written the branch's history to have commit that match logical changes; the before and after changes match exactly. |
| static int run_command(const char *command, handle_line_callback_t handle_line, void *data) { | ||
| DEBUG("about to run command '%s'\n", command); | ||
| FILE *fp = popen(command, "r"); | ||
| static int run_command(const char *bin, const char *argv[], const char *envp[], handle_line_callback_t handle_line, void *data) { |
There was a problem hiding this comment.
Nit: this should probably now be called run_bin or similar.
| } | ||
|
|
||
| for (int i = 0; i < 2; i++) { | ||
| if (fcntl(c_stdout[i], F_SETFD, FD_CLOEXEC) < 0) { |
There was a problem hiding this comment.
What if a child process is created between the pipe call and fcntl(FD_CLOEXEC)? Isn't there still a window where we can leak descriptors?
Looks like we may need to use pipe2 with O_CLOEXEC. If so, let's not do that in this PR; I'd rather it is fixed separately since the existing code would also need to be changed.
| static int run_command(const char *command, handle_line_callback_t handle_line, void *data) { | ||
| DEBUG("about to run command '%s'\n", command); | ||
| FILE *fp = popen(command, "r"); | ||
| static int run_command(const char *bin, const char *argv[], const char *envp[], handle_line_callback_t handle_line, void *data) { |
There was a problem hiding this comment.
I think in the future we should consider handling hangs in the commands. Looks like now, if a child process hangs, and the launcher is terminated via SIGINT/SIGTERM, the child process will be left orphaned?
I mean, at this point we wouldn't even be able to terminate the launcher via the operator command because it'd happen before the console handler is set up.
ifakhrutdinov
left a comment
There was a problem hiding this comment.
It's good to go, but we'll need to improve on the following items in the future:
- It'd be nice not to duplicate all the
spawnlogic between components andrun_command. I know 2 instances is fine to duplicate, but in this case there is a lot of error prone code. We could at least abstract away some logic. - Some fixes could leave in a separate PR; and some fixes like CLOEXEC seem really irrelevant (all the
spawncalls usefd_map, so leaks are kind of impossible) and also not 100% done.
Preserve existing open flags when setting O_NONBLOCK on the read end (F_GETFL before F_SETFL instead of overwriting), check the fcntl return codes, and set FD_CLOEXEC on both pipe fds so they are not inherited by unrelated children. Also close both pipe fds on every error path to avoid descriptor leaks. Signed-off-by: Martin Zeithaml <Martin.Zeithaml@broadcom.com>
Run the instance-prepare step (configmgr JS) via spawn() with an explicit argv and the shared_uss_env envp array instead of building a single shell command string and running it through popen(). Because spawn() takes the environment as a real argv/envp pair, the shell is no longer involved, so the manual per-value quoting and the whole command-string assembly are gone: - remove get_sharedenv() (shell value quoting workaround) - remove get_start_prepare_cmd() (command-string builder) run_command() now creates its own pipe, spawns the child with stdout/stderr redirected, drains the output, and waitpid()s for the exit status (WIFEXITED/WEXITSTATUS), with EINTR retry. prepare_instance() builds the configmgr binary path and the cli.js path with snprintf and checks for truncation. Historical note: popen here was never a spawn replacement; it was added later to run helper shell scripts. Those are now JS invoked via configmgr, so the shell layer is no longer needed. Signed-off-by: Martin Zeithaml <Martin.Zeithaml@broadcom.com>
When debug logging is on, print the keys (names only, never values) of the environment passed to spawn(). This aids troubleshooting without leaking secrets that may be present in the environment values. Signed-off-by: Martin Zeithaml <Martin.Zeithaml@broadcom.com>
f70f8b1 to
63d067e
Compare
Why the change
get_sharedenvwrapped each value in"…"). Any value containing quotes,$, spaces, etc. could corrupt the command or be misinterpreted by the shell.argvstring risks overflowing shell/exec length limits on large configurations.Review pointers for maintainers
spawn.hspawn()withfdremapping) — verify it builds and runs on the target LE/USS environment.shared_uss_envis always NULL-terminated and non-NULL by the timeprepare_instance()runsenvpdebug dump at the top ofrun_commanditeratesenvp— worth a sanity check that env logging in debug mode doesn't leak anything sensitive.