From dbfd1a6fa944670b6dba4b3e937b6ac61bf06996 Mon Sep 17 00:00:00 2001 From: Gilles Peskine Date: Sun, 25 Jan 2026 22:08:30 +0100 Subject: [PATCH] New test helper mbedtls_test_fork_run_child() Run some code in a child process. Propagate output from the child if the test succeeds, and propagate the test result information otherwise. Signed-off-by: Gilles Peskine --- tests/include/test/fork_helpers.h | 59 ++++++++++ tests/include/test/helpers.h | 19 ++++ tests/src/fork_helpers.c | 182 ++++++++++++++++++++++++++++++ tests/src/helpers.c | 22 ++++ 4 files changed, 282 insertions(+) create mode 100644 tests/include/test/fork_helpers.h create mode 100644 tests/src/fork_helpers.c diff --git a/tests/include/test/fork_helpers.h b/tests/include/test/fork_helpers.h new file mode 100644 index 000000000..4bbb12c88 --- /dev/null +++ b/tests/include/test/fork_helpers.h @@ -0,0 +1,59 @@ +/** Helper functions for testing with subprocesses. + */ +/* + * Copyright The Mbed TLS Contributors + * SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later + */ + +#ifndef TEST_FORK_HELPERS_H +#define TEST_FORK_HELPERS_H + +#include "test/helpers.h" + +/** Type of a function to run in a child process. + * + * The function can mark the test case as failed by calling + * mbedtls_test_fail(). This information will be reported to the parent. + * + * \param param Parameter passed to the callback. + * \param[out] output Buffer for data to pass to the parent. + * This data is ignored if the test case is marked + * as failed. + * \param output_size Size of \p output in bytes. + * \param[out] output_length Number of bytes written to \p output, to be + * passed to the parent. The default is \c 0. + */ +typedef void mbedtls_test_fork_child_callback_t( + void *param, + unsigned char *output, size_t output_size, size_t *output_length); + +/* Fork a child process and wait for it to collect some data. + * + * This is similar to backquotes or `$(...)` in a shell. + * + * This function blocks until the child exits. + * + * If the child marks the test as failed or skipped, the child's test + * information (test result and failure location) is propagated to the + * parent. + * + * \param child_callback Callback function to run in the child. + * \param param Parameter to pass to the callback function. + * \param[out] child_output On success, data retrieved from the child. + * Note that the data is only available if the + * child did not mark the test case as failed + * or skipped. + * \param child_output_size Size of \p child_output in bytes. + * \param[out] child_output_length On success, the number of bytes collected + * from the child in \c child_output. + * + * \return \c 0 on success. + * A nonzero value if the test case is marked as failed or skipped. + */ +int mbedtls_test_fork_run_child( + mbedtls_test_fork_child_callback_t *child_callback, + void *param, + unsigned char *child_output, size_t child_output_size, + size_t *child_output_length); + +#endif /* TEST_FORK_HELPERS_H */ diff --git a/tests/include/test/helpers.h b/tests/include/test/helpers.h index 01c47f3c3..0b21bdc32 100644 --- a/tests/include/test/helpers.h +++ b/tests/include/test/helpers.h @@ -147,6 +147,25 @@ void mbedtls_test_get_line1(char *line); */ void mbedtls_test_get_line2(char *line); +/** + * \brief Get a copy of the test result information. + * + * \param[out] out On output, contains a copy of the current test info. + */ +void mbedtls_test_info_save(mbedtls_test_info_t *out); + +/** + * \brief Overwrite the test result information. + * This is intended for some unusual scenarios. + * You probably shouldn't use this in a test function. + * + * \param[in] replacement + * The test info to use instead of the current one. + * The function copies the data, so the pointer does + * not need to be valid after this function returns. + */ +void mbedtls_test_info_overwrite(const mbedtls_test_info_t *replacement); + #if defined(MBEDTLS_TEST_MUTEX_USAGE) /** * \brief Get the current mutex usage error message diff --git a/tests/src/fork_helpers.c b/tests/src/fork_helpers.c new file mode 100644 index 000000000..4da359b74 --- /dev/null +++ b/tests/src/fork_helpers.c @@ -0,0 +1,182 @@ +/** \file fork_helpers.c + * + * \brief Helper functions for testing with subprocesses. + */ + +/* + * Copyright The Mbed TLS Contributors + * SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later + */ + +#include "test_common.h" +#include +#include + +#if defined(MBEDTLS_PLATFORM_IS_UNIXLIKE) + +#include + +#include +#include +#include + +/** Child exit code for mbedtls_test_fork_run_child(). + */ +typedef enum { + /** Reporting of the child output or the child test result through + * the pipe succeeded. + * + * The content sent on the pipe has the following format: + * - [1 byte] #mbedtls_test_result_t \c test_result + * - Case \c test_result: + * - If \c test_result == #MBEDTLS_TEST_RESULT_SUCCESS: + * the output from the child body function. + * - Otherwise: + * the child failure (or skip) information, a direct write of + * a #mbedtls_test_result_t structure. + */ + CHILD_EXIT_CODE_OK = 0, + /** Something went wrong, e.g. a write error on the pipe. */ + CHILD_EXIT_CODE_REPORTING_FAILED = 122, +} child_exit_code_t; + +#if defined(__GNUC__) +__attribute__((__noreturn__)) +#endif +static void run_child( + int write_fd, + mbedtls_test_fork_child_callback_t *child_callback, + void *param, + unsigned char *buf, size_t size) +{ + /* If something goes wrong while trying to report what happened + * in the child, exit with a nonzero status. */ + int child_exit_code = CHILD_EXIT_CODE_REPORTING_FAILED; + /* We'll use stdio to write to the pipe, so we don't have to + * manage EINTR and such. */ + FILE *file = fdopen(write_fd, "a"); + size_t length = 0; + + TEST_ASSERT_ERRNO(file != NULL); + + child_callback(param, buf, size, &length); + + char result_char = mbedtls_test_get_result(); + TEST_ASSERT(fputc(result_char, file) != EOF); + +exit: + if (mbedtls_test_get_result() == MBEDTLS_TEST_RESULT_SUCCESS) { + if (fwrite(buf, length, 1, file) != 1) { + goto write_done; + } + } else { + mbedtls_test_info_t test_info; + mbedtls_test_info_save(&test_info); + if (fwrite(&test_info, sizeof(test_info), 1, file) != 1) { + goto write_done; + } + } + if (fflush(file) != 0) { + goto write_done; + } + child_exit_code = CHILD_EXIT_CODE_OK; + +write_done: + _exit(child_exit_code); +} + +int mbedtls_test_fork_run_child( + mbedtls_test_fork_child_callback_t *child_callback, + void *param, + unsigned char *child_output, size_t child_output_size, + size_t *child_output_length) +{ + *child_output_length = 0; + + int ret = -1; + pid_t pid = -1; + int pipe_fd[2] = { -1, -1 }; + + /* Set up a pipe. The child will write to pipe_fd[1], and the + * parent will read from pipe_fd[0]. */ + TEST_ASSERT_ERRNO(pipe(pipe_fd) != -1); + + pid = fork(); + TEST_ASSERT_ERRNO(pid != -1); + + if (pid == 0) { + /* The child code */ + close(pipe_fd[0]); + run_child(pipe_fd[1], child_callback, param, + child_output, child_output_size); + /* Unreachable */ + } + /* Beyond this point, we're in the parent (original) process. */ + + close(pipe_fd[1]); + pipe_fd[1] = -1; + + unsigned char result_char; + mbedtls_test_info_t child_test_info; + /* Normally, the child should give us a 1-byte result, then either + * the child body's output or a test info. */ + ssize_t n = read(pipe_fd[0], &result_char, 1); + TEST_EQUAL(n, 1); + + /* Tentatively read what we were promised. Don't commit to anything + * until we have the child's exit status. */ + size_t offset = 0; + if (result_char == MBEDTLS_TEST_RESULT_SUCCESS) { + do { + n = read(pipe_fd[0], + child_output + offset, + child_output_size - offset); + if (n > 0) { + offset += n; + } + } while (n > 0 && offset < child_output_size); + TEST_ASSERT_ERRNO(n != -1); + } else { + do { + n = read(pipe_fd[0], + (unsigned char *) &child_test_info + offset, + sizeof(child_test_info) - offset); + if (n > 0) { + offset += n; + } + } while (n > 0 && offset < sizeof(child_test_info)); + TEST_ASSERT_ERRNO(n != -1); + } + /* Check that the child didn't write more than it should. */ + if (n > 0) { + unsigned char excess; + TEST_EQUAL(read(pipe_fd[0], &excess, 1), 0); + } + + /* Close the pipe. If we left it open, there could be a deadlock if the + * child tried to write more than it should, while the parent is just + * waiting for the child to exit. */ + close(pipe_fd[0]); + pipe_fd[0] = -1; + + int wstatus; + TEST_ASSERT_ERRNO(waitpid(pid, &wstatus, 0) == pid); + if (WIFEXITED(wstatus) && WEXITSTATUS(wstatus) == CHILD_EXIT_CODE_OK) { + if (result_char == MBEDTLS_TEST_RESULT_SUCCESS) { + *child_output_length = n; + ret = 0; + } else { + mbedtls_test_info_overwrite(&child_test_info); + } + } else { + /* Weird status, just report it. */ + TEST_EQUAL(wstatus, 0); + } + +exit: + close(pipe_fd[0]); + close(pipe_fd[1]); + return ret; +} + +#endif /* MBEDTLS_PLATFORM_IS_UNIXLIKE */ diff --git a/tests/src/helpers.c b/tests/src/helpers.c index 9bf9a05d5..2b0742052 100644 --- a/tests/src/helpers.c +++ b/tests/src/helpers.c @@ -460,6 +460,28 @@ void mbedtls_test_info_reset(void) #endif /* MBEDTLS_THREADING_C */ } +void mbedtls_test_info_save(mbedtls_test_info_t *out) +{ +#ifdef MBEDTLS_THREADING_C + mbedtls_mutex_lock(&mbedtls_test_info_mutex); +#endif /* MBEDTLS_THREADING_C */ + memcpy(out, &mbedtls_test_info, sizeof(mbedtls_test_info)); +#ifdef MBEDTLS_THREADING_C + mbedtls_mutex_unlock(&mbedtls_test_info_mutex); +#endif /* MBEDTLS_THREADING_C */ +} + +void mbedtls_test_info_overwrite(const mbedtls_test_info_t *replacement) +{ +#ifdef MBEDTLS_THREADING_C + mbedtls_mutex_lock(&mbedtls_test_info_mutex); +#endif /* MBEDTLS_THREADING_C */ + memcpy(&mbedtls_test_info, replacement, sizeof(mbedtls_test_info)); +#ifdef MBEDTLS_THREADING_C + mbedtls_mutex_unlock(&mbedtls_test_info_mutex); +#endif /* MBEDTLS_THREADING_C */ +} + int mbedtls_test_equal(const char *test, int line_no, const char *filename, unsigned long long value1, unsigned long long value2) {