summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-07-20 16:37:52 +0300
committerPaul Buetow <paul@buetow.org>2026-07-20 16:37:52 +0300
commit220b3806e97da745bad6f3b42fd6dfabd4e28c0b (patch)
tree6f1c91a3868908e775ccb29f72d6f5c04b0b78d6 /src
parentdc430ce8ec427529c75e3527a0ff01fcf3858dd2 (diff)
runner: pass full expanded command as one sh -c arg (8u0)
runner_run built "/bin/sh -c " + cmd and ran g_shell_parse_argv on it, which split the command into argv words — sh -c then received only the first word as its script and treated the rest as $0/$1..., silently truncating multi-word commands, pipelines, and redirections. The existing 'true %f' test passed only because true ignores missing args. Build argv directly as {"/bin/sh", "-c", c_cmd, NULL} so sh -c gets the whole expanded command as one script string. %f/%d stay single- quoted via g_shell_quote, so hostile filenames cannot break out of the quotes and inject commands. Also removed dead code in _shell_quote. New observable tests (write to a temp file, read it back): - multi_word: printf 'hello world' (would fail on old code) - pipeline: echo | cat - redirection: echo > file - spaces_in_args: printf 'a b c' - hostile_filename: ';touch sentinel;' name must NOT create the sentinel
Diffstat (limited to 'src')
-rw-r--r--src/runner.c28
1 files changed, 14 insertions, 14 deletions
diff --git a/src/runner.c b/src/runner.c
index 8166e3e..53c38bd 100644
--- a/src/runner.c
+++ b/src/runner.c
@@ -54,10 +54,8 @@ runner_get_scripts(Runner *r) {
/* Single-quote a path for safe shell interpolation. Caller frees. */
static char *
_shell_quote(const char *c_path) {
- /* Replace each ' with '\'' and wrap in single quotes. */
- char *c_escaped = g_strdup_printf("'%s'", c_path);
- /* Simple approach: g_shell_quote does this correctly. */
- g_free(c_escaped);
+ /* g_shell_quote wraps the path in single quotes and escapes any
+ * embedded quotes, making it safe to interpolate into a sh -c script. */
return g_shell_quote(c_path);
}
@@ -102,18 +100,20 @@ runner_run(Runner *r, GFile *p_file, GFile *p_dir, const RunnerScript *p_script,
GAsyncReadyCallback p_cb, gpointer p_data, GError **p_err) {
(void)r;
g_return_val_if_fail(p_script, FALSE);
- char *c_cmd = _expand(p_script->c_command, p_file, p_dir);
- char *c_full = g_strdup_printf("/bin/sh -c %s", c_cmd);
- g_free(c_cmd);
- char **argv = NULL;
- if (!g_shell_parse_argv(c_full, NULL, &argv, p_err)) {
- g_free(c_full);
+ char *c_cmd = _expand(p_script->c_command, p_file, p_dir);
+ if (c_cmd == NULL) {
+ g_set_error(p_err, G_SHELL_ERROR, G_SHELL_ERROR_FAILED,
+ "runner: failed to expand script command");
return FALSE;
}
- g_free(c_full);
- GSubprocess *p_sub = g_subprocess_newv((const char *const *)argv,
- G_SUBPROCESS_FLAGS_NONE, p_err);
- g_strfreev(argv);
+ /* Pass the whole expanded command as a single argv element to sh -c so
+ * that pipelines, redirections, && and multi-word arguments are parsed
+ * by the shell as one script (not split by g_shell_parse_argv, which
+ * would feed sh -c only the first word and treat the rest as $0/$1...).
+ * %f/%d are already single-quoted by _expand, so paths stay safe. */
+ const char *argv[] = {"/bin/sh", "-c", c_cmd, NULL};
+ GSubprocess *p_sub = g_subprocess_newv(argv, G_SUBPROCESS_FLAGS_NONE, p_err);
+ g_free(c_cmd);
if (p_sub == NULL)
return FALSE;
if (p_cb) {