summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJack Rosenthal <jrosenth@chromium.org>2019-12-06 09:48:59 -0700
committerCommit Bot <commit-bot@chromium.org>2019-12-07 08:51:30 +0000
commit1ba6d1f87c5d1ae85ef42a3fae30220892ca9c9f (patch)
tree9640baafe4f7e264cb685e9d726292b3128014f7
parenta4f3615472dd64f41035897fa7adef4d870c8049 (diff)
downloadvboot-1ba6d1f87c5d1ae85ef42a3fae30220892ca9c9f.tar.gz
host/lib: add a subprocess library
This is a powerful library for interacting with processes. We'll be able to clean up much of the code which manually sets up the pipes and calls exec* with this well-tested and expressive abstraction. This code will initially be used in crossystem for calling out to flashrom instead of relying on mosys. BUG=chromium:1030473 BRANCH=none TEST=provided unit tests Change-Id: I56f28419406d0b1299bb91058dd4500079b2435e Signed-off-by: Jack Rosenthal <jrosenth@chromium.org> Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/vboot_reference/+/1955805 Reviewed-by: Julius Werner <jwerner@chromium.org> Reviewed-by: Joel Kitching <kitching@chromium.org>
-rw-r--r--Makefile3
-rw-r--r--host/lib/include/subprocess.h108
-rw-r--r--host/lib/subprocess.c239
-rw-r--r--tests/subprocess_tests.c185
4 files changed, 535 insertions, 0 deletions
diff --git a/Makefile b/Makefile
index 7acdfc64..05b94c6b 100644
--- a/Makefile
+++ b/Makefile
@@ -465,6 +465,7 @@ UTILLIB_SRCS = \
host/lib/host_signature.c \
host/lib/host_signature2.c \
host/lib/signature_digest.c \
+ host/lib/subprocess.c \
host/lib/util_misc.c \
host/lib21/host_fw_preamble.c \
host/lib21/host_key.c \
@@ -687,6 +688,7 @@ TEST_OBJS += ${TESTLIB_OBJS}
TEST_NAMES = \
tests/cgptlib_test \
tests/sha_benchmark \
+ tests/subprocess_tests \
tests/utility_string_tests \
tests/vboot_api_devmode_tests \
tests/vboot_api_kernel2_tests \
@@ -1277,6 +1279,7 @@ ifeq (${TPM2_MODE},)
${RUNTEST} ${BUILD_RUN}/tests/tlcl_tests
endif
endif
+ ${RUNTEST} ${BUILD_RUN}/tests/subprocess_tests
${RUNTEST} ${BUILD_RUN}/tests/utility_string_tests
${RUNTEST} ${BUILD_RUN}/tests/vboot_api_devmode_tests
${RUNTEST} ${BUILD_RUN}/tests/vboot_api_kernel2_tests
diff --git a/host/lib/include/subprocess.h b/host/lib/include/subprocess.h
new file mode 100644
index 00000000..cddcf05c
--- /dev/null
+++ b/host/lib/include/subprocess.h
@@ -0,0 +1,108 @@
+/* Copyright 2019 The Chromium OS Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+/* Library for creating subprocesses in a high level manner. */
+
+#ifndef VBOOT_REFERENCE_SUBPROCESS_H_
+#define VBOOT_REFERENCE_SUBPROCESS_H_
+
+#include <stdio.h>
+#include <stdlib.h>
+
+/**
+ * subprocess_target is the "mini language" of the subprocess
+ * library. It describes where to read or write data from the process.
+ *
+ * There are currently five target of targets:
+ *
+ * - TARGET_NULL: /dev/null, no need to describe any other fields.
+ *
+ * - TARGET_FD: file descriptor, put the fd in the fd field.
+ *
+ * - TARGET_FILE: FILE *, put the FILE pointer in the file field.
+ *
+ * - TARGET_BUFFER: read to, or write from, a buffer. Fields:
+ * - buffer->buf: the buffer
+ * - buffer->size: the size of that buffer
+ * - buffer->bytes_consumed: do not fill out this field.
+ * subprocess_run will set it to the number of bytes read from the
+ * process (if writing to a buffer). Goes unused when reading from
+ * a buffer.
+ *
+ * - TARGET_BUFFER_NULL_TERMINATED: when reading from a buffer, don't
+ * fill out the size field and subprocess_run will strlen for you.
+ * When writing to a buffer, subprocess_run will reserve one byte of
+ * the size for a null terminator and guarantee that the output is
+ * always NULL terminated.
+ */
+struct subprocess_target {
+ enum {
+ TARGET_NULL,
+ TARGET_FD,
+ TARGET_FILE,
+ TARGET_BUFFER,
+ TARGET_BUFFER_NULL_TERMINATED,
+ } type;
+ union {
+ int fd;
+ FILE *file;
+ struct {
+ char *buf;
+ size_t size;
+
+ /* This variable is used internally by "run" and
+ * shouldn't be operated on by the caller.
+ */
+ int _pipefd[2];
+
+ /* This variable is the output of the number of bytes
+ * read or written. It should be read by the caller, not
+ * set.
+ */
+ size_t bytes_consumed;
+ } buffer;
+ };
+};
+
+/**
+ * A convenience subprocess target which uses TARGET_NULL.
+ */
+struct subprocess_target subprocess_null;
+
+/**
+ * A convenience subprocess target which uses TARGET_FD to
+ * STDIN_FILENO.
+ */
+struct subprocess_target subprocess_stdin;
+
+/**
+ * A convenience subprocess target which uses TARGET_FD to
+ * STDOUT_FILENO.
+ */
+struct subprocess_target subprocess_stdout;
+
+/**
+ * A convenience subprocess target which uses TARGET_FD to
+ * STDERR_FILENO.
+ */
+struct subprocess_target subprocess_stderr;
+
+/**
+ * Call a process described by argv and run until completion. Provide
+ * input from the subprocess target input, output to the subprocess
+ * target output, and error to the subprocess target error.
+ *
+ * If either input, output, or error are set to NULL, the will be
+ * &subprocess_stdin, &subprocess_stdout, or &subprocess_stderr
+ * respectively.
+ *
+ * Returns the exit status on success, or negative values on error.
+ */
+int subprocess_run(const char *const argv[],
+ struct subprocess_target *input,
+ struct subprocess_target *output,
+ struct subprocess_target *error);
+
+#endif /* VBOOT_REFERENCE_SUBPROCESS_H_ */
diff --git a/host/lib/subprocess.c b/host/lib/subprocess.c
new file mode 100644
index 00000000..5721a576
--- /dev/null
+++ b/host/lib/subprocess.c
@@ -0,0 +1,239 @@
+/* Copyright 2019 The Chromium OS Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include <fcntl.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "subprocess.h"
+
+static char *program_name;
+
+__attribute__((constructor, used))
+static int libinit(int argc, char **argv)
+{
+ program_name = *argv;
+ return 0;
+}
+
+static int init_target_private(struct subprocess_target *target)
+{
+ switch (target->type) {
+ case TARGET_BUFFER:
+ case TARGET_BUFFER_NULL_TERMINATED:
+ return pipe(target->buffer._pipefd);
+ default:
+ return 0;
+ }
+}
+
+static int flags_for_fd(int fd)
+{
+ switch (fd) {
+ case STDIN_FILENO:
+ return O_RDONLY;
+ case STDOUT_FILENO:
+ case STDERR_FILENO:
+ return O_WRONLY;
+ default:
+ return -1;
+ }
+}
+
+static int connect_process_target(struct subprocess_target *target, int fd)
+{
+ int target_fd;
+
+ switch (target->type) {
+ case TARGET_NULL:
+ target_fd = open("/dev/null", flags_for_fd(fd));
+ break;
+ case TARGET_FD:
+ target_fd = target->fd;
+ break;
+ case TARGET_FILE:
+ target_fd = fileno(target->file);
+ break;
+ case TARGET_BUFFER:
+ case TARGET_BUFFER_NULL_TERMINATED:
+ switch (fd) {
+ case STDIN_FILENO:
+ target_fd = target->buffer._pipefd[0];
+ close(target->buffer._pipefd[1]);
+ break;
+ case STDOUT_FILENO:
+ case STDERR_FILENO:
+ target_fd = target->buffer._pipefd[1];
+ close(target->buffer._pipefd[0]);
+ break;
+ default:
+ return -1;
+ }
+ break;
+ }
+
+ return dup2(target_fd, fd);
+}
+
+static int process_target_input(struct subprocess_target *target)
+{
+ int rv = 0;
+ ssize_t write_rv;
+ size_t bytes_to_write;
+ char *buf;
+
+ switch (target->type) {
+ case TARGET_BUFFER:
+ bytes_to_write = target->buffer.size;
+ break;
+ case TARGET_BUFFER_NULL_TERMINATED:
+ bytes_to_write = strlen(target->buffer.buf);
+ break;
+ default:
+ return 0;
+ }
+
+ close(target->buffer._pipefd[0]);
+ buf = target->buffer.buf;
+ while (bytes_to_write) {
+ write_rv =
+ write(target->buffer._pipefd[1], buf, bytes_to_write);
+ if (write_rv <= 0) {
+ rv = -1;
+ goto cleanup;
+ }
+ buf += write_rv;
+ bytes_to_write -= write_rv;
+ }
+
+cleanup:
+ close(target->buffer._pipefd[1]);
+ return rv;
+}
+
+static int process_target_output(struct subprocess_target *target)
+{
+ int rv = 0;
+ ssize_t read_rv;
+ size_t bytes_remaining;
+
+ switch (target->type) {
+ case TARGET_BUFFER:
+ bytes_remaining = target->buffer.size;
+ break;
+ case TARGET_BUFFER_NULL_TERMINATED:
+ if (target->buffer.size == 0)
+ return -1;
+ bytes_remaining = target->buffer.size - 1;
+ break;
+ default:
+ return 0;
+ }
+
+ close(target->buffer._pipefd[1]);
+ target->buffer.bytes_consumed = 0;
+ while (bytes_remaining) {
+ read_rv = read(
+ target->buffer._pipefd[0],
+ target->buffer.buf + target->buffer.bytes_consumed,
+ bytes_remaining);
+ if (read_rv < 0) {
+ rv = -1;
+ goto cleanup;
+ }
+ if (read_rv == 0)
+ break;
+ target->buffer.bytes_consumed += read_rv;
+ bytes_remaining -= read_rv;
+ }
+
+ if (target->type == TARGET_BUFFER_NULL_TERMINATED)
+ target->buffer.buf[target->buffer.bytes_consumed] = '\0';
+
+cleanup:
+ close(target->buffer._pipefd[0]);
+ return rv;
+}
+
+struct subprocess_target subprocess_null = {
+ .type = TARGET_NULL,
+};
+
+struct subprocess_target subprocess_stdin = {
+ .type = TARGET_FD,
+ .fd = STDIN_FILENO,
+};
+
+struct subprocess_target subprocess_stdout = {
+ .type = TARGET_FD,
+ .fd = STDOUT_FILENO,
+};
+
+struct subprocess_target subprocess_stderr = {
+ .type = TARGET_FD,
+ .fd = STDERR_FILENO,
+};
+
+int subprocess_run(const char *const argv[],
+ struct subprocess_target *input,
+ struct subprocess_target *output,
+ struct subprocess_target *error)
+{
+ int status;
+ pid_t pid = -1;
+
+ if (!input)
+ input = &subprocess_stdin;
+ if (!output)
+ output = &subprocess_stdout;
+ if (!error)
+ error = &subprocess_stderr;
+
+ if (init_target_private(input) < 0)
+ goto fail;
+ if (init_target_private(output) < 0)
+ goto fail;
+ if (init_target_private(error) < 0)
+ goto fail;
+
+ if ((pid = fork()) < 0)
+ goto fail;
+ if (pid == 0) {
+ /* Child process */
+ if (connect_process_target(input, STDIN_FILENO) < 0)
+ goto fail;
+ if (connect_process_target(output, STDOUT_FILENO) < 0)
+ goto fail;
+ if (connect_process_target(error, STDERR_FILENO) < 0)
+ goto fail;
+ execvp(*argv, (char *const *)argv);
+ goto fail;
+ }
+
+ /* Parent process */
+ if (process_target_input(input) < 0)
+ goto fail;
+ if (process_target_output(output) < 0)
+ goto fail;
+ if (process_target_output(error) < 0)
+ goto fail;
+
+ if (waitpid(pid, &status, 0) < 0)
+ goto fail;
+
+ if (WIFEXITED(status))
+ return WEXITSTATUS(status);
+
+fail:
+ if (program_name)
+ perror(program_name);
+ else
+ perror("subprocess");
+ if (pid == 0)
+ exit(127);
+ return -1;
+}
diff --git a/tests/subprocess_tests.c b/tests/subprocess_tests.c
new file mode 100644
index 00000000..138c1019
--- /dev/null
+++ b/tests/subprocess_tests.c
@@ -0,0 +1,185 @@
+/* Copyright 2019 The Chromium OS Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include <stdlib.h>
+#include <string.h>
+
+#include "subprocess.h"
+#include "test_common.h"
+
+#define TEST_STRING "hello world"
+#define TEST_STRING_LN TEST_STRING "\n"
+
+static void test_subprocess_output_to_buffer(void)
+{
+ char output_buffer[__builtin_strlen(TEST_STRING_LN)];
+
+ struct subprocess_target output = {
+ .type = TARGET_BUFFER,
+ .buffer = {
+ .buf = output_buffer,
+ .size = sizeof(output_buffer),
+ },
+ };
+ const char *const argv[] = {
+ "echo", TEST_STRING, NULL
+ };
+
+ TEST_EQ(subprocess_run(argv, &subprocess_null, &output, NULL), 0,
+ "Return value of \"echo 'hello world'\" is 0");
+ TEST_EQ(memcmp(output_buffer, TEST_STRING_LN, sizeof(output_buffer)), 0,
+ "Output is \"hello world\\n\"");
+ TEST_EQ(output.buffer.bytes_consumed, sizeof(output_buffer),
+ "The entire output buffer should have been used.");
+}
+
+static void test_subprocess_output_to_buffer_null_terminated(void)
+{
+ char output_buffer[__builtin_strlen(TEST_STRING_LN) + 1];
+
+ struct subprocess_target output = {
+ .type = TARGET_BUFFER_NULL_TERMINATED,
+ .buffer = {
+ .buf = output_buffer,
+ .size = sizeof(output_buffer),
+ },
+ };
+ const char *const argv[] = {
+ "echo", TEST_STRING, NULL
+ };
+
+ TEST_EQ(subprocess_run(argv, &subprocess_null, &output, NULL), 0,
+ "Return value of \"echo 'hello world'\" is 0");
+ TEST_STR_EQ(output_buffer, TEST_STRING_LN,
+ "Output is \"hello world\\n\"");
+ TEST_EQ(output.buffer.bytes_consumed, sizeof(output_buffer) - 1,
+ "The entire output buffer should have been used.");
+}
+
+#define TEST_STRING_2 "hello\0world!"
+
+static void test_subprocess_input_buffer(void)
+{
+ char input_buffer[sizeof(TEST_STRING_2)];
+ char output_buffer[20];
+ char error_buffer[20];
+
+ memcpy(input_buffer, TEST_STRING_2, sizeof(input_buffer));
+
+ struct subprocess_target input = {
+ .type = TARGET_BUFFER,
+ .buffer = {
+ .buf = input_buffer,
+ .size = sizeof(input_buffer),
+ },
+ };
+ struct subprocess_target output = {
+ .type = TARGET_BUFFER_NULL_TERMINATED,
+ .buffer = {
+ .buf = output_buffer,
+ .size = sizeof(output_buffer),
+ },
+ };
+ struct subprocess_target error = {
+ .type = TARGET_BUFFER_NULL_TERMINATED,
+ .buffer = {
+ .buf = error_buffer,
+ .size = sizeof(error_buffer),
+ },
+ };
+ const char *const argv[] = {"cat", NULL};
+
+ TEST_EQ(subprocess_run(argv, &input, &output, &error), 0,
+ "Return value of \"cat\" is 0");
+ TEST_EQ(memcmp(output_buffer, TEST_STRING_2, sizeof(TEST_STRING_2)),
+ 0, "Output is \"hello\\0world!\"");
+ TEST_STR_EQ(error_buffer, "", "No output captured on stderr");
+ TEST_EQ(output.buffer.bytes_consumed, sizeof(TEST_STRING_2),
+ "Bytes consumed is correct");
+ TEST_EQ(error.buffer.bytes_consumed, 0, "No bytes used for error");
+}
+
+static void test_subprocess_input_null_terminated(void)
+{
+ char input_buffer[20];
+ char output_buffer[20];
+ char error_buffer[20];
+
+ memcpy(input_buffer, TEST_STRING_2, sizeof(TEST_STRING_2));
+
+ struct subprocess_target input = {
+ .type = TARGET_BUFFER_NULL_TERMINATED,
+ .buffer = {
+ .buf = input_buffer,
+ },
+ };
+ struct subprocess_target output = {
+ .type = TARGET_BUFFER_NULL_TERMINATED,
+ .buffer = {
+ .buf = output_buffer,
+ .size = sizeof(output_buffer),
+ },
+ };
+ struct subprocess_target error = {
+ .type = TARGET_BUFFER_NULL_TERMINATED,
+ .buffer = {
+ .buf = error_buffer,
+ .size = sizeof(error_buffer),
+ },
+ };
+ const char *const argv[] = {"cat", NULL};
+
+ TEST_EQ(subprocess_run(argv, &input, &output, &error), 0,
+ "Return value of \"cat\" is 0");
+ TEST_STR_EQ(output_buffer, "hello", "Output is \"hello\"");
+ TEST_STR_EQ(error_buffer, "", "No output captured on stderr");
+ TEST_EQ(output.buffer.bytes_consumed, 5, "5 bytes used");
+ TEST_EQ(error.buffer.bytes_consumed, 0, "No bytes used for error");
+}
+
+static void test_subprocess_small_output_buffer(void)
+{
+ char output_buffer[3];
+
+ struct subprocess_target output = {
+ .type = TARGET_BUFFER_NULL_TERMINATED,
+ .buffer = {
+ .buf = output_buffer,
+ .size = sizeof(output_buffer),
+ },
+ };
+ const char *const argv[] = {
+ "echo", TEST_STRING, NULL
+ };
+
+ TEST_EQ(subprocess_run(argv, &subprocess_null, &output, NULL), 0,
+ "Return value of \"echo 'hello world'\" is 0");
+ TEST_STR_EQ(output_buffer, "he",
+ "Output is \"he\" (truncated to small buffer)");
+ TEST_EQ(output.buffer.bytes_consumed, sizeof(output_buffer) - 1,
+ "The entire output buffer should have been used.");
+}
+
+static void test_subprocess_return_code_failure(void)
+{
+ const char *const argv[] = {"false"};
+
+ TEST_NEQ(subprocess_run(argv, NULL, NULL, NULL), 0,
+ "Return value of \"false\" is nonzero");
+}
+
+int main(int argc, char *argv[])
+{
+ test_subprocess_output_to_buffer();
+ test_subprocess_output_to_buffer_null_terminated();
+ test_subprocess_input_buffer();
+ test_subprocess_input_null_terminated();
+ test_subprocess_small_output_buffer();
+ test_subprocess_return_code_failure();
+
+ if (!gTestSuccess)
+ return 255;
+ return 0;
+}