From 677181d23c334a314a7ee5d91556a17d69050d7e Mon Sep 17 00:00:00 2001 From: Pascal Date: Sat, 25 Jul 2026 20:45:21 +0200 Subject: [PATCH 1/2] Fix Windows argument quoting for backslash runs before quotes The command line builder doubles a backslash only when the next source character is a quote. Per the CRT parsing rules, every backslash in a run preceding a double quote must be doubled, and this includes a run that ends up before the generated closing quote of a quoted argument. As a result, a quoted argument ending in a backslash swallows its closing quote and merges with every following argument, a run of two or more backslashes before an embedded quote is under-escaped, and a quoted argument ending in several backslashes loses half of them. Count backslash runs in the length and emit passes and double the whole run before an escaped quote or before the closing quote. Extend the special argv test with the three cases plus a merge canary. --- subprocess.h | 81 ++++++++++++++++++++---------- test/process_return_special_argv.c | 7 +++ test/test_shared.h | 11 +++- 3 files changed, 71 insertions(+), 28 deletions(-) diff --git a/subprocess.h b/subprocess.h index 5e80902..71127af 100644 --- a/subprocess.h +++ b/subprocess.h @@ -653,6 +653,7 @@ int subprocess_create_ex(const char *const commandLine[], int options, int wide_len; int i, j; int need_quoting; + subprocess_size_t bs_run; unsigned long flags = 0; unsigned long last_error = 0; int result = subprocess_error_unknown; @@ -906,25 +907,35 @@ int subprocess_create_ex(const char *const commandLine[], int options, len++; // Quote the argument if it has a space in it - if (strpbrk(commandLine[i], "\t\v ") != SUBPROCESS_NULL || - commandLine[i][0] == SUBPROCESS_NULL) + need_quoting = strpbrk(commandLine[i], "\t\v ") != SUBPROCESS_NULL || + commandLine[i][0] == SUBPROCESS_NULL; + if (need_quoting) len += 2; - for (j = 0; '\0' != commandLine[i][j]; j++) { - switch (commandLine[i][j]) { - default: - break; - case '\\': - if (commandLine[i][j + 1] == '"') { + for (j = 0; '\0' != commandLine[i][j];) { + if ('\\' == commandLine[i][j]) { + bs_run = 0; + while ('\\' == commandLine[i][j]) { + bs_run++; + j++; + } + if ('"' == commandLine[i][j]) { + // the whole run is doubled before an escaped quote + len += (bs_run * 2) + 2; + j++; + } else if ('\0' == commandLine[i][j] && need_quoting) { + // the whole run is doubled before the closing quote + len += bs_run * 2; + } else { + len += bs_run; + } + } else { + if ('"' == commandLine[i][j]) { len++; } - - break; - case '"': len++; - break; + j++; } - len++; } } @@ -949,22 +960,40 @@ int subprocess_create_ex(const char *const commandLine[], int options, commandLineCombined[len++] = '"'; } - for (j = 0; '\0' != commandLine[i][j]; j++) { - switch (commandLine[i][j]) { - default: - break; - case '\\': - if (commandLine[i][j + 1] == '"') { + for (j = 0; '\0' != commandLine[i][j];) { + if ('\\' == commandLine[i][j]) { + bs_run = 0; + while ('\\' == commandLine[i][j]) { + bs_run++; + j++; + } + if ('"' == commandLine[i][j]) { + // the whole run is doubled before an escaped quote + for (; bs_run > 0; bs_run--) { + commandLineCombined[len++] = '\\'; + commandLineCombined[len++] = '\\'; + } commandLineCombined[len++] = '\\'; + commandLineCombined[len++] = '"'; + j++; + } else if ('\0' == commandLine[i][j] && need_quoting) { + // the whole run is doubled before the closing quote + for (; bs_run > 0; bs_run--) { + commandLineCombined[len++] = '\\'; + commandLineCombined[len++] = '\\'; + } + } else { + for (; bs_run > 0; bs_run--) { + commandLineCombined[len++] = '\\'; + } } - - break; - case '"': - commandLineCombined[len++] = '\\'; - break; + } else { + if ('"' == commandLine[i][j]) { + commandLineCombined[len++] = '\\'; + } + commandLineCombined[len++] = commandLine[i][j]; + j++; } - - commandLineCombined[len++] = commandLine[i][j]; } if (need_quoting) { commandLineCombined[len++] = '"'; diff --git a/test/process_return_special_argv.c b/test/process_return_special_argv.c index c60907b..0d24464 100644 --- a/test/process_return_special_argv.c +++ b/test/process_return_special_argv.c @@ -26,8 +26,15 @@ #include int main(int argc, const char *const argv[]) { unsigned int res = 0; + if (argc != 8) { + return 0; + } res |= ((strcmp(argv[1], "foo\nbar") == 0) << 0); res |= ((strcmp(argv[2], "\"baz\"") == 0) << 1); res |= ((strcmp(argv[3], "faz\\\"faz") == 0) << 2); + res |= ((strcmp(argv[4], "a b\\") == 0) << 3); + res |= ((strcmp(argv[5], "foo\\\\\"bar") == 0) << 4); + res |= ((strcmp(argv[6], "a b\\\\") == 0) << 5); + res |= ((strcmp(argv[7], "after") == 0) << 6); return (int)res; } \ No newline at end of file diff --git a/test/test_shared.h b/test/test_shared.h index a694ed6..20e60ea 100644 --- a/test/test_shared.h +++ b/test/test_shared.h @@ -411,14 +411,21 @@ SUBPROCESS_TEST(create, subprocess_stderr_argv) { SUBPROCESS_TEST(create, subprocess_return_special_argv) { const char *const commandLine[] = {"./process_return_special_argv", - "foo\nbar", "\"baz\"", "faz\\\"faz", 0}; + "foo\nbar", + "\"baz\"", + "faz\\\"faz", + "a b\\", + "foo\\\\\"bar", + "a b\\\\", + "after", + 0}; struct subprocess_s process; int ret = -1; ASSERT_EQ(0, subprocess_create(commandLine, 0, &process)); ASSERT_EQ(0, subprocess_join(&process, &ret)); - ASSERT_EQ(7, ret); // 0b111 + ASSERT_EQ(127, ret); // 0b1111111 ASSERT_EQ(0, subprocess_destroy(&process)); } From 8df3c5c724f8819a96805ec65a8087f4e440a546 Mon Sep 17 00:00:00 2001 From: Pascal Date: Mon, 27 Jul 2026 08:53:20 +0200 Subject: [PATCH 2/2] Simplify Windows argument quoting passes Rework the two command line builder passes without changing the emitted bytes. The length pass becomes a linear scan that carries the current backslash run, and the emit pass folds the duplicated write loops into the explicit 2n + 1 and 2n CRT formulas. Extend the special argv test with a quoted argument combining a space and a backslash run before an embedded quote. --- subprocess.h | 83 ++++++++++++------------------ test/process_return_special_argv.c | 7 +-- test/test_shared.h | 3 +- 3 files changed, 39 insertions(+), 54 deletions(-) diff --git a/subprocess.h b/subprocess.h index 71127af..b06ad4d 100644 --- a/subprocess.h +++ b/subprocess.h @@ -912,31 +912,25 @@ int subprocess_create_ex(const char *const commandLine[], int options, if (need_quoting) len += 2; - for (j = 0; '\0' != commandLine[i][j];) { + bs_run = 0; + for (j = 0; '\0' != commandLine[i][j]; j++) { + len++; + if ('\\' == commandLine[i][j]) { - bs_run = 0; - while ('\\' == commandLine[i][j]) { - bs_run++; - j++; - } - if ('"' == commandLine[i][j]) { - // the whole run is doubled before an escaped quote - len += (bs_run * 2) + 2; - j++; - } else if ('\0' == commandLine[i][j] && need_quoting) { - // the whole run is doubled before the closing quote - len += bs_run * 2; - } else { - len += bs_run; - } + bs_run++; } else { if ('"' == commandLine[i][j]) { - len++; + // Duplicate the preceding run and escape the quote. + len += bs_run + 1; } - len++; - j++; + bs_run = 0; } } + + if (need_quoting) { + // Duplicate trailing slashes before the generated closing quote. + len += bs_run; + } } commandLineCombined = SUBPROCESS_CAST(char *, _alloca(len)); @@ -961,39 +955,28 @@ int subprocess_create_ex(const char *const commandLine[], int options, } for (j = 0; '\0' != commandLine[i][j];) { - if ('\\' == commandLine[i][j]) { - bs_run = 0; - while ('\\' == commandLine[i][j]) { - bs_run++; - j++; - } - if ('"' == commandLine[i][j]) { - // the whole run is doubled before an escaped quote - for (; bs_run > 0; bs_run--) { - commandLineCombined[len++] = '\\'; - commandLineCombined[len++] = '\\'; - } - commandLineCombined[len++] = '\\'; - commandLineCombined[len++] = '"'; - j++; - } else if ('\0' == commandLine[i][j] && need_quoting) { - // the whole run is doubled before the closing quote - for (; bs_run > 0; bs_run--) { - commandLineCombined[len++] = '\\'; - commandLineCombined[len++] = '\\'; - } - } else { - for (; bs_run > 0; bs_run--) { - commandLineCombined[len++] = '\\'; - } - } - } else { - if ('"' == commandLine[i][j]) { - commandLineCombined[len++] = '\\'; - } - commandLineCombined[len++] = commandLine[i][j]; + bs_run = 0; + while ('\\' == commandLine[i][j]) { + bs_run++; j++; } + + if ('"' == commandLine[i][j]) { + // 2n + 1 slashes preserve n slashes and escape the quote. + bs_run = (bs_run * 2) + 1; + } else if ('\0' == commandLine[i][j] && need_quoting) { + // 2n slashes preserve n slashes before the closing quote. + bs_run *= 2; + } + + while (bs_run > 0) { + commandLineCombined[len++] = '\\'; + bs_run--; + } + + if ('\0' != commandLine[i][j]) { + commandLineCombined[len++] = commandLine[i][j++]; + } } if (need_quoting) { commandLineCombined[len++] = '"'; diff --git a/test/process_return_special_argv.c b/test/process_return_special_argv.c index 0d24464..c921c0b 100644 --- a/test/process_return_special_argv.c +++ b/test/process_return_special_argv.c @@ -26,7 +26,7 @@ #include int main(int argc, const char *const argv[]) { unsigned int res = 0; - if (argc != 8) { + if (argc != 9) { return 0; } res |= ((strcmp(argv[1], "foo\nbar") == 0) << 0); @@ -35,6 +35,7 @@ int main(int argc, const char *const argv[]) { res |= ((strcmp(argv[4], "a b\\") == 0) << 3); res |= ((strcmp(argv[5], "foo\\\\\"bar") == 0) << 4); res |= ((strcmp(argv[6], "a b\\\\") == 0) << 5); - res |= ((strcmp(argv[7], "after") == 0) << 6); + res |= ((strcmp(argv[7], "a \\\\\"b") == 0) << 6); + res |= ((strcmp(argv[8], "after") == 0) << 7); return (int)res; -} \ No newline at end of file +} diff --git a/test/test_shared.h b/test/test_shared.h index 20e60ea..4a57e14 100644 --- a/test/test_shared.h +++ b/test/test_shared.h @@ -417,6 +417,7 @@ SUBPROCESS_TEST(create, subprocess_return_special_argv) { "a b\\", "foo\\\\\"bar", "a b\\\\", + "a \\\\\"b", "after", 0}; struct subprocess_s process; @@ -425,7 +426,7 @@ SUBPROCESS_TEST(create, subprocess_return_special_argv) { ASSERT_EQ(0, subprocess_create(commandLine, 0, &process)); ASSERT_EQ(0, subprocess_join(&process, &ret)); - ASSERT_EQ(127, ret); // 0b1111111 + ASSERT_EQ(255, ret); // 0b11111111 ASSERT_EQ(0, subprocess_destroy(&process)); }