diff options
| author | Paul Buetow <paul@buetow.org> | 2026-07-20 16:37:52 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-07-20 16:37:52 +0300 |
| commit | 220b3806e97da745bad6f3b42fd6dfabd4e28c0b (patch) | |
| tree | 6f1c91a3868908e775ccb29f72d6f5c04b0b78d6 /tests | |
| parent | dc430ce8ec427529c75e3527a0ff01fcf3858dd2 (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 'tests')
| -rw-r--r-- | tests/test_runner.c | 135 |
1 files changed, 134 insertions, 1 deletions
diff --git a/tests/test_runner.c b/tests/test_runner.c index 172368b..a3e2b5a 100644 --- a/tests/test_runner.c +++ b/tests/test_runner.c @@ -91,11 +91,144 @@ test_injection_guard(void) { g_free(d); } +/* Observable injection test: a hostile filename containing shell + * metacharacters that, if executed, would create a sentinel file. The + * single-quoted %f must prevent the ';' from being interpreted. */ +static void +test_hostile_filename(void) { + GError *e = NULL; + char *d = g_dir_make_tmp("ggaze-runner-XXXXXX", &e); + g_assert_no_error(e); + + /* Sentinel that would be created if the ';' in the name were executed. */ + char *sentinel_p = g_build_filename(d, "PWNED", NULL); + GFile *sentinel = g_file_new_for_path(sentinel_p); + + /* Hostile filename: ;touch <sentinel>; — if unquoted this runs touch. */ + char *name = g_strdup_printf(";touch %s;", sentinel_p); + char *fp = g_build_filename(d, name, NULL); + GFile *f = g_file_new_for_path(fp); + g_file_replace_contents(f, "x", 1, NULL, FALSE, + G_FILE_CREATE_REPLACE_DESTINATION, NULL, NULL, NULL); + GFile *dd = g_file_new_for_path(d); + + Runner *r = runner_new(); + RunnerScript s = {"true", "true %f"}; + run_and_wait(r, f, dd, &s); + g_assert_cmpint(g_exit_code, ==, 0); + /* The sentinel must NOT exist: the ';' was quoted, not executed. */ + g_assert_false(g_file_query_exists(sentinel, NULL)); + + runner_delete(r); + g_object_unref(sentinel); + g_object_unref(f); + g_object_unref(dd); + g_free(sentinel_p); + g_free(name); + g_free(fp); + GFile *ddd = g_file_new_for_path(d); + GFileEnumerator *en = g_file_enumerate_children( + ddd, "standard::name", G_FILE_QUERY_INFO_NONE, NULL, NULL); + if (en) { + GFileInfo *i; + while ((i = g_file_enumerator_next_file(en, NULL, NULL))) { + GFile *c = g_file_get_child(ddd, g_file_info_get_name(i)); + g_file_delete(c, NULL, NULL); + g_object_unref(c); + g_object_unref(i); + } + g_object_unref(en); + } + g_file_delete(ddd, NULL, NULL); + g_object_unref(ddd); + g_free(d); +} + +/* Run a command that writes to a temp outfile, wait, then read the outfile + * back and compare its contents to c_expected. Cleans up the outfile. */ +static void +_run_and_check_output(const char *c_command, const char *c_expected) { + GError *e = NULL; + char *d = g_dir_make_tmp("ggaze-runner-XXXXXX", &e); + g_assert_no_error(e); + char *out_p = g_build_filename(d, "out.txt", NULL); + GFile *dd = g_file_new_for_path(d); + + /* Interpolate the outfile path via %s wrapped in literal single quotes, + * e.g. `printf 'hello world\n' > '/tmp/.../out.txt'`. Temp paths from + * g_dir_make_tmp contain no quotes, so this is safe for the fixtures. */ + char *cmd = g_strdup_printf(c_command, out_p); + + Runner *r = runner_new(); + RunnerScript s = {"obs", cmd}; + run_and_wait(r, NULL, dd, &s); + g_assert_cmpint(g_exit_code, ==, 0); + + char *contents = NULL; + gsize len = 0; + g_assert_true(g_file_get_contents(out_p, &contents, &len, &e)); + g_assert_no_error(e); + g_assert_cmpstr(contents, ==, c_expected); + g_free(contents); + + g_free(cmd); + runner_delete(r); + g_object_unref(dd); + g_free(out_p); + GFile *ddd = g_file_new_for_path(d); + GFileEnumerator *en = g_file_enumerate_children( + ddd, "standard::name", G_FILE_QUERY_INFO_NONE, NULL, NULL); + if (en) { + GFileInfo *i; + while ((i = g_file_enumerator_next_file(en, NULL, NULL))) { + GFile *c = g_file_get_child(ddd, g_file_info_get_name(i)); + g_file_delete(c, NULL, NULL); + g_object_unref(c); + g_object_unref(i); + } + g_object_unref(en); + } + g_file_delete(ddd, NULL, NULL); + g_object_unref(ddd); + g_free(d); +} + +/* Regression: a multi-word command must reach sh -c intact. The buggy + * g_shell_parse_argv code fed sh -c only the first word ("printf"), so the + * outfile would never contain "hello world". */ +static void +test_multi_word(void) { + _run_and_check_output("printf 'hello world\\n' > '%s'", "hello world\n"); +} + +/* Pipeline: '|' must be parsed by sh, not split into argv words. */ +static void +test_pipeline(void) { + _run_and_check_output("echo pipeline | cat > '%s'", "pipeline\n"); +} + +/* Redirection: '>' must be interpreted by the shell. */ +static void +test_redirection(void) { + _run_and_check_output("echo redir > '%s'", "redir\n"); +} + +/* Multiple space-separated arguments survive intact. */ +static void +test_spaces_in_args(void) { + _run_and_check_output("printf 'a b c\\n' > '%s'", "a b c\n"); +} + int main(int argc, char **argv) { g_test_init(&argc, &argv, NULL); g_test_add_func("/runner/true_exit_zero", test_true_exit_zero); g_test_add_func("/runner/false_exit_nonzero", test_false_exit_nonzero); g_test_add_func("/runner/injection_guard", test_injection_guard); + g_test_add_func("/runner/hostile_filename", test_hostile_filename); + g_test_add_func("/runner/multi_word", test_multi_word); + g_test_add_func("/runner/pipeline", test_pipeline); + g_test_add_func("/runner/redirection", test_redirection); + g_test_add_func("/runner/spaces_in_args", test_spaces_in_args); return g_test_run(); -}
\ No newline at end of file +} |
