diff options
-rw-r--r-- | Makefile | 3 | ||||
-rw-r--r-- | host/lib/include/subprocess.h | 108 | ||||
-rw-r--r-- | host/lib/subprocess.c | 239 | ||||
-rw-r--r-- | tests/subprocess_tests.c | 185 |
4 files changed, 535 insertions, 0 deletions
@@ -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; +} |