load-fragment: allow quoting in command name and document allowed escapes

The handling of the command name and other arguments is unified. This
simplifies things and should make them more predictable for users.
Incidentally, this makes ExecStart handling match the .desktop file
specification, apart for the requirment for an absolute path.

https://bugs.freedesktop.org/show_bug.cgi?id=86171
This commit is contained in:
Zbigniew Jędrzejewski-Szmek 2014-12-18 18:08:13 -05:00
parent ba774317ac
commit c853953658
3 changed files with 272 additions and 107 deletions

View file

@ -377,11 +377,10 @@
<para>For each of the specified
commands, the first argument must be
an absolute and literal path to an
executable. Optionally, if the
absolute file name is prefixed with
<literal>@</literal>, the second token
will be passed as
an absolute path to an executable.
Optionally, if this file name is
prefixed with <literal>@</literal>,
the second token will be passed as
<literal>argv[0]</literal> to the
executed process, followed by the
further arguments specified. If the
@ -1148,13 +1147,15 @@
<para>Each command line is split on whitespace, with
the first item being the command to execute, and the
subsequent items being the arguments. Double quotes
subsequent items being the arguments. Double quotes
("...") and single quotes ('...') may be used, in
which case everything until the next matching quote
becomes part of the same argument. Quotes themselves
are removed after parsing. In addition, a trailing
backslash (<literal>\</literal>) may be used to merge
lines. </para>
becomes part of the same argument. C-style escapes are
also supported, see table below. Quotes themselves are
removed after parsing and escape sequences
substituted. In addition, a trailing backslash
(<literal>\</literal>) may be used to merge lines.
</para>
<para>This syntax is intended to be very similar to
shell syntax, but only the meta-characters and
@ -1168,6 +1169,10 @@
<emphasis>other elements of shell syntax are not
supported</emphasis>.</para>
<para>The command to execute must an absolute path
name. It may contain spaces, but control characters
are not allowed.</para>
<para>The command line accepts <literal>%</literal>
specifiers as described in
<citerefentry><refentrytitle>systemd.unit</refentrytitle><manvolnum>5</manvolnum></citerefentry>.
@ -1253,6 +1258,74 @@ ExecStart=/bin/echo $ONE $TWO $THREE</programlisting>
<literal>&gt;/dev/null</literal>,
<literal>&amp;</literal>, <literal>;</literal>, and
<literal>/bin/ls</literal>.</para>
<table>
<title>C escapes supported in command lines and environment variables</title>
<tgroup cols='2'>
<colspec colname='escape' />
<colspec colname='meaning' />
<thead>
<row>
<entry>Literal</entry>
<entry>Actual value</entry>
</row>
</thead>
<tbody>
<row>
<entry><literal>\a</literal></entry>
<entry>bell</entry>
</row>
<row>
<entry><literal>\b</literal></entry>
<entry>backspace</entry>
</row>
<row>
<entry><literal>\f</literal></entry>
<entry>form feed</entry>
</row>
<row>
<entry><literal>\n</literal></entry>
<entry>newline</entry>
</row>
<row>
<entry><literal>\r</literal></entry>
<entry>carriage return</entry>
</row>
<row>
<entry><literal>\t</literal></entry>
<entry>tab</entry>
</row>
<row>
<entry><literal>\v</literal></entry>
<entry>vertical tab</entry>
</row>
<row>
<entry><literal>\\</literal></entry>
<entry>backslash</entry>
</row>
<row>
<entry><literal>\"</literal></entry>
<entry>double quotation mark</entry>
</row>
<row>
<entry><literal>\'</literal></entry>
<entry>single quotation mark</entry>
</row>
<row>
<entry><literal>\s</literal></entry>
<entry>space</entry>
</row>
<row>
<entry><literal>\x<replaceable>xx</replaceable></literal></entry>
<entry>character number <replaceable>xx</replaceable> in hexadecimal encoding</entry>
</row>
<row>
<entry><literal>\<replaceable>nnn</replaceable></literal></entry>
<entry>character number <replaceable>nnn</replaceable> in octal encoding</entry>
</row>
</tbody>
</tgroup>
</table>
</refsect1>
<refsect1>

View file

@ -547,9 +547,9 @@ int config_parse_exec(const char *unit,
* overriding of argv[0]. */
for (;;) {
int i;
const char *word, *state;
const char *word, *state, *reason;
size_t l;
bool honour_argv0 = false, ignore = false;
bool separate_argv0 = false, ignore = false;
path = NULL;
nce = NULL;
@ -560,28 +560,23 @@ int config_parse_exec(const char *unit,
if (rvalue[0] == 0)
break;
for (i = 0; i < 2; i++) {
if (rvalue[0] == '-' && !ignore) {
ignore = true;
rvalue ++;
}
if (rvalue[0] == '@' && !honour_argv0) {
honour_argv0 = true;
rvalue ++;
}
}
if (*rvalue != '/') {
log_syntax(unit, LOG_ERR, filename, line, EINVAL,
"Executable path is not absolute, ignoring: %s", rvalue);
return 0;
}
k = 0;
FOREACH_WORD_QUOTED(word, l, rvalue, state) {
if (strneq(word, ";", MAX(l, 1U)))
goto found;
if (k == 0) {
for (i = 0; i < 2; i++) {
if (*word == '-' && !ignore) {
ignore = true;
word ++;
}
if (*word == '@' && !separate_argv0) {
separate_argv0 = true;
word ++;
}
}
} else
if (strneq(word, ";", MAX(l, 1U)))
goto found;
k++;
}
@ -592,60 +587,69 @@ int config_parse_exec(const char *unit,
}
found:
n = new(char*, k + !honour_argv0);
n = new(char*, k + !separate_argv0);
if (!n)
return log_oom();
k = 0;
FOREACH_WORD_QUOTED(word, l, rvalue, state) {
if (strneq(word, ";", MAX(l, 1U)))
char *c;
unsigned skip;
if (separate_argv0 ? path == NULL : k == 0) {
/* first word, very special */
skip = separate_argv0 + ignore;
/* skip special chars in the beginning */
assert(skip < l);
} else if (strneq(word, ";", MAX(l, 1U)))
/* new commandline */
break;
else if (strneq(word, "\\;", MAX(l, 1U))) {
word ++;
l --;
else
skip = strneq(word, "\\;", MAX(l, 1U));
c = cunescape_length(word + skip, l - skip);
if (!c) {
r = log_oom();
goto fail;
}
if (honour_argv0 && word == rvalue) {
assert(!path);
path = strndup(word, l);
if (!path) {
r = log_oom();
goto fail;
}
if (!utf8_is_valid(path)) {
log_invalid_utf8(unit, LOG_ERR, filename, line, EINVAL, rvalue);
r = 0;
goto fail;
}
} else {
char *c;
c = n[k++] = cunescape_length(word, l);
if (!c) {
r = log_oom();
goto fail;
}
if (!utf8_is_valid(c)) {
log_invalid_utf8(unit, LOG_ERR, filename, line, EINVAL, rvalue);
r = 0;
goto fail;
}
if (!utf8_is_valid(c)) {
log_invalid_utf8(unit, LOG_ERR, filename, line, EINVAL, rvalue);
r = 0;
goto fail;
}
/* where to stuff this? */
if (separate_argv0 && path == NULL)
path = c;
else
n[k++] = c;
}
n[k] = NULL;
if (!n[0]) {
log_syntax(unit, LOG_ERR, filename, line, EINVAL,
"Invalid command line, ignoring: %s", rvalue);
r = 0;
goto fail;
}
log_debug("path: %s", path ?: n[0]);
if (!n[0])
reason = "Empty executable name or zeroeth argument";
else if (!string_is_safe(path ?: n[0]))
reason = "Executable path contains special characters";
else if (!path_is_absolute(path ?: n[0]))
reason = "Executable path is not absolute";
else if (endswith(path ?: n[0], "/"))
reason = "Executable path specifies a directory";
else
goto ok;
log_syntax(unit, LOG_ERR, filename, line, EINVAL,
"%s, ignoring: %s", reason, rvalue);
r = 0;
goto fail;
ok:
if (!path) {
path = strdup(n[0]);
if (!path) {
@ -654,8 +658,6 @@ int config_parse_exec(const char *unit,
}
}
assert(path_is_absolute(path));
nce = new0(ExecCommand, 1);
if (!nce) {
r = log_oom();

View file

@ -74,31 +74,34 @@ static void check_execcommand(ExecCommand *c,
const char* argv2,
bool ignore) {
assert_se(c);
log_info("%s %s %s %s",
log_info("expect: \"%s\" [\"%s\" \"%s\" \"%s\"]",
path, argv0 ?: path, argv1, argv2);
log_info("actual: \"%s\" [\"%s\" \"%s\" \"%s\"]",
c->path, c->argv[0], c->argv[1], c->argv[2]);
assert_se(streq(c->path, path));
assert_se(streq(c->argv[0], argv0));
assert_se(streq(c->argv[1], argv1));
assert_se(streq(c->argv[0], argv0 ?: path));
assert_se(streq_ptr(c->argv[1], argv1));
assert_se(streq_ptr(c->argv[2], argv2));
assert_se(c->ignore == ignore);
}
static void test_config_parse_exec(void) {
/* int config_parse_exec( */
/* const char *filename, */
/* unsigned line, */
/* const char *section, */
/* unsigned section_line, */
/* const char *lvalue, */
/* int ltype, */
/* const char *rvalue, */
/* void *data, */
/* void *userdata) */
/* int config_parse_exec(
const char *filename,
unsigned line,
const char *section,
unsigned section_line,
const char *lvalue,
int ltype,
const char *rvalue,
void *data,
void *userdata) */
int r;
ExecCommand *c = NULL, *c1;
const char *ccc;
/* basic test */
log_info("/* basic test */");
r = config_parse_exec(NULL, "fake", 1, "section", 1,
"LValue", 0, "/RValue r1",
&c, NULL);
@ -106,52 +109,60 @@ static void test_config_parse_exec(void) {
check_execcommand(c, "/RValue", "/RValue", "r1", NULL, false);
r = config_parse_exec(NULL, "fake", 2, "section", 1,
"LValue", 0, "/RValue///slashes/// r1",
"LValue", 0, "/RValue///slashes r1///",
&c, NULL);
/* test slashes */
log_info("/* test slashes */");
assert_se(r >= 0);
c1 = c->command_next;
check_execcommand(c1, "/RValue/slashes", "/RValue///slashes///", "r1", NULL, false);
check_execcommand(c1, "/RValue/slashes", "/RValue///slashes", "r1///", NULL, false);
/* honour_argv0 */
log_info("/* trailing slash */");
r = config_parse_exec(NULL, "fake", 4, "section", 1,
"LValue", 0, "/RValue/ argv0 r1",
&c, NULL);
assert_se(r == 0);
assert_se(c1->command_next == NULL);
log_info("/* honour_argv0 */");
r = config_parse_exec(NULL, "fake", 3, "section", 1,
"LValue", 0, "@/RValue///slashes2/// argv0 r1",
"LValue", 0, "@/RValue///slashes2 ///argv0 r1",
&c, NULL);
assert_se(r >= 0);
c1 = c1->command_next;
check_execcommand(c1, "/RValue/slashes2", "argv0", "r1", NULL, false);
check_execcommand(c1, "/RValue/slashes2", "///argv0", "r1", NULL, false);
/* ignore && honour_argv0 */
log_info("/* ignore && honour_argv0 */");
r = config_parse_exec(NULL, "fake", 4, "section", 1,
"LValue", 0, "-@/RValue///slashes3/// argv0a r1",
"LValue", 0, "-@/RValue///slashes3 argv0a r1",
&c, NULL);
assert_se(r >= 0);
c1 = c1->command_next;
check_execcommand(c1, "/RValue/slashes3", "argv0a", "r1", NULL, true);
/* ignore && honour_argv0 */
log_info("/* ignore && honour_argv0 */");
r = config_parse_exec(NULL, "fake", 4, "section", 1,
"LValue", 0, "@-/RValue///slashes4/// argv0b r1",
"LValue", 0, "@-/RValue///slashes4 argv0b r1",
&c, NULL);
assert_se(r >= 0);
c1 = c1->command_next;
check_execcommand(c1, "/RValue/slashes4", "argv0b", "r1", NULL, true);
/* ignore && ignore */
log_info("/* ignore && ignore */");
r = config_parse_exec(NULL, "fake", 4, "section", 1,
"LValue", 0, "--/RValue argv0 r1",
&c, NULL);
assert_se(r == 0);
assert_se(c1->command_next == NULL);
/* ignore && ignore */
log_info("/* ignore && ignore (2) */");
r = config_parse_exec(NULL, "fake", 4, "section", 1,
"LValue", 0, "-@-/RValue argv0 r1",
&c, NULL);
assert_se(r == 0);
assert_se(c1->command_next == NULL);
/* semicolon */
log_info("/* semicolon */");
r = config_parse_exec(NULL, "fake", 5, "section", 1,
"LValue", 0,
"-@/RValue argv0 r1 ; "
@ -162,9 +173,9 @@ static void test_config_parse_exec(void) {
check_execcommand(c1, "/RValue", "argv0", "r1", NULL, true);
c1 = c1->command_next;
check_execcommand(c1, "/goo/goo", "/goo/goo", "boo", NULL, false);
check_execcommand(c1, "/goo/goo", NULL, "boo", NULL, false);
/* trailing semicolon */
log_info("/* trailing semicolon */");
r = config_parse_exec(NULL, "fake", 5, "section", 1,
"LValue", 0,
"-@/RValue argv0 r1 ; ",
@ -175,16 +186,16 @@ static void test_config_parse_exec(void) {
assert_se(c1->command_next == NULL);
/* escaped semicolon */
log_info("/* escaped semicolon */");
r = config_parse_exec(NULL, "fake", 5, "section", 1,
"LValue", 0,
"/bin/find \\;",
&c, NULL);
assert_se(r >= 0);
c1 = c1->command_next;
check_execcommand(c1, "/bin/find", "/bin/find", ";", NULL, false);
check_execcommand(c1, "/bin/find", NULL, ";", NULL, false);
/* escaped semicolon with following arg */
log_info("/* escaped semicolon with following arg */");
r = config_parse_exec(NULL, "fake", 5, "section", 1,
"LValue", 0,
"/sbin/find \\; x",
@ -192,7 +203,86 @@ static void test_config_parse_exec(void) {
assert_se(r >= 0);
c1 = c1->command_next;
check_execcommand(c1,
"/sbin/find", "/sbin/find", ";", "x", false);
"/sbin/find", NULL, ";", "x", false);
log_info("/* spaces in the filename */");
r = config_parse_exec(NULL, "fake", 5, "section", 1,
"LValue", 0,
"\"/PATH WITH SPACES/daemon\" -1 -2",
&c, NULL);
assert_se(r >= 0);
c1 = c1->command_next;
check_execcommand(c1,
"/PATH WITH SPACES/daemon", NULL, "-1", "-2", false);
log_info("/* spaces in the filename, no args */");
r = config_parse_exec(NULL, "fake", 5, "section", 1,
"LValue", 0,
"\"/PATH WITH SPACES/daemon -1 -2\"",
&c, NULL);
assert_se(r >= 0);
c1 = c1->command_next;
check_execcommand(c1,
"/PATH WITH SPACES/daemon -1 -2", NULL, NULL, NULL, false);
log_info("/* spaces in the filename, everything quoted */");
r = config_parse_exec(NULL, "fake", 5, "section", 1,
"LValue", 0,
"\"/PATH WITH SPACES/daemon\" \"-1\" '-2'",
&c, NULL);
assert_se(r >= 0);
c1 = c1->command_next;
check_execcommand(c1,
"/PATH WITH SPACES/daemon", NULL, "-1", "-2", false);
log_info("/* escaped spaces in the filename */");
r = config_parse_exec(NULL, "fake", 5, "section", 1,
"LValue", 0,
"\"/PATH\\sWITH\\sSPACES/daemon\" '-1 -2'",
&c, NULL);
assert_se(r >= 0);
c1 = c1->command_next;
check_execcommand(c1,
"/PATH WITH SPACES/daemon", NULL, "-1 -2", NULL, false);
log_info("/* escaped spaces in the filename (2) */");
r = config_parse_exec(NULL, "fake", 5, "section", 1,
"LValue", 0,
"\"/PATH\\x20WITH\\x20SPACES/daemon\" \"-1 -2\"",
&c, NULL);
assert_se(r >= 0);
c1 = c1->command_next;
check_execcommand(c1,
"/PATH WITH SPACES/daemon", NULL, "-1 -2", NULL, false);
for (ccc = "abfnrtv\\\'\"x"; *ccc; ccc++) {
/* \\x is an incomplete hexadecimal sequence, invalid because of the slash */
char path[] = "/path\\X";
path[sizeof(path) - 2] = *ccc;
log_info("/* invalid character: \\%c */", *ccc);
r = config_parse_exec(NULL, "fake", 4, "section", 1,
"LValue", 0, path,
&c, NULL);
assert_se(r == 0);
assert_se(c1->command_next == NULL);
}
log_info("/* valid character: \\s */");
r = config_parse_exec(NULL, "fake", 4, "section", 1,
"LValue", 0, "/path\\s",
&c, NULL);
assert_se(r >= 0);
c1 = c1->command_next;
check_execcommand(c1, "/path ", NULL, NULL, NULL, false);
log_info("/* trailing backslash: \\ */");
/* backslash is invalid */
r = config_parse_exec(NULL, "fake", 4, "section", 1,
"LValue", 0, "/path\\",
&c, NULL);
assert_se(r == 0);
assert_se(c1->command_next == NULL);
exec_command_free_list(c);
}