]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
basic/process-util: allow quoting of commandlines
authorZbigniew Jędrzejewski-Szmek <zbyszek@in.waw.pl>
Tue, 30 Mar 2021 17:42:36 +0000 (19:42 +0200)
committerZbigniew Jędrzejewski-Szmek <zbyszek@in.waw.pl>
Wed, 5 May 2021 11:59:23 +0000 (13:59 +0200)
Since the new functionality is controlled by an option, this causes no change
in output yet, except tests.

The login in the old branch of !(flags & PROCESS_CMDLINE_QUOTE) is essentially
unmodified. But there is an important difference in behaviour: instead of
unconditionally reading the whole virtual file, we now read only 'max_columns'
bytes. This makes out code to write process lists quite a bit more efficient
when there are processes with long command lines.

src/basic/fileio.c
src/basic/process-util.c
src/basic/process-util.h
src/test/test-process-util.c

index 90484a98c2ac4390659e2f67416338bf28e39e35..024eb29bb9f944f94a6f1c6a06e28c53b0f0b662 100644 (file)
@@ -368,6 +368,7 @@ int read_virtual_file(const char *filename, size_t max_size, char **ret_contents
         _cleanup_close_ int fd = -1;
         size_t n, size;
         int n_retries;
+        bool truncated = false;
 
         assert(ret_contents);
 
@@ -381,7 +382,8 @@ int read_virtual_file(const char *filename, size_t max_size, char **ret_contents
          *
          * max_size specifies a limit on the bytes read. If max_size is SIZE_MAX, the full file is read. If
          * the the full file is too large to read, an error is returned. For other values of max_size,
-         * *partial contents* may be returned. (Though the read is still done using one syscall.) */
+         * *partial contents* may be returned. (Though the read is still done using one syscall.)
+         * Returns 0 on partial success, 1 if untruncated contents were read. */
 
         fd = open(filename, O_RDONLY|O_CLOEXEC);
         if (fd < 0)
@@ -454,6 +456,7 @@ int read_virtual_file(const char *filename, size_t max_size, char **ret_contents
 
                         /* Accept a short read, but truncate it appropropriately. */
                         n = MIN(n, max_size);
+                        truncated = true;
                         break;
                 }
 
@@ -484,7 +487,7 @@ int read_virtual_file(const char *filename, size_t max_size, char **ret_contents
         buf[n] = 0;
         *ret_contents = TAKE_PTR(buf);
 
-        return 0;
+        return !truncated;
 }
 
 int read_full_stream_full(
index d7afc4fe5aa5b0bcf5ce576717d3d11cda66aad8..fd708eed98a90f3f43bd8152b756a72b8b7fa4c3 100644 (file)
@@ -123,64 +123,133 @@ int get_process_comm(pid_t pid, char **ret) {
         return 0;
 }
 
-int get_process_cmdline(pid_t pid, size_t max_columns, ProcessCmdlineFlags flags, char **line) {
-        _cleanup_free_ char *t = NULL, *ans = NULL;
+static int get_process_cmdline_nulstr(
+                pid_t pid,
+                size_t max_size,
+                ProcessCmdlineFlags flags,
+                char **ret,
+                size_t *ret_size) {
+
         const char *p;
+        char *t;
         size_t k;
         int r;
 
-        assert(line);
-        assert(pid >= 0);
-
-        /* Retrieves a process' command line. Replaces non-utf8 bytes by replacement character (�). If
-         * max_columns is != -1 will return a string of the specified console width at most, abbreviated with
-         * an ellipsis. If PROCESS_CMDLINE_COMM_FALLBACK is specified in flags and the process has no command
-         * line set (the case for kernel threads), or has a command line that resolves to the empty string
-         * will return the "comm" name of the process instead. This will use at most _SC_ARG_MAX bytes of
-         * input data.
+        /* Retrieves a process' command line as a "sized nulstr", i.e. possibly without the last NUL, but
+         * with a specified size.
          *
-         * Returns -ESRCH if the process doesn't exist, and -ENOENT if the process has no command line (and
-         * comm_fallback is false). Returns 0 and sets *line otherwise. */
+         * If PROCESS_CMDLINE_COMM_FALLBACK is specified in flags and the process has no command line set
+         * (the case for kernel threads), or has a command line that resolves to the empty string, will
+         * return the "comm" name of the process instead. This will use at most _SC_ARG_MAX bytes of input
+         * data.
+         *
+         * Returns an error, 0 if output was read but is truncated, 1 otherwise.
+         */
 
         p = procfs_file_alloca(pid, "cmdline");
-        r = read_full_virtual_file(p, &t, &k);
+        r = read_virtual_file(p, max_size, &t, &k); /* Let's assume that each input byte results in >= 1
+                                                     * columns of output. We ignore zero-width codepoints. */
         if (r == -ENOENT)
                 return -ESRCH;
         if (r < 0)
                 return r;
 
-        if (k > 0) {
-                /* Arguments are separated by NULs. Let's replace those with spaces. */
-                for (size_t i = 0; i < k - 1; i++)
-                        if (t[i] == '\0')
-                                t[i] = ' ';
-        } else {
+        if (k == 0) {
+                t = mfree(t);
+
                 if (!(flags & PROCESS_CMDLINE_COMM_FALLBACK))
                         return -ENOENT;
 
                 /* Kernel threads have no argv[] */
-                _cleanup_free_ char *t2 = NULL;
+                _cleanup_free_ char *comm = NULL;
 
-                r = get_process_comm(pid, &t2);
+                r = get_process_comm(pid, &comm);
                 if (r < 0)
                         return r;
 
-                free(t);
-                t = strjoin("[", t2, "]");
+                t = strjoin("[", comm, "]");
                 if (!t)
                         return -ENOMEM;
+
+                k = strlen(t);
+                r = k <= max_size;
+                if (r == 0) /* truncation */
+                        t[max_size] = '\0';
         }
 
-        delete_trailing_chars(t, WHITESPACE);
+        *ret = t;
+        *ret_size = k;
+        return r;
+}
 
-        bool eight_bit = (flags & PROCESS_CMDLINE_USE_LOCALE) && !is_locale_utf8();
+int get_process_cmdline(pid_t pid, size_t max_columns, ProcessCmdlineFlags flags, char **line) {
+        _cleanup_free_ char *t = NULL;
+        size_t k;
+        char *ans;
 
-        ans = escape_non_printable_full(t, max_columns, eight_bit * XESCAPE_8_BIT);
-        if (!ans)
-                return -ENOMEM;
+        assert(line);
+        assert(pid >= 0);
+
+        /* Retrieve adn format a commandline. See above for discussion of retrieval options.
+         *
+         * There are two main formatting modes:
+         *
+         * - when PROCESS_CMDLINE_QUOTE is specified, output is quoted in C/Python style. If no shell special
+         *   characters are present, this output can be copy-pasted into the terminal to execute. UTF-8
+         *   output is assumed.
+         *
+         * - otherwise, a compact non-roundtrippable form is returned. Non-UTF8 bytes are replaced by �. The
+         *   returned string is of the specified console width at most, abbreviated with an ellipsis.
+         *
+         * Returns -ESRCH if the process doesn't exist, and -ENOENT if the process has no command line (and
+         * PROCESS_CMDLINE_COMM_FALLBACK is not specified). Returns 0 and sets *line otherwise. */
+
+        int full = get_process_cmdline_nulstr(pid, max_columns, flags, &t, &k);
+        if (full < 0)
+                return full;
+
+        if (flags & PROCESS_CMDLINE_QUOTE) {
+                assert(!(flags & PROCESS_CMDLINE_USE_LOCALE));
+
+                _cleanup_strv_free_ char **args = NULL;
+
+                args = strv_parse_nulstr(t, k);
+                if (!args)
+                        return -ENOMEM;
+
+                for (size_t i = 0; args[i]; i++) {
+                        char *e;
+
+                        e = shell_maybe_quote(args[i], SHELL_ESCAPE_EMPTY);
+                        if (!e)
+                                return -ENOMEM;
+
+                        free_and_replace(args[i], e);
+                }
+
+                ans = strv_join(args, " ");
+                if (!ans)
+                        return -ENOMEM;
+
+        } else {
+                /* Arguments are separated by NULs. Let's replace those with spaces. */
+                for (size_t i = 0; i < k - 1; i++)
+                        if (t[i] == '\0')
+                                t[i] = ' ';
+
+                delete_trailing_chars(t, WHITESPACE);
+
+                bool eight_bit = (flags & PROCESS_CMDLINE_USE_LOCALE) && !is_locale_utf8();
+
+                ans = escape_non_printable_full(t, max_columns,
+                                                eight_bit * XESCAPE_8_BIT | !full * XESCAPE_FORCE_ELLIPSIS);
+                if (!ans)
+                        return -ENOMEM;
+
+                ans = str_realloc(ans);
+        }
 
-        ans = str_realloc(ans);
-        *line = TAKE_PTR(ans);
+        *line = ans;
         return 0;
 }
 
index ddce7bd2722b0b7155caa5f4e49295c5c8861788..3121d82d3ff1d54d7a5e64f8dff73d7d490e42e9 100644 (file)
@@ -35,6 +35,7 @@
 typedef enum ProcessCmdlineFlags {
         PROCESS_CMDLINE_COMM_FALLBACK = 1 << 0,
         PROCESS_CMDLINE_USE_LOCALE    = 1 << 1,
+        PROCESS_CMDLINE_QUOTE         = 1 << 2,
 } ProcessCmdlineFlags;
 
 int get_process_comm(pid_t pid, char **name);
index 7f0a771ba70cc1d722bc501f9c5076a9e118934a..5f148c1522e7308095f1204c1c0faaede65e3c92 100644 (file)
@@ -248,9 +248,15 @@ static void test_get_process_cmdline_harder(void) {
         assert_se(get_process_cmdline(0, SIZE_MAX, 0, &line) == -ENOENT);
 
         assert_se(get_process_cmdline(0, SIZE_MAX, PROCESS_CMDLINE_COMM_FALLBACK, &line) >= 0);
+        log_info("'%s'", line);
         assert_se(streq(line, "[testa]"));
         line = mfree(line);
 
+        assert_se(get_process_cmdline(0, SIZE_MAX, PROCESS_CMDLINE_COMM_FALLBACK | PROCESS_CMDLINE_QUOTE, &line) >= 0);
+        log_info("'%s'", line);
+        assert_se(streq(line, "\"[testa]\"")); /* quoting is enabled here */
+        line = mfree(line);
+
         assert_se(get_process_cmdline(0, 0, PROCESS_CMDLINE_COMM_FALLBACK, &line) >= 0);
         log_info("'%s'", line);
         assert_se(streq(line, ""));
@@ -288,6 +294,8 @@ static void test_get_process_cmdline_harder(void) {
         assert_se(streq(line, "[testa]"));
         line = mfree(line);
 
+        /* Test with multiple arguments that don't require quoting */
+
         assert_se(write(fd, "foo\0bar", 8) == 8);
 
         assert_se(get_process_cmdline(0, SIZE_MAX, 0, &line) >= 0);
@@ -390,6 +398,32 @@ static void test_get_process_cmdline_harder(void) {
         assert_se(streq(line, "[aaaa bbbb …"));
         line = mfree(line);
 
+        /* Test with multiple arguments that do require quoting */
+
+#define CMDLINE1 "foo\0'bar'\0\"bar$\"\0x y z\0!``\0"
+#define EXPECT1  "foo \"'bar'\" \"\\\"bar\\$\\\"\" \"x y z\" \"!\\`\\`\" \"\""
+        assert_se(lseek(fd, SEEK_SET, 0) == 0);
+        assert_se(write(fd, CMDLINE1, sizeof CMDLINE1) == sizeof CMDLINE1);
+        assert_se(ftruncate(fd, sizeof CMDLINE1) == 0);
+
+        assert_se(get_process_cmdline(0, SIZE_MAX, PROCESS_CMDLINE_QUOTE, &line) >= 0);
+        log_info("got: ==%s==", line);
+        log_info("exp: ==%s==", EXPECT1);
+        assert_se(streq(line, EXPECT1));
+        line = mfree(line);
+
+#define CMDLINE2 "foo\0\1\2\3\0\0"
+#define EXPECT2  "foo \"\\001\\002\\003\" \"\" \"\""
+        assert_se(lseek(fd, SEEK_SET, 0) == 0);
+        assert_se(write(fd, CMDLINE2, sizeof CMDLINE2) == sizeof CMDLINE2);
+        assert_se(ftruncate(fd, sizeof CMDLINE2) == 0);
+
+        assert_se(get_process_cmdline(0, SIZE_MAX, PROCESS_CMDLINE_QUOTE, &line) >= 0);
+        log_info("got: ==%s==", line);
+        log_info("exp: ==%s==", EXPECT2);
+        assert_se(streq(line, EXPECT2));
+        line = mfree(line);
+
         safe_close(fd);
         _exit(EXIT_SUCCESS);
 }
@@ -403,6 +437,8 @@ static void test_rename_process_now(const char *p, int ret) {
                   (ret == 0 && r >= 0) ||
                   (ret > 0 && r > 0));
 
+        log_info_errno(r, "rename_process(%s): %m", p);
+
         if (r < 0)
                 return;
 
@@ -425,9 +461,12 @@ static void test_rename_process_now(const char *p, int ret) {
                 if (r == 0 && detect_container() > 0)
                         log_info("cmdline = <%s> (not verified, Running in unprivileged container?)", cmdline);
                 else {
-                        log_info("cmdline = <%s>", cmdline);
-                        assert_se(strneq(p, cmdline, STRLEN("test-process-util")));
-                        assert_se(startswith(p, cmdline));
+                        log_info("cmdline = <%s> (expected <%.*s>)", cmdline, (int) strlen("test-process-util"), p);
+
+                        bool skip = cmdline[0] == '"'; /* A shortcut to check if the string is quoted */
+
+                        assert_se(strneq(cmdline + skip, p, strlen("test-process-util")));
+                        assert_se(startswith(cmdline + skip, p));
                 }
         } else
                 log_info("cmdline = <%s> (not verified)", cmdline);