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 <Gilles.Peskine@arm.com>
This commit is contained in:
Gilles Peskine
2026-01-25 22:08:30 +01:00
parent d1a8b5b596
commit dbfd1a6fa9
4 changed files with 282 additions and 0 deletions
+59
View File
@@ -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 */
+19
View File
@@ -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
+182
View File
@@ -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 <test/helpers.h>
#include <test/macros.h>
#if defined(MBEDTLS_PLATFORM_IS_UNIXLIKE)
#include <test/fork_helpers.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
/** 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 */
+22
View File
@@ -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)
{