Skip to content

Commit 37ef2fc

Browse files
authored
Merge pull request systemd#18863 from keszybz/cmdline-escaping
Escape command lines properly
2 parents d0f14a6 + 2f960b3 commit 37ef2fc

23 files changed

+633
-327
lines changed

man/coredumpctl.xml

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -354,10 +354,40 @@ Fri … 552351 1000 1000 SIGSEGV present /usr/lib64/firefox/firefox 28.7M
354354
</example>
355355

356356
<example>
357-
<title>Show information about a process that dumped core,
358-
matching by its PID 6654</title>
359-
360-
<programlisting>$ coredumpctl info 6654</programlisting>
357+
<title>Show information about a core dump matched by PID</title>
358+
359+
<programlisting>$ coredumpctl info 6654
360+
PID: 6654 (bash)
361+
UID: 1000 (user)
362+
GID: 1000 (user)
363+
Signal: 11 (SEGV)
364+
Timestamp: Mon 2021-01-01 00:00:01 CET (20s ago)
365+
Command Line: bash -c $'kill -SEGV $$'
366+
Executable: /usr/bin/bash
367+
Control Group: /user.slice/user-1000.slice/…
368+
Unit: user@1000.service
369+
User Unit: vte-spawn-….scope
370+
Slice: user-1000.slice
371+
Owner UID: 1000 (user)
372+
Boot ID: …
373+
Machine ID: …
374+
Hostname: …
375+
Storage: /var/lib/systemd/coredump/core.bash.1000.….zst (present)
376+
Disk Size: 51.7K
377+
Message: Process 130414 (bash) of user 1000 dumped core.
378+
379+
Stack trace of thread 130414:
380+
#0 0x00007f398142358b kill (libc.so.6 + 0x3d58b)
381+
#1 0x0000558c2c7fda09 kill_builtin (bash + 0xb1a09)
382+
#2 0x0000558c2c79dc59 execute_builtin.lto_priv.0 (bash + 0x51c59)
383+
#3 0x0000558c2c79709c execute_simple_command (bash + 0x4b09c)
384+
#4 0x0000558c2c798408 execute_command_internal (bash + 0x4c408)
385+
#5 0x0000558c2c7f6bdc parse_and_execute (bash + 0xaabdc)
386+
#6 0x0000558c2c85415c run_one_command.isra.0 (bash + 0x10815c)
387+
#7 0x0000558c2c77d040 main (bash + 0x31040)
388+
#8 0x00007f398140db75 __libc_start_main (libc.so.6 + 0x27b75)
389+
#9 0x0000558c2c77dd1e _start (bash + 0x31d1e)
390+
</programlisting>
361391
</example>
362392

363393
<example>

src/basic/escape.c

Lines changed: 53 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -360,15 +360,16 @@ int cunescape_length_with_prefix(const char *s, size_t length, const char *prefi
360360
return t - r;
361361
}
362362

363-
char* xescape_full(const char *s, const char *bad, size_t console_width, bool eight_bits) {
363+
char* xescape_full(const char *s, const char *bad, size_t console_width, XEscapeFlags flags) {
364364
char *ans, *t, *prev, *prev2;
365365
const char *f;
366366

367367
/* Escapes all chars in bad, in addition to \ and all special chars, in \xFF style escaping. May be
368-
* reversed with cunescape(). If eight_bits is true, characters >= 127 are let through unchanged.
369-
* This corresponds to non-ASCII printable characters in pre-unicode encodings.
368+
* reversed with cunescape(). If XESCAPE_8_BIT is specified, characters >= 127 are let through
369+
* unchanged. This corresponds to non-ASCII printable characters in pre-unicode encodings.
370370
*
371-
* If console_width is reached, output is truncated and "..." is appended. */
371+
* If console_width is reached, or XESCAPE_FORCE_ELLIPSIS is set, output is truncated and "..." is
372+
* appended. */
372373

373374
if (console_width == 0)
374375
return strdup("");
@@ -380,25 +381,31 @@ char* xescape_full(const char *s, const char *bad, size_t console_width, bool ei
380381
memset(ans, '_', MIN(strlen(s), console_width) * 4);
381382
ans[MIN(strlen(s), console_width) * 4] = 0;
382383

384+
bool force_ellipsis = FLAGS_SET(flags, XESCAPE_FORCE_ELLIPSIS);
385+
383386
for (f = s, t = prev = prev2 = ans; ; f++) {
384387
char *tmp_t = t;
385388

386389
if (!*f) {
390+
if (force_ellipsis)
391+
break;
392+
387393
*t = 0;
388394
return ans;
389395
}
390396

391-
if ((unsigned char) *f < ' ' || (!eight_bits && (unsigned char) *f >= 127) ||
397+
if ((unsigned char) *f < ' ' ||
398+
(!FLAGS_SET(flags, XESCAPE_8_BIT) && (unsigned char) *f >= 127) ||
392399
*f == '\\' || strchr(bad, *f)) {
393-
if ((size_t) (t - ans) + 4 > console_width)
400+
if ((size_t) (t - ans) + 4 + 3 * force_ellipsis > console_width)
394401
break;
395402

396403
*(t++) = '\\';
397404
*(t++) = 'x';
398405
*(t++) = hexchar(*f >> 4);
399406
*(t++) = hexchar(*f);
400407
} else {
401-
if ((size_t) (t - ans) + 1 > console_width)
408+
if ((size_t) (t - ans) + 1 + 3 * force_ellipsis > console_width)
402409
break;
403410

404411
*(t++) = *f;
@@ -427,11 +434,13 @@ char* xescape_full(const char *s, const char *bad, size_t console_width, bool ei
427434
return ans;
428435
}
429436

430-
char* escape_non_printable_full(const char *str, size_t console_width, bool eight_bit) {
431-
if (eight_bit)
432-
return xescape_full(str, "", console_width, true);
437+
char* escape_non_printable_full(const char *str, size_t console_width, XEscapeFlags flags) {
438+
if (FLAGS_SET(flags, XESCAPE_8_BIT))
439+
return xescape_full(str, "", console_width, flags);
433440
else
434-
return utf8_escape_non_printable_full(str, console_width);
441+
return utf8_escape_non_printable_full(str,
442+
console_width,
443+
FLAGS_SET(flags, XESCAPE_FORCE_ELLIPSIS));
435444
}
436445

437446
char* octescape(const char *s, size_t len) {
@@ -462,88 +471,74 @@ char* octescape(const char *s, size_t len) {
462471

463472
}
464473

465-
static char* strcpy_backslash_escaped(char *t, const char *s, const char *bad, bool escape_tab_nl) {
474+
static char* strcpy_backslash_escaped(char *t, const char *s, const char *bad) {
466475
assert(bad);
467476

468-
for (; *s; s++) {
469-
if (escape_tab_nl && IN_SET(*s, '\n', '\t')) {
470-
*(t++) = '\\';
471-
*(t++) = *s == '\n' ? 'n' : 't';
472-
continue;
477+
for (; *s; s++)
478+
if (char_is_cc(*s))
479+
t += cescape_char(*s, t);
480+
else {
481+
if (*s == '\\' || strchr(bad, *s))
482+
*(t++) = '\\';
483+
*(t++) = *s;
473484
}
474485

475-
if (*s == '\\' || strchr(bad, *s))
476-
*(t++) = '\\';
477-
478-
*(t++) = *s;
479-
}
480-
481486
return t;
482487
}
483488

484489
char* shell_escape(const char *s, const char *bad) {
485-
char *r, *t;
490+
char *buf, *t;
486491

487-
r = new(char, strlen(s)*2+1);
488-
if (!r)
492+
buf = new(char, strlen(s)*4+1);
493+
if (!buf)
489494
return NULL;
490495

491-
t = strcpy_backslash_escaped(r, s, bad, false);
496+
t = strcpy_backslash_escaped(buf, s, bad);
492497
*t = 0;
493498

494-
return r;
499+
return buf;
495500
}
496501

497-
char* shell_maybe_quote(const char *s, EscapeStyle style) {
502+
char* shell_maybe_quote(const char *s, ShellEscapeFlags flags) {
498503
const char *p;
499-
char *r, *t;
504+
char *buf, *t;
500505

501506
assert(s);
502507

503-
/* Encloses a string in quotes if necessary to make it OK as a shell
504-
* string. Note that we treat benign UTF-8 characters as needing
505-
* escaping too, but that should be OK. */
508+
/* Encloses a string in quotes if necessary to make it OK as a shell string. */
509+
510+
if (FLAGS_SET(flags, SHELL_ESCAPE_EMPTY) && isempty(s))
511+
return strdup("\"\""); /* We don't use $'' here in the POSIX mode. "" is fine too. */
506512

507513
for (p = s; *p; p++)
508-
if (*p <= ' ' ||
509-
*p >= 127 ||
510-
strchr(SHELL_NEED_QUOTES, *p))
514+
if (char_is_cc(*p) ||
515+
strchr(WHITESPACE SHELL_NEED_QUOTES, *p))
511516
break;
512517

513518
if (!*p)
514519
return strdup(s);
515520

516-
r = new(char, (style == ESCAPE_POSIX) + 1 + strlen(s)*2 + 1 + 1);
517-
if (!r)
521+
buf = new(char, FLAGS_SET(flags, SHELL_ESCAPE_POSIX) + 1 + strlen(s)*4 + 1 + 1);
522+
if (!buf)
518523
return NULL;
519524

520-
t = r;
521-
switch (style) {
522-
case ESCAPE_BACKSLASH:
523-
case ESCAPE_BACKSLASH_ONELINE:
524-
*(t++) = '"';
525-
break;
526-
case ESCAPE_POSIX:
525+
t = buf;
526+
if (FLAGS_SET(flags, SHELL_ESCAPE_POSIX)) {
527527
*(t++) = '$';
528528
*(t++) = '\'';
529-
break;
530-
default:
531-
assert_not_reached("Bad EscapeStyle");
532-
}
529+
} else
530+
*(t++) = '"';
533531

534532
t = mempcpy(t, s, p - s);
535533

536-
if (IN_SET(style, ESCAPE_BACKSLASH, ESCAPE_BACKSLASH_ONELINE))
537-
t = strcpy_backslash_escaped(t, p, SHELL_NEED_ESCAPE,
538-
style == ESCAPE_BACKSLASH_ONELINE);
539-
else
540-
t = strcpy_backslash_escaped(t, p, SHELL_NEED_ESCAPE_POSIX, true);
534+
t = strcpy_backslash_escaped(t, p,
535+
FLAGS_SET(flags, SHELL_ESCAPE_POSIX) ? SHELL_NEED_ESCAPE_POSIX : SHELL_NEED_ESCAPE);
541536

542-
if (IN_SET(style, ESCAPE_BACKSLASH, ESCAPE_BACKSLASH_ONELINE))
543-
*(t++) = '"';
544-
else
537+
if (FLAGS_SET(flags, SHELL_ESCAPE_POSIX))
545538
*(t++) = '\'';
539+
else
540+
*(t++) = '"';
546541
*t = 0;
547542

548-
return r;
543+
return str_realloc(buf);
549544
}

src/basic/escape.h

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,13 @@ typedef enum UnescapeFlags {
3333
UNESCAPE_ACCEPT_NUL = 1 << 1,
3434
} UnescapeFlags;
3535

36-
typedef enum EscapeStyle {
37-
ESCAPE_BACKSLASH = 1, /* Add shell quotes ("") so the shell will consider this a single
38-
argument, possibly multiline. Tabs and newlines are not escaped. */
39-
ESCAPE_BACKSLASH_ONELINE = 2, /* Similar to ESCAPE_BACKSLASH, but always produces a single-line
40-
string instead. Shell escape sequences are produced for tabs and
41-
newlines. */
42-
ESCAPE_POSIX = 3, /* Similar to ESCAPE_BACKSLASH_ONELINE, but uses POSIX shell escape
43-
* syntax (a string enclosed in $'') instead of plain quotes. */
44-
} EscapeStyle;
36+
typedef enum ShellEscapeFlags {
37+
/* The default is to add shell quotes ("") so the shell will consider this a single argument.
38+
* Tabs and newlines are escaped. */
39+
40+
SHELL_ESCAPE_POSIX = 1 << 1, /* Use POSIX shell escape syntax (a string enclosed in $'') instead of plain quotes. */
41+
SHELL_ESCAPE_EMPTY = 1 << 2, /* Format empty arguments as "". */
42+
} ShellEscapeFlags;
4543

4644
char* cescape(const char *s);
4745
char* cescape_length(const char *s, size_t n);
@@ -56,12 +54,17 @@ static inline int cunescape(const char *s, UnescapeFlags flags, char **ret) {
5654
}
5755
int cunescape_one(const char *p, size_t length, char32_t *ret, bool *eight_bit, bool accept_nul);
5856

59-
char* xescape_full(const char *s, const char *bad, size_t console_width, bool eight_bits);
57+
typedef enum XEscapeFlags {
58+
XESCAPE_8_BIT = 1 << 0,
59+
XESCAPE_FORCE_ELLIPSIS = 1 << 1,
60+
} XEscapeFlags;
61+
62+
char* xescape_full(const char *s, const char *bad, size_t console_width, XEscapeFlags flags);
6063
static inline char* xescape(const char *s, const char *bad) {
61-
return xescape_full(s, bad, SIZE_MAX, false);
64+
return xescape_full(s, bad, SIZE_MAX, 0);
6265
}
6366
char* octescape(const char *s, size_t len);
64-
char* escape_non_printable_full(const char *str, size_t console_width, bool eight_bit);
67+
char* escape_non_printable_full(const char *str, size_t console_width, XEscapeFlags flags);
6568

6669
char* shell_escape(const char *s, const char *bad);
67-
char* shell_maybe_quote(const char *s, EscapeStyle style);
70+
char* shell_maybe_quote(const char *s, ShellEscapeFlags flags);

src/basic/fileio.c

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -364,32 +364,40 @@ int verify_file(const char *fn, const char *blob, bool accept_extra_nl) {
364364
return 1;
365365
}
366366

367-
int read_full_virtual_file(const char *filename, char **ret_contents, size_t *ret_size) {
367+
int read_virtual_file(const char *filename, size_t max_size, char **ret_contents, size_t *ret_size) {
368368
_cleanup_free_ char *buf = NULL;
369369
_cleanup_close_ int fd = -1;
370-
struct stat st;
371370
size_t n, size;
372371
int n_retries;
372+
bool truncated = false;
373373

374374
assert(ret_contents);
375375

376-
/* Virtual filesystems such as sysfs or procfs use kernfs, and kernfs can work
377-
* with two sorts of virtual files. One sort uses "seq_file", and the results of
378-
* the first read are buffered for the second read. The other sort uses "raw"
379-
* reads which always go direct to the device. In the latter case, the content of
380-
* the virtual file must be retrieved with a single read otherwise a second read
381-
* might get the new value instead of finding EOF immediately. That's the reason
382-
* why the usage of fread(3) is prohibited in this case as it always performs a
383-
* second call to read(2) looking for EOF. See issue 13585. */
376+
/* Virtual filesystems such as sysfs or procfs use kernfs, and kernfs can work with two sorts of
377+
* virtual files. One sort uses "seq_file", and the results of the first read are buffered for the
378+
* second read. The other sort uses "raw" reads which always go direct to the device. In the latter
379+
* case, the content of the virtual file must be retrieved with a single read otherwise a second read
380+
* might get the new value instead of finding EOF immediately. That's the reason why the usage of
381+
* fread(3) is prohibited in this case as it always performs a second call to read(2) looking for
382+
* EOF. See issue #13585.
383+
*
384+
* max_size specifies a limit on the bytes read. If max_size is SIZE_MAX, the full file is read. If
385+
* the the full file is too large to read, an error is returned. For other values of max_size,
386+
* *partial contents* may be returned. (Though the read is still done using one syscall.)
387+
* Returns 0 on partial success, 1 if untruncated contents were read. */
384388

385389
fd = open(filename, O_RDONLY|O_CLOEXEC);
386390
if (fd < 0)
387391
return -errno;
388392

393+
assert(max_size <= READ_FULL_BYTES_MAX || max_size == SIZE_MAX);
394+
389395
/* Limit the number of attempts to read the number of bytes returned by fstat(). */
390396
n_retries = 3;
391397

392398
for (;;) {
399+
struct stat st;
400+
393401
if (fstat(fd, &st) < 0)
394402
return -errno;
395403

@@ -399,21 +407,24 @@ int read_full_virtual_file(const char *filename, char **ret_contents, size_t *re
399407
/* Be prepared for files from /proc which generally report a file size of 0. */
400408
assert_cc(READ_FULL_BYTES_MAX < SSIZE_MAX);
401409
if (st.st_size > 0) {
402-
if (st.st_size > READ_FULL_BYTES_MAX)
410+
if (st.st_size > SSIZE_MAX) /* Avoid overflow with 32-bit size_t and 64-bit off_t. */
411+
return -EFBIG;
412+
413+
size = MIN((size_t) st.st_size, max_size);
414+
if (size > READ_FULL_BYTES_MAX)
403415
return -EFBIG;
404416

405-
size = st.st_size;
406417
n_retries--;
407418
} else {
408-
size = READ_FULL_BYTES_MAX;
419+
size = MIN(READ_FULL_BYTES_MAX, max_size);
409420
n_retries = 0;
410421
}
411422

412423
buf = malloc(size + 1);
413424
if (!buf)
414425
return -ENOMEM;
415426
/* Use a bigger allocation if we got it anyway, but not more than the limit. */
416-
size = MIN(malloc_usable_size(buf) - 1, READ_FULL_BYTES_MAX);
427+
size = MIN3(malloc_usable_size(buf) - 1, max_size, READ_FULL_BYTES_MAX);
417428

418429
for (;;) {
419430
ssize_t k;
@@ -440,8 +451,15 @@ int read_full_virtual_file(const char *filename, char **ret_contents, size_t *re
440451
* processing, let's try again either with a bigger guessed size or the new
441452
* file size. */
442453

443-
if (n_retries <= 0)
444-
return st.st_size > 0 ? -EIO : -EFBIG;
454+
if (n_retries <= 0) {
455+
if (max_size == SIZE_MAX)
456+
return st.st_size > 0 ? -EIO : -EFBIG;
457+
458+
/* Accept a short read, but truncate it appropropriately. */
459+
n = MIN(n, max_size);
460+
truncated = true;
461+
break;
462+
}
445463

446464
if (lseek(fd, 0, SEEK_SET) < 0)
447465
return -errno;
@@ -470,7 +488,7 @@ int read_full_virtual_file(const char *filename, char **ret_contents, size_t *re
470488
buf[n] = 0;
471489
*ret_contents = TAKE_PTR(buf);
472490

473-
return 0;
491+
return !truncated;
474492
}
475493

476494
int read_full_stream_full(

0 commit comments

Comments
 (0)