From c01d3ed7246e198cdf70674a256c59c18bc90381 Mon Sep 17 00:00:00 2001 From: Colton Willey Date: Fri, 10 Jul 2026 09:50:59 -0700 Subject: [PATCH] wp_seed_src: read /dev/urandom via raw fd to avoid seccomp SIGSYS SEED-SRC opened /dev/urandom with XFOPEN (buffered fopen). The first fread pulls read-ahead into the stdio buffer. sshd primes its RNG before the privsep fork, so the preauth child inherits that buffered FILE*; at child exit glibc stdio cleanup calls lseek() to drop the unread read-ahead. lseek() isn't in OpenSSH's preauth seccomp filter, so the child is killed with SIGSYS. Use a raw fd (open/read/close, O_CLOEXEC) instead: no buffer, no read-ahead, no exit-time lseek, matching OpenSSL's own device seeding. The fd and wolfSSL seed callback are shared and reference counted across provider contexts; the last unload closes the fd and restores the seed callback to wc_GenerateSeed (not NULL, which fails wc_InitRng with DRBG_NO_SEED_CB under FIPS). Also fixes a re-init leak that orphaned the prior buffered handle. --- .github/workflows/README.md | 2 +- .github/workflows/seed-src.yml | 10 +- include/wolfprovider/internal.h | 18 +- src/wp_seed_src.c | 296 +++++----- src/wp_wolfprov.c | 16 +- test/test_rand_seed.c | 392 +++++++++++++ test/test_seccomp_sandbox.c | 980 ++++++++++++++++++++++++++------ test/unit.c | 59 ++ test/unit.h | 22 + 9 files changed, 1486 insertions(+), 309 deletions(-) diff --git a/.github/workflows/README.md b/.github/workflows/README.md index a706a656..b199923d 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -31,7 +31,7 @@ ready_for_review) and on every push to `master`, `main`, or | `smoke-test.yml` | Minimal end-to-end: build, load the provider into stock OpenSSL, run `openssl list -providers` and a handful of `openssl` subcommands. Catches link-time and provider-registration regressions. | | `cmdline.yml` | Runs `scripts/cmd_test/do-cmd-tests.sh` — exercises every `openssl` CLI verb (genrsa, pkeyutl, enc, dgst, …) through wolfProvider. | | `fips-ready.yml` | Same as `simple` but builds wolfSSL with `--enable-fips=ready`. Sanity check that FIPS-ready compiles and basic tests pass without the full FIPS bundle. | -| `seed-src.yml` | Builds with `--enable-seed-src` (entropy seed source variant) and runs the unit tests. | +| `seed-src.yml` | Builds with `--enable-seed-src` (entropy seed source variant) and `-DWP_TEST_SECCOMP_SANDBOX`, then runs the unit tests including the OpenSSH fork+seccomp-sandbox regression suite. | | `multi-compiler.yml` | Cross-compiler sweep: gcc-9 through gcc-14 and clang-12 through latest. Catches toolchain-specific warnings / UB. | | `codespell.yml` | Spell-check on tracked source. `*.patch` is excluded because OSP patches mirror upstream source whose original spelling we shouldn't silently rewrite. | | `sanitizers.yml` | Builds wolfProvider with `-fsanitize=address,undefined` (one job) and `-fsanitize=thread` (separate job — TSan and ASan can't coexist in one binary), runs `make test` + `cmd_test/do-cmd-tests.sh` under each. Caches OpenSSL + wolfSSL source/install to avoid the ~15 min rebuild on every push. | diff --git a/.github/workflows/seed-src.yml b/.github/workflows/seed-src.yml index 6a774cba..969813bc 100644 --- a/.github/workflows/seed-src.yml +++ b/.github/workflows/seed-src.yml @@ -51,9 +51,13 @@ jobs: - name: Build and test wolfProvider with SEED-SRC run: | - # Force wolfSSL to not use getrandom syscall via ac_cv_func_getrandom=no. - # This ensures /dev/urandom is used as the entropy source, which is - # required to test the SEED-SRC feature's fork-safe caching behavior. + # ac_cv_func_getrandom=no forces wolfSSL onto /dev/urandom, the + # entropy path the SEED-SRC fork-safe caching test needs. + # WOLFPROV_CONFIG_CFLAGS reaches the unit.test compile via configure + # CFLAGS; -DWP_TEST_SECCOMP_SANDBOX compiles in the OpenSSH preauth + # seccomp suite (else it builds to a skip stub). ubuntu-22.04 runners + # are full VMs where PR_SET_SECCOMP works. + WOLFPROV_CONFIG_CFLAGS="-DWP_TEST_SECCOMP_SANDBOX" \ WOLFSSL_CONFIG_OPTS="--enable-all-crypto --with-eccminsz=192 --with-max-ecc-bits=1024 --enable-opensslcoexist --enable-sha ac_cv_func_getrandom=no" \ OPENSSL_TAG=${{ matrix.openssl_ref }} \ WOLFSSL_TAG=${{ matrix.wolfssl_ref }} \ diff --git a/include/wolfprovider/internal.h b/include/wolfprovider/internal.h index e0660520..21242eb1 100644 --- a/include/wolfprovider/internal.h +++ b/include/wolfprovider/internal.h @@ -36,6 +36,10 @@ #include #include +/* WP_HAVE_* feature macros: keep the config-guarded WOLFPROV_CTX fields below + * consistent across every translation unit that includes this header. */ +#include + #include "wp_params.h" #ifndef AES_BLOCK_SIZE @@ -146,16 +150,18 @@ typedef struct WOLFPROV_CTX { wolfSSL_Mutex rng_mutex; #endif BIO_METHOD *coreBioMethod; +#if defined(WP_HAVE_SEED_SRC) && defined(WP_HAVE_RANDOM) + /** Set once this context holds a urandom reference, so a failed init is not + * balanced by a cleanup that releases a reference it never took. */ + int urandomInitialized; +#endif } WOLFPROV_CTX; #if defined(WP_HAVE_SEED_SRC) && defined(WP_HAVE_RANDOM) /* - * Global /dev/urandom subsystem functions. - * - * These manage a cached file handle to /dev/urandom that is opened lazily - * on first entropy request (matching OpenSSL's default provider behavior). - * The file stays open so child processes inherit it and can read even - * in sandboxed environments that block openat(). + * Shared /dev/urandom entropy subsystem (cached fd + wolfSSL seed callback), + * reference counted: balance each wp_urandom_init() with one + * wp_urandom_cleanup(). See wp_seed_src.c. */ int wp_urandom_init(void); void wp_urandom_cleanup(void); diff --git a/src/wp_seed_src.c b/src/wp_seed_src.c index 49268853..491f103e 100644 --- a/src/wp_seed_src.c +++ b/src/wp_seed_src.c @@ -24,6 +24,8 @@ #include #include +#include +#include #include #include @@ -33,25 +35,26 @@ #include #include -/* Include wolfSSL port header for XFOPEN/XFREAD/XFCLOSE macros */ +/* Include wolfSSL port header for XREAD/XCLOSE macros. */ #include -/* - * /dev/urandom file caching for fork-safe entropy. - * - * These functions manage a cached file handle to /dev/urandom that is - * opened lazily on first entropy request and kept open. This matches - * OpenSSL's default provider behavior. The file stays open so child - * processes after fork() can inherit the underlying fd and read from it - * without needing to call openat(), which may be blocked by seccomp sandboxes. - */ +/* SEED-SRC reads /dev/urandom through wolfSSL's XREAD/XCLOSE, which wc_port.h + * only defines when file/directory support is enabled. NO_WOLFSSL_DIR builds + * are intentionally unsupported for SEED-SRC: fail with a clear message here + * rather than a cryptic "XREAD undefined" later. */ +#if defined(NO_WOLFSSL_DIR) + #error "wolfProvider SEED-SRC requires wolfSSL file support; NO_WOLFSSL_DIR builds are unsupported. Rebuild wolfSSL without NO_WOLFSSL_DIR, or disable SEED-SRC." +#endif + +#ifndef XBADFD + #define XBADFD -1 +#endif +#ifndef O_CLOEXEC + #define O_CLOEXEC 0 +#endif #define URANDOM_PATH "/dev/urandom" -/* - * Helper macros for thread-safe urandom access. - * These expand to no-ops when WP_SINGLE_THREADED is defined. - */ #ifndef WP_SINGLE_THREADED #define WP_URANDOM_LOCK() wc_LockMutex(wp_get_urandom_mutex()) #define WP_URANDOM_UNLOCK() wc_UnLockMutex(wp_get_urandom_mutex()) @@ -60,115 +63,161 @@ #define WP_URANDOM_UNLOCK() (void)0 #endif -/* - * Global cached /dev/urandom file handle. - * Opened lazily on first entropy request, kept open for the lifetime of the - * provider. This matches OpenSSL's model where random devices are opened - * on-demand and cached. - */ -static XFILE g_urandom_file = XBADFILE; +/* Cached /dev/urandom fd, shared across provider contexts and reference + * counted. Kept open so forked children inherit it and can read under a + * seccomp sandbox that blocks open()/openat(). */ +static int g_urandom_fd = XBADFD; + +/* Live provider contexts referencing the shared fd/callback; mutex-guarded. */ +static int g_urandom_ref_count = 0; -/* - * Flag indicating whether the seed callback has been registered. - */ #ifdef WC_RNG_SEED_CB static int g_seed_cb_registered = 0; #endif /** - * wolfSSL seed callback that uses the cached /dev/urandom file. + * Open the cached /dev/urandom fd if not already open. Idempotent; caller must + * hold the urandom mutex. * - * This callback is registered with wc_SetSeed_Cb() and is called by wolfSSL's - * DRBG when it needs entropy (including after fork detection). + * @return 0 on success. + * @return -1 if /dev/urandom cannot be opened. + */ +static int wp_urandom_open_locked(void) +{ + if (g_urandom_fd == XBADFD) { + do { + g_urandom_fd = open(URANDOM_PATH, O_RDONLY | O_CLOEXEC); + } while (g_urandom_fd == XBADFD && errno == EINTR); + + if (g_urandom_fd == XBADFD) { + WOLFPROV_MSG_DEBUG(WP_LOG_COMP_RNG, + "wp_urandom_open: failed to open " URANDOM_PATH); + return -1; + } + WOLFPROV_MSG_DEBUG(WP_LOG_COMP_RNG, + "wp_urandom_open: opened " URANDOM_PATH); + } + + return 0; +} + +/** + * Read exactly len bytes from the cached /dev/urandom fd, retrying on EINTR. + * Caller must hold the urandom mutex. * - * @param [in] os OS_Seed structure (unused) - * @param [out] seed Buffer to fill with seed data - * @param [in] sz Number of bytes to generate - * @return 0 on success - * @return -1 on failure + * @param [out] buf Buffer to fill. + * @param [in] len Number of bytes to read; must not exceed INT_MAX. + * @return len on success. + * @return -1 on failure, or if len exceeds INT_MAX. */ -#ifdef WC_RNG_SEED_CB -static int wp_wolfssl_seed_cb(OS_Seed* os, byte* seed, word32 sz) +static int wp_urandom_read_locked(unsigned char* buf, size_t len) { - size_t bytesRead; size_t total = 0; - (void)os; - - /* Lock before checking/opening file to prevent race conditions. - * The urandom mutex is initialized via constructor at library load, - * so it's guaranteed to be ready for use here. - */ - if (WP_URANDOM_LOCK() != 0) { + if (len > (size_t)INT_MAX) { + WOLFPROV_MSG_DEBUG(WP_LOG_COMP_RNG, + "wp_urandom_read: requested length too large"); return -1; } - /* Lazy open: open file on first entropy request */ - if (g_urandom_file == XBADFILE) { - g_urandom_file = XFOPEN(URANDOM_PATH, "rb"); - if (g_urandom_file == XBADFILE) { - WP_URANDOM_UNLOCK(); - return -1; - } + if (wp_urandom_open_locked() != 0) { + return -1; } - /* Read until we have all the bytes we need */ - while (total < sz) { - bytesRead = XFREAD(seed + total, 1, sz - total, g_urandom_file); + while (total < len) { + size_t toRead = len - total; + ssize_t bytesRead; + + bytesRead = XREAD(g_urandom_fd, buf + total, toRead); + if (bytesRead > 0) { - total += bytesRead; + total += (size_t)bytesRead; + } + else if (bytesRead < 0 && errno == EINTR) { + continue; } else { - /* EOF or error */ - WP_URANDOM_UNLOCK(); + WOLFPROV_MSG_DEBUG(WP_LOG_COMP_RNG, + "wp_urandom_read: XREAD failed"); return -1; } } + return (int)total; +} + +/** + * wolfSSL seed callback: fills seed from the cached /dev/urandom fd. Called by + * wolfSSL's DRBG when it needs entropy (including after fork). + * + * @param [in] os Unused. + * @param [out] seed Buffer to fill. + * @param [in] sz Number of bytes to generate. + * @return 0 on success. + * @return -1 on failure. + */ +#ifdef WC_RNG_SEED_CB +static int wp_wolfssl_seed_cb(OS_Seed* os, byte* seed, word32 sz) +{ + int rc; + + (void)os; + + if (WP_URANDOM_LOCK() != 0) { + return -1; + } + + rc = wp_urandom_read_locked(seed, sz); + WP_URANDOM_UNLOCK(); - return 0; + return (rc >= 0 && (size_t)rc == (size_t)sz) ? 0 : -1; } #endif /* WC_RNG_SEED_CB */ /** - * Initialize the urandom subsystem. - * - * This performs lazy initialization - the file handle is not opened until - * first entropy request. This matches OpenSSL's default provider behavior. - * The seed callback is registered here so wolfSSL can use our entropy source. + * Take one reference on the shared urandom subsystem. The seed callback is + * re-registered on every call because provider init resets wolfSSL's global + * callback. Balance each successful call with one wp_urandom_cleanup(). * * @return 0 on success. * @return -1 on failure. */ int wp_urandom_init(void) { - /* Lock to ensure thread-safe initialization. - * The urandom mutex is initialized via constructor at library load. - */ +#ifdef WC_RNG_SEED_CB + int firstRef; +#endif + if (WP_URANDOM_LOCK() != 0) { return -1; } - /* Initialize global file handle to invalid - will be opened lazily */ - g_urandom_file = XBADFILE; + if (g_urandom_ref_count == INT_MAX) { + WP_URANDOM_UNLOCK(); + return -1; + } + +#ifdef WC_RNG_SEED_CB + firstRef = (g_urandom_ref_count == 0); +#endif + g_urandom_ref_count++; #ifdef WC_RNG_SEED_CB - /* Register our seed callback with wolfSSL. - * This is critical for fork safety - wolfSSL will call this callback - * instead of wc_GenerateSeed() when it needs to reseed after fork. - * The callback will open the file lazily on first use. - */ - if (!g_seed_cb_registered) { - if (wc_SetSeed_Cb(wp_wolfssl_seed_cb) != 0) { - WOLFPROV_MSG_DEBUG_RETCODE(WP_LOG_LEVEL_DEBUG, - "wc_SetSeed_Cb failed", -1); - /* Non-fatal - continue without callback */ + if (wc_SetSeed_Cb(wp_wolfssl_seed_cb) != 0) { + WOLFPROV_MSG_DEBUG(WP_LOG_COMP_RNG, + "wc_SetSeed_Cb failed to register seed callback"); + /* Non-fatal - continue without callback */ + } + else { + g_seed_cb_registered = 1; + if (firstRef) { + WOLFPROV_MSG_DEBUG(WP_LOG_COMP_RNG, + "wp_urandom_init: registered wolfSSL seed callback"); } else { - g_seed_cb_registered = 1; - WOLFPROV_MSG_DEBUG_RETCODE(WP_LOG_LEVEL_DEBUG, - "wp_urandom_init: registered wolfSSL seed callback", 0); + WOLFPROV_MSG_DEBUG(WP_LOG_COMP_RNG, + "wp_urandom_init: re-registered wolfSSL seed callback"); } } #endif @@ -179,57 +228,58 @@ int wp_urandom_init(void) } /** - * Clean up the urandom subsystem. - * - * Closes the cached file handle if it was opened, and unregisters - * the seed callback. + * Release one reference taken by wp_urandom_init(). On the last reference, + * close the cached fd and restore wolfSSL's default seed callback. No-op if the + * reference count is already zero. */ void wp_urandom_cleanup(void) { - /* Lock to ensure thread-safe cleanup. - * The urandom mutex is initialized via constructor at library load. - */ if (WP_URANDOM_LOCK() != 0) { return; } + if (g_urandom_ref_count == 0) { + WP_URANDOM_UNLOCK(); + return; + } + + g_urandom_ref_count--; + if (g_urandom_ref_count > 0) { + WP_URANDOM_UNLOCK(); + return; + } + #ifdef WC_RNG_SEED_CB - /* Unregister seed callback */ + /* Restore wolfSSL's default seed callback rather than NULL: a later + * wc_InitRng() in the same process would otherwise fail with + * DRBG_NO_SEED_CB. wc_GenerateSeed is the baseline provider init installs. */ if (g_seed_cb_registered) { - wc_SetSeed_Cb(NULL); + wc_SetSeed_Cb(wc_GenerateSeed); g_seed_cb_registered = 0; } #endif - /* Close global file if it was opened */ - if (g_urandom_file != XBADFILE) { - XFCLOSE(g_urandom_file); - g_urandom_file = XBADFILE; - WOLFPROV_MSG_DEBUG(WP_LOG_LEVEL_DEBUG, + if (g_urandom_fd != XBADFD) { + XCLOSE(g_urandom_fd); + g_urandom_fd = XBADFD; + WOLFPROV_MSG_DEBUG(WP_LOG_COMP_RNG, "wp_urandom_cleanup: closed " URANDOM_PATH); } WP_URANDOM_UNLOCK(); - - /* Note: global urandom mutex is managed via constructor/destructor */ } /** - * Read random bytes from /dev/urandom. + * Read random bytes from /dev/urandom (public entry; takes the urandom mutex). * - * Opens /dev/urandom lazily on first call, then keeps it open for subsequent - * reads. This matches OpenSSL's default provider behavior. The file stays open - * so child processes can inherit it and read even in sandboxed environments. - * - * @param [out] buf Buffer to fill with random bytes. - * @param [in] len Number of bytes to read. + * @param [out] buf Buffer to fill. + * @param [in] len Number of bytes to read. * @return Number of bytes read on success. * @return -1 on failure. */ int wp_urandom_read(unsigned char* buf, size_t len) { - size_t bytesRead; - size_t total = 0; + int rc; if (buf == NULL || len == 0) { return -1; @@ -239,37 +289,11 @@ int wp_urandom_read(unsigned char* buf, size_t len) return -1; } - /* Lazy open: open file on first entropy request */ - if (g_urandom_file == XBADFILE) { - g_urandom_file = XFOPEN(URANDOM_PATH, "rb"); - if (g_urandom_file == XBADFILE) { - WOLFPROV_MSG_DEBUG(WP_LOG_LEVEL_DEBUG, - "wp_urandom_read: failed to open " URANDOM_PATH); - WP_URANDOM_UNLOCK(); - return -1; - } - WOLFPROV_MSG_DEBUG(WP_LOG_LEVEL_DEBUG, - "wp_urandom_read: opened " URANDOM_PATH); - } - - /* Read until we have all the bytes we need */ - while (total < len) { - bytesRead = XFREAD(buf + total, 1, len - total, g_urandom_file); - if (bytesRead > 0) { - total += bytesRead; - } - else { - /* EOF or error - shouldn't happen with /dev/urandom */ - WOLFPROV_MSG_DEBUG(WP_LOG_LEVEL_DEBUG, - "wp_urandom_read: XFREAD failed"); - WP_URANDOM_UNLOCK(); - return -1; - } - } + rc = wp_urandom_read_locked(buf, len); WP_URANDOM_UNLOCK(); - return (int)total; + return rc; } @@ -467,8 +491,8 @@ static int wp_seed_src_generate(wp_SeedSrcCtx* ctx, unsigned char* out, * processes can inherit the fd and read even in seccomp sandboxes. */ rc = wp_urandom_read(buf, outLen); - if (rc != (int)outLen) { - WOLFPROV_MSG_DEBUG_RETCODE(WP_LOG_LEVEL_DEBUG, + if (rc < 0 || (size_t)rc != outLen) { + WOLFPROV_MSG_DEBUG_RETCODE(WP_LOG_COMP_RNG, "wp_urandom_read failed", rc); ok = 0; } @@ -654,8 +678,8 @@ static size_t wp_seed_src_get_seed(wp_SeedSrcCtx* ctx, unsigned char** pSeed, * processes can inherit the fd and read even in seccomp sandboxes. */ rc = wp_urandom_read(buffer, minLen); - if (rc != (int)minLen) { - WOLFPROV_MSG_DEBUG_RETCODE(WP_LOG_LEVEL_DEBUG, + if (rc < 0 || (size_t)rc != minLen) { + WOLFPROV_MSG_DEBUG_RETCODE(WP_LOG_COMP_RNG, "wp_urandom_read failed", rc); OPENSSL_clear_free(buffer, minLen); buffer = NULL; diff --git a/src/wp_wolfprov.c b/src/wp_wolfprov.c index d92618ef..0110b2e1 100644 --- a/src/wp_wolfprov.c +++ b/src/wp_wolfprov.c @@ -255,8 +255,11 @@ static WOLFPROV_CTX* wolfssl_prov_ctx_new(void) if (ctx != NULL) { if (wp_urandom_init() != 0) { /* Non-fatal - fall back to normal RNG on platforms without urandom */ - WOLFPROV_MSG_DEBUG_RETCODE(WP_LOG_LEVEL_DEBUG, - "wp_urandom_init failed, will use WC_RNG instead", 0); + WOLFPROV_MSG_DEBUG(WP_LOG_COMP_PROVIDER, + "wp_urandom_init failed, will use WC_RNG instead"); + } + else { + ctx->urandomInitialized = 1; } } #endif @@ -272,8 +275,11 @@ static WOLFPROV_CTX* wolfssl_prov_ctx_new(void) static void wolfssl_prov_ctx_free(WOLFPROV_CTX* ctx) { #if defined(WP_HAVE_SEED_SRC) && defined(WP_HAVE_RANDOM) - /* Clean up urandom subsystem */ - wp_urandom_cleanup(); + /* Release a reference only if this context took one; pairing a failed init + * with a cleanup would tear down the shared subsystem other contexts use. */ + if (ctx->urandomInitialized) { + wp_urandom_cleanup(); + } #endif BIO_meth_free(ctx->coreBioMethod); @@ -1463,6 +1469,8 @@ int wolfssl_provider_init(const OSSL_CORE_HANDLE* handle, if (ok) { #ifdef WC_RNG_SEED_CB + /* Required under FIPS: the global seedCb defaults to NULL, so the + * wc_InitRng() in wolfssl_prov_ctx_new() would fail DRBG_NO_SEED_CB. */ wc_SetSeed_Cb(wc_GenerateSeed); #endif /* FIPS CAST tests are now run lazily per-algorithm via wp_init_cast() */ diff --git a/test/test_rand_seed.c b/test/test_rand_seed.c index e2c62cdc..51ed2042 100644 --- a/test/test_rand_seed.c +++ b/test/test_rand_seed.c @@ -33,12 +33,28 @@ #include "unit.h" +/* test_seed_src_refcount / test_seed_src_reload are declared in unit.h and + * defined in both configurations below (real tests when SEED-SRC is enabled, + * otherwise skip stubs). */ + #if defined(WP_HAVE_SEED_SRC) && defined(WP_HAVE_RANDOM) +#include +#include +#include +#include +#include + #include #include #include +#include +#include + +/* wpUnitProviderDir/wpUnitProviderName are declared in unit.h for every + * SEED-SRC build (see the WP_HAVE_SEED_SRC && WP_HAVE_RANDOM block there). */ + /** * Test that we can fetch a SEED-SRC and generate random bytes from it. * @@ -721,6 +737,368 @@ static int test_drbg_reseed_helper(OSSL_LIB_CTX *libCtx, const char *propq) return err; } +/** + * Non-seccomp coverage for reference counting of the shared /dev/urandom fd and + * wolfSSL seed callback: load wolfProvider into two library contexts, draw + * entropy from both, unload the first, and confirm the survivor still gets + * entropy via both the OpenSSL RAND path and the wolfSSL seed callback. The main + * suite holds a wolfProvider reference throughout, so the fd is not closed here; + * the seccomp T3 test covers the decrement-to-zero close path. + */ +int test_seed_src_refcount(void *data) +{ + int err = 0; + const char *providerDir = wpUnitProviderDir; + const char *providerName = wpUnitProviderName; + OSSL_LIB_CTX *ctx1 = NULL; + OSSL_LIB_CTX *ctx2 = NULL; + OSSL_PROVIDER *provider1 = NULL; + OSSL_PROVIDER *provider2 = NULL; + EVP_RAND_CTX *rctx = NULL; + unsigned char buf[32]; + WC_RNG rng; + int rngInit = 0; + + (void)data; + + if (providerDir == NULL) { + providerDir = ".libs"; + } + if (providerName == NULL) { + providerName = wolfprovider_id; + } + + PRINT_MSG("Testing SEED-SRC shared urandom fd/callback refcount lifecycle"); + + /* First provider context: forces wp_urandom_init (refcount increments). */ + ctx1 = OSSL_LIB_CTX_new(); + if (ctx1 == NULL) { + PRINT_ERR_MSG("Failed to create first library context"); + err = 1; + goto cleanup; + } + if (OSSL_PROVIDER_set_default_search_path(ctx1, providerDir) != 1) { + PRINT_ERR_MSG("Failed to set search path for first context: %s", + providerDir); + err = 1; + goto cleanup; + } + provider1 = OSSL_PROVIDER_load(ctx1, providerName); + if (provider1 == NULL) { + PRINT_ERR_MSG("Failed to load provider %s into first context", + providerName); + err = 1; + goto cleanup; + } + if (RAND_bytes_ex(ctx1, buf, sizeof(buf), 0) != 1) { + PRINT_ERR_MSG("First-context RAND_bytes_ex failed"); + err = 1; + goto cleanup; + } + + /* Second provider context: shares the fd/callback (refcount increments). */ + ctx2 = OSSL_LIB_CTX_new(); + if (ctx2 == NULL) { + PRINT_ERR_MSG("Failed to create second library context"); + err = 1; + goto cleanup; + } + if (OSSL_PROVIDER_set_default_search_path(ctx2, providerDir) != 1) { + PRINT_ERR_MSG("Failed to set search path for second context: %s", + providerDir); + err = 1; + goto cleanup; + } + provider2 = OSSL_PROVIDER_load(ctx2, providerName); + if (provider2 == NULL) { + PRINT_ERR_MSG("Failed to load provider %s into second context", + providerName); + err = 1; + goto cleanup; + } + if (RAND_bytes_ex(ctx2, buf, sizeof(buf), 0) != 1) { + PRINT_ERR_MSG("Second-context RAND_bytes_ex failed"); + err = 1; + goto cleanup; + } + + /* Unload the first context (one cleanup, one decrement). The second context + * and the main suite still hold references, so the fd/callback must survive. */ + OSSL_PROVIDER_unload(provider1); + provider1 = NULL; + OSSL_LIB_CTX_free(ctx1); + ctx1 = NULL; + + /* Survivor must still draw entropy; force a fresh seed pull so an fd wrongly + * closed on the first teardown would surface here. */ + rctx = RAND_get0_public(ctx2); + if (rctx == NULL) { + PRINT_ERR_MSG("Survivor RAND_get0_public failed after first unload"); + err = 1; + goto cleanup; + } + if (EVP_RAND_reseed(rctx, 0, NULL, 0, NULL, 0) != 1) { + PRINT_ERR_MSG("Survivor EVP_RAND_reseed failed after first unload"); + err = 1; + goto cleanup; + } + if (RAND_bytes_ex(ctx2, buf, sizeof(buf), 0) != 1) { + PRINT_ERR_MSG("Survivor RAND_bytes_ex failed after first unload"); + err = 1; + goto cleanup; + } + + /* The wolfSSL seed callback must also still be registered for the survivor. */ + if (wc_InitRng(&rng) != 0) { + PRINT_ERR_MSG("wc_InitRng failed after first unload"); + err = 1; + goto cleanup; + } + rngInit = 1; + if (wc_RNG_GenerateBlock(&rng, buf, sizeof(buf)) != 0) { + PRINT_ERR_MSG("wc_RNG_GenerateBlock failed after first unload"); + err = 1; + goto cleanup; + } + + PRINT_MSG("Survivor context still produces entropy after first unload"); + +cleanup: + if (rngInit) { + wc_FreeRng(&rng); + } + /* Unload the second context too. The main suite's reference keeps the count + * above zero, so the fd stays open for the remaining tests. */ + OSSL_PROVIDER_unload(provider2); + OSSL_LIB_CTX_free(ctx2); + OSSL_PROVIDER_unload(provider1); + OSSL_LIB_CTX_free(ctx1); + + return err; +} + +/** + * Fresh-process worker for the SEED-SRC full teardown -> reload cycle. Runs + * after a re-exec (see test_seed_src_reload) so the refcount starts at zero and + * can be driven to zero and back: load reopens the shared fd and registers + * wp_wolfssl_seed_cb; a full unload closes the fd and restores the seed callback + * to wc_GenerateSeed (not NULL, which would yield DRBG_NO_SEED_CB); reload must + * reopen and re-register. + * + * Returns 0 on success, non-zero on failure. + */ +int test_seed_src_reload_helper(void) +{ + int err = 0; + const char *providerDir = wpUnitProviderDir; + const char *providerName = wpUnitProviderName; + OSSL_LIB_CTX *ctx1 = NULL; + OSSL_LIB_CTX *ctx2 = NULL; + OSSL_PROVIDER *provider1 = NULL; + OSSL_PROVIDER *provider2 = NULL; + unsigned char buf[32]; + WC_RNG rng; + + if (providerDir == NULL) { + providerDir = ".libs"; + } + if (providerName == NULL) { + providerName = wolfprovider_id; + } + + PRINT_MSG("Testing SEED-SRC teardown-to-zero then reload in fresh process"); + + /* + * Load wolfProvider into the first context. This is the first reference, so + * wp_urandom_init lazily opens the shared /dev/urandom fd (refcount 0 -> 1) + * and registers wp_wolfssl_seed_cb. + */ + ctx1 = OSSL_LIB_CTX_new(); + if (ctx1 == NULL) { + PRINT_ERR_MSG("Failed to create first library context"); + err = 1; + } + if (err == 0 && + OSSL_PROVIDER_set_default_search_path(ctx1, providerDir) != 1) { + PRINT_ERR_MSG("Failed to set search path for first context: %s", + providerDir); + err = 1; + } + if (err == 0) { + provider1 = OSSL_PROVIDER_load(ctx1, providerName); + if (provider1 == NULL) { + PRINT_ERR_MSG("Failed to load provider %s into first context", + providerName); + err = 1; + } + } + + /* Draw entropy so the shared fd is actually opened. */ + if (err == 0 && RAND_bytes_ex(ctx1, buf, sizeof(buf), 0) != 1) { + PRINT_ERR_MSG("First-context RAND_bytes_ex failed"); + err = 1; + } + + /* Fully unload: releases the last reference, so cleanup decrements to zero, + * closes the fd, and restores the seed callback to wc_GenerateSeed. */ + if (provider1 != NULL) { + OSSL_PROVIDER_unload(provider1); + provider1 = NULL; + } + OSSL_LIB_CTX_free(ctx1); + ctx1 = NULL; + + /* With the callback restored (not NULL), a direct wolfCrypt RNG must still + * seed and generate; a NULL callback would surface as DRBG_NO_SEED_CB. */ + if (err == 0) { + if (wc_InitRng(&rng) != 0) { + PRINT_ERR_MSG("wc_InitRng failed after teardown to zero"); + err = 1; + } + else { + if (wc_RNG_GenerateBlock(&rng, buf, sizeof(buf)) != 0) { + PRINT_ERR_MSG( + "wc_RNG_GenerateBlock failed after teardown to zero"); + err = 1; + } + wc_FreeRng(&rng); + } + } + + /* Reload into a second context (refcount 0 -> 1): the fd must lazily reopen + * and wp_wolfssl_seed_cb must re-register. */ + if (err == 0) { + ctx2 = OSSL_LIB_CTX_new(); + if (ctx2 == NULL) { + PRINT_ERR_MSG("Failed to create second library context"); + err = 1; + } + } + if (err == 0 && + OSSL_PROVIDER_set_default_search_path(ctx2, providerDir) != 1) { + PRINT_ERR_MSG("Failed to set search path for second context: %s", + providerDir); + err = 1; + } + if (err == 0) { + provider2 = OSSL_PROVIDER_load(ctx2, providerName); + if (provider2 == NULL) { + PRINT_ERR_MSG("Failed to load provider %s into second context", + providerName); + err = 1; + } + } + + /* Entropy through the reopened fd must succeed. */ + if (err == 0 && RAND_bytes_ex(ctx2, buf, sizeof(buf), 0) != 1) { + PRINT_ERR_MSG("Second-context RAND_bytes_ex failed after reload"); + err = 1; + } + + /* The wolfSSL seed callback path must work after reload too. */ + if (err == 0) { + if (wc_InitRng(&rng) != 0) { + PRINT_ERR_MSG("wc_InitRng failed after reload"); + err = 1; + } + else { + if (wc_RNG_GenerateBlock(&rng, buf, sizeof(buf)) != 0) { + PRINT_ERR_MSG("wc_RNG_GenerateBlock failed after reload"); + err = 1; + } + wc_FreeRng(&rng); + } + } + + if (err == 0) { + PRINT_MSG("SEED-SRC reload after full teardown succeeded"); + } + + OSSL_PROVIDER_unload(provider2); + OSSL_LIB_CTX_free(ctx2); + OSSL_PROVIDER_unload(provider1); + OSSL_LIB_CTX_free(ctx1); + + return err; +} + +/** + * Non-seccomp coverage for the SEED-SRC shared fd/seed callback being fully torn + * down (refcount to zero) and reloaded. The main suite always holds a + * wolfProvider reference, so this re-execs a fresh unit.test as + * --seed-src-reload-helper (refcount starts at zero) and passes iff that child + * completes the teardown-then-reload sequence and exits 0. + */ +int test_seed_src_reload(void *data) +{ + const char *providerDir = wpUnitProviderDir; + const char *providerName = wpUnitProviderName; + char exePath[PATH_MAX]; + ssize_t exeLen; + pid_t pid; + int status; + int err = 0; + + (void)data; + + if (providerDir == NULL) { + providerDir = ".libs"; + } + if (providerName == NULL) { + providerName = wolfprovider_id; + } + + PRINT_MSG("Testing SEED-SRC full teardown then reload via re-exec"); + + /* This test re-execs the unit binary via /proc/self/exe to get a fresh, + * zero-based refcount. On platforms without /proc/self/exe (non-Linux) + * there is no portable way to re-exec, so skip rather than hard-fail. */ + exeLen = readlink("/proc/self/exe", exePath, sizeof(exePath) - 1); + if (exeLen < 0 || exeLen >= (ssize_t)sizeof(exePath) - 1) { + PRINT_MSG("SEED-SRC reload test skipped - /proc/self/exe unavailable"); + return 0; + } + exePath[exeLen] = '\0'; + + pid = fork(); + if (pid == -1) { + PRINT_ERR_MSG("fork() failed: %s", strerror(errno)); + return 1; + } + + if (pid == 0) { + execl(exePath, exePath, "--seed-src-reload-helper", providerDir, + providerName, (char *)NULL); + PRINT_ERR_MSG("execl reload helper failed: %s", strerror(errno)); + _exit(127); + } + + if (waitpid(pid, &status, 0) == -1) { + PRINT_ERR_MSG("waitpid(reload helper) failed: %s", strerror(errno)); + return 1; + } + + if (WIFEXITED(status) && WEXITSTATUS(status) == 0) { + PRINT_MSG("SEED-SRC reload helper passed"); + } + else if (WIFEXITED(status)) { + PRINT_ERR_MSG("SEED-SRC reload helper exited with status %d", + WEXITSTATUS(status)); + err = 1; + } + else if (WIFSIGNALED(status)) { + PRINT_ERR_MSG("SEED-SRC reload helper killed by signal %d", + WTERMSIG(status)); + err = 1; + } + else { + PRINT_ERR_MSG("SEED-SRC reload helper exited abnormally"); + err = 1; + } + + return err; +} + #else /* !(WP_HAVE_SEED_SRC && WP_HAVE_RANDOM) */ int test_rand_seed(void *data) @@ -731,6 +1109,20 @@ int test_rand_seed(void *data) return 0; } +int test_seed_src_refcount(void *data) +{ + (void)data; + PRINT_MSG("SEED-SRC refcount lifecycle test skipped - not enabled"); + return 0; +} + +int test_seed_src_reload(void *data) +{ + (void)data; + PRINT_MSG("SEED-SRC reload lifecycle test skipped - not enabled"); + return 0; +} + #endif /* WP_HAVE_SEED_SRC && WP_HAVE_RANDOM */ int test_drbg_reseed(void *data) diff --git a/test/test_seccomp_sandbox.c b/test/test_seccomp_sandbox.c index 9e43a64c..50f05b56 100644 --- a/test/test_seccomp_sandbox.c +++ b/test/test_seccomp_sandbox.c @@ -19,41 +19,37 @@ */ /* - * This test mimics OpenSSH's seccomp sandbox behavior to verify that - * wolfProvider's DRBG can operate correctly after fork() when file - * descriptor operations are restricted. + * Seccomp coverage for wolfProvider SEED-SRC under OpenSSH's preauth sandbox. * - * OpenSSH Flow (sshd-session.c privsep_preauth): - * 1. Parent forks child for privilege separation - * 2. Child calls reseed_prngs() BEFORE sandbox (RAND_poll, RAND_bytes) - * 3. Child applies seccomp sandbox (blocks open/openat syscalls) - * 4. Child performs crypto operations under sandbox restrictions - * - * The problem: After sandbox is applied, if DRBG needs to reseed, - * it cannot open /dev/urandom because openat() returns EACCES. - * - * This test verifies: - * - Default OpenSSL provider works under these conditions (baseline) - * - wolfProvider should eventually work (currently expected to fail) - * - * NOTE: This test is disabled by default because it uses seccomp which - * can interfere with other tests and debugging. Enable it by defining - * WP_TEST_SECCOMP_SANDBOX in CFLAGS: - * ./configure CFLAGS="-DWP_TEST_SECCOMP_SANDBOX" + * These are passing regression tests. The original bug held /dev/urandom as a + * buffered stdio FILE*: OpenSSH's preauth seccomp filter does not allow + * lseek(), so glibc stdio cleanup could kill the child with SIGSYS while + * rewinding fread read-ahead. The SEED-SRC path now uses a raw /dev/urandom fd + * (open/read/close, no stdio buffering), so libc exit performs no lseek(). The + * tests here would catch a reintroduction of the buffered-stream SIGSYS bug. */ #include "unit.h" -#if defined(WP_TEST_SECCOMP_SANDBOX) && defined(WP_HAVE_SEED_SRC) && defined(WP_HAVE_RANDOM) +#if defined(WP_TEST_SECCOMP_SANDBOX) && defined(WP_HAVE_SEED_SRC) && \ + defined(WP_HAVE_RANDOM) #include #include #include #include +#include #include #include #include #include +#include +#include +#include +#include +#include +#include +#include /* Check for seccomp support */ #ifdef __has_include @@ -70,9 +66,11 @@ #include #include #include +#include +#include #include -/* Determine the correct audit architecture */ +/* Determine the correct audit architecture. */ #if defined(__x86_64__) #define SECCOMP_AUDIT_ARCH AUDIT_ARCH_X86_64 #elif defined(__i386__) @@ -82,7 +80,7 @@ #elif defined(__arm__) #define SECCOMP_AUDIT_ARCH AUDIT_ARCH_ARM #else - /* Unsupported architecture - disable test */ + /* Unsupported architecture - disable test. */ #undef WP_HAVE_SECCOMP #endif @@ -90,111 +88,357 @@ #ifdef WP_HAVE_SECCOMP -/* BPF macros for seccomp filter - mirrors OpenSSH sandbox-seccomp-filter.c */ +#ifdef SECCOMP_RET_KILL_PROCESS +#define SECCOMP_FILTER_FAIL SECCOMP_RET_KILL_PROCESS +#else +#define SECCOMP_FILTER_FAIL SECCOMP_RET_KILL +#endif + +/* OpenSSH 9.9p1 sandbox-seccomp-filter.c preauth filter body. */ +#if __BYTE_ORDER == __LITTLE_ENDIAN +# define ARG_LO_OFFSET 0 +# define ARG_HI_OFFSET sizeof(uint32_t) +#elif __BYTE_ORDER == __BIG_ENDIAN +# define ARG_LO_OFFSET sizeof(uint32_t) +# define ARG_HI_OFFSET 0 +#else +#error "Unknown endianness" +#endif + +/* Simple helpers to avoid manual errors (but larger BPF programs). */ #define SC_DENY(_nr, _errno) \ BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, (_nr), 0, 1), \ BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_ERRNO|(_errno)) - #define SC_ALLOW(_nr) \ BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, (_nr), 0, 1), \ BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_ALLOW) +#define SC_ALLOW_ARG(_nr, _arg_nr, _arg_val) \ + BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, (_nr), 0, 6), \ + /* load and test syscall argument, low word */ \ + BPF_STMT(BPF_LD+BPF_W+BPF_ABS, \ + offsetof(struct seccomp_data, args[(_arg_nr)]) + ARG_LO_OFFSET), \ + BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, \ + ((_arg_val) & 0xFFFFFFFF), 0, 3), \ + /* load and test syscall argument, high word */ \ + BPF_STMT(BPF_LD+BPF_W+BPF_ABS, \ + offsetof(struct seccomp_data, args[(_arg_nr)]) + ARG_HI_OFFSET), \ + BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, \ + (((uint32_t)((uint64_t)(_arg_val) >> 32)) & 0xFFFFFFFF), 0, 1), \ + BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_ALLOW), \ + /* reload syscall number; all rules expect it in accumulator */ \ + BPF_STMT(BPF_LD+BPF_W+BPF_ABS, \ + offsetof(struct seccomp_data, nr)) +/* Allow if syscall argument contains only values in mask */ +#define SC_ALLOW_ARG_MASK(_nr, _arg_nr, _arg_mask) \ + BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, (_nr), 0, 8), \ + /* load, mask and test syscall argument, low word */ \ + BPF_STMT(BPF_LD+BPF_W+BPF_ABS, \ + offsetof(struct seccomp_data, args[(_arg_nr)]) + ARG_LO_OFFSET), \ + BPF_STMT(BPF_ALU+BPF_AND+BPF_K, ~((_arg_mask) & 0xFFFFFFFF)), \ + BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, 0, 0, 4), \ + /* load, mask and test syscall argument, high word */ \ + BPF_STMT(BPF_LD+BPF_W+BPF_ABS, \ + offsetof(struct seccomp_data, args[(_arg_nr)]) + ARG_HI_OFFSET), \ + BPF_STMT(BPF_ALU+BPF_AND+BPF_K, \ + ~(((uint32_t)((uint64_t)(_arg_mask) >> 32)) & 0xFFFFFFFF)), \ + BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, 0, 0, 1), \ + BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_ALLOW), \ + /* reload syscall number; all rules expect it in accumulator */ \ + BPF_STMT(BPF_LD+BPF_W+BPF_ABS, \ + offsetof(struct seccomp_data, nr)) +/* Deny unless syscall argument contains only values in mask */ +#define SC_DENY_UNLESS_ARG_MASK(_nr, _arg_nr, _arg_mask, _errno) \ + BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, (_nr), 0, 8), \ + /* load, mask and test syscall argument, low word */ \ + BPF_STMT(BPF_LD+BPF_W+BPF_ABS, \ + offsetof(struct seccomp_data, args[(_arg_nr)]) + ARG_LO_OFFSET), \ + BPF_STMT(BPF_ALU+BPF_AND+BPF_K, ~((_arg_mask) & 0xFFFFFFFF)), \ + BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, 0, 0, 3), \ + /* load, mask and test syscall argument, high word */ \ + BPF_STMT(BPF_LD+BPF_W+BPF_ABS, \ + offsetof(struct seccomp_data, args[(_arg_nr)]) + ARG_HI_OFFSET), \ + BPF_STMT(BPF_ALU+BPF_AND+BPF_K, \ + ~(((uint32_t)((uint64_t)(_arg_mask) >> 32)) & 0xFFFFFFFF)), \ + BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, 0, 1, 0), \ + BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_ERRNO|(_errno)), \ + /* reload syscall number; all rules expect it in accumulator */ \ + BPF_STMT(BPF_LD+BPF_W+BPF_ABS, \ + offsetof(struct seccomp_data, nr)) +/* Special handling for futex(2) that combines a bitmap and operation number */ +#if defined(__NR_futex) || defined(__NR_futex_time64) +#define SC_FUTEX_MASK (FUTEX_PRIVATE_FLAG|FUTEX_CLOCK_REALTIME) +#define SC_ALLOW_FUTEX_OP(_nr, _op) \ + BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, (_nr), 0, 8), \ + /* load syscall argument, low word */ \ + BPF_STMT(BPF_LD+BPF_W+BPF_ABS, \ + offsetof(struct seccomp_data, args[1]) + ARG_LO_OFFSET), \ + /* mask off allowed bitmap values, low word */ \ + BPF_STMT(BPF_ALU+BPF_AND+BPF_K, ~(SC_FUTEX_MASK & 0xFFFFFFFF)), \ + /* test operation number, low word */ \ + BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, ((_op) & 0xFFFFFFFF), 0, 4), \ + /* load syscall argument, high word */ \ + BPF_STMT(BPF_LD+BPF_W+BPF_ABS, \ + offsetof(struct seccomp_data, args[1]) + ARG_HI_OFFSET), \ + /* mask off allowed bitmap values, high word */ \ + BPF_STMT(BPF_ALU+BPF_AND+BPF_K, \ + ~(((uint32_t)((uint64_t)SC_FUTEX_MASK >> 32)) & 0xFFFFFFFF)), \ + /* test operation number, high word */ \ + BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, \ + (((uint32_t)((uint64_t)(_op) >> 32)) & 0xFFFFFFFF), 0, 1), \ + BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_ALLOW), \ + /* reload syscall number; all rules expect it in accumulator */ \ + BPF_STMT(BPF_LD+BPF_W+BPF_ABS, offsetof(struct seccomp_data, nr)) -/* - * Minimal seccomp filter that mimics OpenSSH's preauth sandbox. - * Key restrictions: - * - Deny open/openat with EACCES (prevents opening /dev/urandom) - * - Allow essential syscalls for the test to run - */ -static const struct sock_filter naomi_insns[] = { - /* Verify architecture */ - BPF_STMT(BPF_LD+BPF_W+BPF_ABS, offsetof(struct seccomp_data, arch)), - BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, SECCOMP_AUDIT_ARCH, 1, 0), - BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_KILL), +/* Use this for both __NR_futex and __NR_futex_time64 */ +# define SC_FUTEX(_nr) \ + SC_ALLOW_FUTEX_OP(_nr, FUTEX_WAIT), \ + SC_ALLOW_FUTEX_OP(_nr, FUTEX_WAIT_BITSET), \ + SC_ALLOW_FUTEX_OP(_nr, FUTEX_WAKE), \ + SC_ALLOW_FUTEX_OP(_nr, FUTEX_WAKE_BITSET), \ + SC_ALLOW_FUTEX_OP(_nr, FUTEX_REQUEUE), \ + SC_ALLOW_FUTEX_OP(_nr, FUTEX_CMP_REQUEUE) +#endif /* __NR_futex || __NR_futex_time64 */ + +#if defined(__NR_mmap) || defined(__NR_mmap2) +# ifdef MAP_FIXED_NOREPLACE +# define SC_MMAP_FLAGS MAP_PRIVATE|MAP_ANONYMOUS|MAP_FIXED|MAP_FIXED_NOREPLACE +# else +# define SC_MMAP_FLAGS MAP_PRIVATE|MAP_ANONYMOUS|MAP_FIXED +# endif /* MAP_FIXED_NOREPLACE */ +/* Use this for both __NR_mmap and __NR_mmap2 variants */ +# define SC_MMAP(_nr) \ + SC_DENY_UNLESS_ARG_MASK(_nr, 3, SC_MMAP_FLAGS, EINVAL), \ + SC_ALLOW_ARG_MASK(_nr, 2, PROT_READ|PROT_WRITE|PROT_NONE) +#endif /* __NR_mmap || __NR_mmap2 */ - /* Load syscall number */ - BPF_STMT(BPF_LD+BPF_W+BPF_ABS, offsetof(struct seccomp_data, nr)), +/* Syscall filtering set for preauth. */ +static const struct sock_filter preauth_insns[] = { + /* Ensure the syscall arch convention is as expected. */ + BPF_STMT(BPF_LD+BPF_W+BPF_ABS, + offsetof(struct seccomp_data, arch)), + BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, SECCOMP_AUDIT_ARCH, 1, 0), + BPF_STMT(BPF_RET+BPF_K, SECCOMP_FILTER_FAIL), + /* Load the syscall number for checking. */ + BPF_STMT(BPF_LD+BPF_W+BPF_ABS, + offsetof(struct seccomp_data, nr)), - /* Deny file open syscalls with EACCES - this is the key restriction */ + /* Syscalls to non-fatally deny */ +#ifdef __NR_lstat + SC_DENY(__NR_lstat, EACCES), +#endif +#ifdef __NR_lstat64 + SC_DENY(__NR_lstat64, EACCES), +#endif +#ifdef __NR_fstat + SC_DENY(__NR_fstat, EACCES), +#endif +#ifdef __NR_fstat64 + SC_DENY(__NR_fstat64, EACCES), +#endif +#ifdef __NR_fstatat64 + SC_DENY(__NR_fstatat64, EACCES), +#endif #ifdef __NR_open SC_DENY(__NR_open, EACCES), #endif #ifdef __NR_openat SC_DENY(__NR_openat, EACCES), #endif +#ifdef __NR_newfstatat + SC_DENY(__NR_newfstatat, EACCES), +#endif +#ifdef __NR_stat + SC_DENY(__NR_stat, EACCES), +#endif +#ifdef __NR_stat64 + SC_DENY(__NR_stat64, EACCES), +#endif +#ifdef __NR_shmget + SC_DENY(__NR_shmget, EACCES), +#endif +#ifdef __NR_shmat + SC_DENY(__NR_shmat, EACCES), +#endif +#ifdef __NR_shmdt + SC_DENY(__NR_shmdt, EACCES), +#endif +#ifdef __NR_ipc + SC_DENY(__NR_ipc, EACCES), +#endif +#ifdef __NR_statx + SC_DENY(__NR_statx, EACCES), +#endif - /* Allow syscalls needed for the test to function */ -#ifdef __NR_read - SC_ALLOW(__NR_read), + /* Syscalls to permit */ +#ifdef __NR_brk + SC_ALLOW(__NR_brk), #endif -#ifdef __NR_write - SC_ALLOW(__NR_write), +#ifdef __NR_clock_gettime + SC_ALLOW(__NR_clock_gettime), +#endif +#ifdef __NR_clock_gettime64 + SC_ALLOW(__NR_clock_gettime64), #endif #ifdef __NR_close SC_ALLOW(__NR_close), #endif -#ifdef __NR_exit_group - SC_ALLOW(__NR_exit_group), -#endif #ifdef __NR_exit SC_ALLOW(__NR_exit), #endif -#ifdef __NR_brk - SC_ALLOW(__NR_brk), +#ifdef __NR_exit_group + SC_ALLOW(__NR_exit_group), #endif -#ifdef __NR_mmap - SC_ALLOW(__NR_mmap), +#ifdef __NR_futex + SC_FUTEX(__NR_futex), #endif -#ifdef __NR_munmap - SC_ALLOW(__NR_munmap), +#ifdef __NR_futex_time64 + SC_FUTEX(__NR_futex_time64), #endif -#ifdef __NR_mprotect - SC_ALLOW(__NR_mprotect), +#ifdef __NR_geteuid + SC_ALLOW(__NR_geteuid), #endif -#ifdef __NR_futex - SC_ALLOW(__NR_futex), +#ifdef __NR_geteuid32 + SC_ALLOW(__NR_geteuid32), #endif -#ifdef __NR_getrandom - SC_ALLOW(__NR_getrandom), +#ifdef __NR_getpgid + SC_ALLOW(__NR_getpgid), #endif #ifdef __NR_getpid SC_ALLOW(__NR_getpid), #endif +#ifdef __NR_getrandom + SC_ALLOW(__NR_getrandom), +#endif #ifdef __NR_gettid SC_ALLOW(__NR_gettid), #endif -#ifdef __NR_rt_sigprocmask - SC_ALLOW(__NR_rt_sigprocmask), +#ifdef __NR_gettimeofday + SC_ALLOW(__NR_gettimeofday), #endif -#ifdef __NR_rt_sigaction - SC_ALLOW(__NR_rt_sigaction), +#ifdef __NR_getuid + SC_ALLOW(__NR_getuid), #endif -#ifdef __NR_clock_gettime - SC_ALLOW(__NR_clock_gettime), +#ifdef __NR_getuid32 + SC_ALLOW(__NR_getuid32), #endif -#ifdef __NR_nanosleep - SC_ALLOW(__NR_nanosleep), +#ifdef __NR_madvise + SC_ALLOW_ARG(__NR_madvise, 2, MADV_NORMAL), +# ifdef MADV_FREE + SC_ALLOW_ARG(__NR_madvise, 2, MADV_FREE), +# endif +# ifdef MADV_DONTNEED + SC_ALLOW_ARG(__NR_madvise, 2, MADV_DONTNEED), +# endif +# ifdef MADV_DONTFORK + SC_ALLOW_ARG(__NR_madvise, 2, MADV_DONTFORK), +# endif +# ifdef MADV_DONTDUMP + SC_ALLOW_ARG(__NR_madvise, 2, MADV_DONTDUMP), +# endif +# ifdef MADV_WIPEONFORK + SC_ALLOW_ARG(__NR_madvise, 2, MADV_WIPEONFORK), +# endif + SC_DENY(__NR_madvise, EINVAL), +#endif +#ifdef __NR_mmap + SC_MMAP(__NR_mmap), +#endif +#ifdef __NR_mmap2 + SC_MMAP(__NR_mmap2), #endif -#ifdef __NR_sched_yield - SC_ALLOW(__NR_sched_yield), +#ifdef __NR_mprotect + SC_ALLOW_ARG_MASK(__NR_mprotect, 2, PROT_READ|PROT_WRITE|PROT_NONE), #endif #ifdef __NR_mremap SC_ALLOW(__NR_mremap), #endif -#ifdef __NR_madvise - SC_ALLOW(__NR_madvise), +#ifdef __NR_munmap + SC_ALLOW(__NR_munmap), +#endif +#ifdef __NR_nanosleep + SC_ALLOW(__NR_nanosleep), +#endif +#ifdef __NR_clock_nanosleep + SC_ALLOW(__NR_clock_nanosleep), +#endif +#ifdef __NR_clock_nanosleep_time64 + SC_ALLOW(__NR_clock_nanosleep_time64), +#endif +#ifdef __NR__newselect + SC_ALLOW(__NR__newselect), +#endif +#ifdef __NR_ppoll + SC_ALLOW(__NR_ppoll), +#endif +#ifdef __NR_ppoll_time64 + SC_ALLOW(__NR_ppoll_time64), +#endif +#ifdef __NR_poll + SC_ALLOW(__NR_poll), +#endif +#ifdef __NR_pselect6 + SC_ALLOW(__NR_pselect6), +#endif +#ifdef __NR_pselect6_time64 + SC_ALLOW(__NR_pselect6_time64), +#endif +#ifdef __NR_read + SC_ALLOW(__NR_read), +#endif +#ifdef __NR_rt_sigprocmask + SC_ALLOW(__NR_rt_sigprocmask), +#endif +#ifdef __NR_select + SC_ALLOW(__NR_select), +#endif +#ifdef __NR_shutdown + SC_ALLOW(__NR_shutdown), +#endif +#ifdef __NR_sigprocmask + SC_ALLOW(__NR_sigprocmask), +#endif +#ifdef __NR_time + SC_ALLOW(__NR_time), +#endif +#ifdef __NR_write + SC_ALLOW(__NR_write), +#endif +#ifdef __NR_writev + SC_ALLOW(__NR_writev), +#endif +#ifdef __NR_socketcall + SC_ALLOW_ARG(__NR_socketcall, 0, SYS_SHUTDOWN), + SC_DENY(__NR_socketcall, EACCES), +#endif +#if defined(__NR_ioctl) && defined(__s390__) + /* Allow ioctls for ICA crypto card on s390 */ + SC_ALLOW_ARG(__NR_ioctl, 1, Z90STAT_STATUS_MASK), + SC_ALLOW_ARG(__NR_ioctl, 1, ICARSAMODEXPO), + SC_ALLOW_ARG(__NR_ioctl, 1, ICARSACRT), + SC_ALLOW_ARG(__NR_ioctl, 1, ZSECSENDCPRB), + /* Allow ioctls for EP11 crypto card on s390 */ + SC_ALLOW_ARG(__NR_ioctl, 1, ZSENDEP11CPRB), +#endif +#if defined(__x86_64__) && defined(__ILP32__) && defined(__X32_SYSCALL_BIT) + /* + * On Linux x32, the clock_gettime VDSO falls back to the + * x86-64 syscall under some circumstances, e.g. + * https://bugs.debian.org/849923 + */ + SC_ALLOW(__NR_clock_gettime & ~__X32_SYSCALL_BIT), #endif - /* Default: allow other syscalls (we only want to block file opens) */ - BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_ALLOW), + /* Default deny */ + BPF_STMT(BPF_RET+BPF_K, SECCOMP_FILTER_FAIL), }; -static const struct sock_fprog naomi_program = { - .len = (unsigned short)(sizeof(naomi_insns) / sizeof(naomi_insns[0])), - .filter = (struct sock_filter *)naomi_insns, +static const struct sock_fprog preauth_program = { + .len = (unsigned short)(sizeof(preauth_insns) / sizeof(preauth_insns[0])), + .filter = (struct sock_filter *)preauth_insns, }; /* - * Apply seccomp sandbox restrictions mimicking OpenSSH behavior. + * Apply seccomp sandbox restrictions matching OpenSSH preauth behavior. * Returns 0 on success, -1 on failure. */ static int apply_seccomp_sandbox(void) @@ -202,8 +446,6 @@ static int apply_seccomp_sandbox(void) /* * Set resource limits like OpenSSH does. * RLIMIT_NOFILE = 1 allows existing fds but prevents new ones. - * Note: OpenSSH uses 1, not 0, because poll() fails with EINVAL - * if npfds > RLIMIT_NOFILE. */ struct rlimit rl_zero = {0, 0}; struct rlimit rl_one = {1, 1}; @@ -218,13 +460,12 @@ static int apply_seccomp_sandbox(void) return -1; } - /* Apply seccomp filter - must set NO_NEW_PRIVS first */ if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) == -1) { PRINT_ERR_MSG("prctl(PR_SET_NO_NEW_PRIVS) failed: %s", strerror(errno)); return -1; } - if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &naomi_program) == -1) { + if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &preauth_program) == -1) { PRINT_ERR_MSG("prctl(PR_SET_SECCOMP) failed: %s", strerror(errno)); return -1; } @@ -232,8 +473,464 @@ static int apply_seccomp_sandbox(void) return 0; } +static int load_provider_into_ctx(OSSL_LIB_CTX *ctx, const char *providerDir, + const char *providerName, OSSL_PROVIDER **provider) +{ + int err = 0; + + if (OSSL_PROVIDER_set_default_search_path(ctx, providerDir) != 1) { + PRINT_ERR_MSG("OSSL_PROVIDER_set_default_search_path failed: %s", + providerDir); + err = 1; + } + + if (err == 0) { + *provider = OSSL_PROVIDER_load(ctx, providerName); + if (*provider == NULL) { + PRINT_ERR_MSG("Failed to load provider %s from %s", providerName, + providerDir); + err = 1; + } + } + + return err; +} + +/* Exit codes reported by seccomp helper child processes. */ +#define WP_SECCOMP_CHILD_FILTER_ERR 4 +#define WP_SECCOMP_CHILD_OPENSSL_RAND_ERR 20 +#define WP_SECCOMP_CHILD_WC_INIT_ERR 21 +#define WP_SECCOMP_CHILD_WC_RAND_ERR 22 + +static int child_exit_under_filter(void) +{ + if (apply_seccomp_sandbox() != 0) { + _exit(WP_SECCOMP_CHILD_FILTER_ERR); + } + + /* libc exit() is required here; _exit() skips glibc _IO_cleanup. */ + exit(0); +} + +static int run_exit_cleanup_case(const char *name) +{ + pid_t pid; + int status; + int err = 0; + + pid = fork(); + if (pid == -1) { + PRINT_ERR_MSG("%s: fork() failed: %s", name, strerror(errno)); + return 1; + } + + if (pid == 0) { + return child_exit_under_filter(); + } + + if (waitpid(pid, &status, 0) == -1) { + PRINT_ERR_MSG("%s: waitpid() failed: %s", name, strerror(errno)); + return 1; + } + + if (WIFEXITED(status) && WEXITSTATUS(status) == 0) { + PRINT_MSG("%s: child exited cleanly under OpenSSH preauth filter", + name); + } + else if (WIFSIGNALED(status) && WTERMSIG(status) == SIGSYS) { + PRINT_ERR_MSG("%s: child killed by SIGSYS during libc exit", name); + err = 1; + } + else if (WIFSIGNALED(status)) { + PRINT_ERR_MSG("%s: child killed by unexpected signal %d", name, + WTERMSIG(status)); + err = 1; + } + else if (WIFEXITED(status)) { + PRINT_ERR_MSG("%s: child exited with status %d", name, + WEXITSTATUS(status)); + err = 1; + } + else { + PRINT_ERR_MSG("%s: child exited abnormally", name); + err = 1; + } + + return err; +} + +static int seccomp_helper_single_stream(const char *providerDir, + const char *providerName) +{ + OSSL_LIB_CTX *ctx = NULL; + OSSL_PROVIDER *provider = NULL; + unsigned char buf[32]; + int err = 0; + + /* Entered before unit.c's OpenSSL setup. The customer's preauth child skips + * provider teardown; NO_ATEXIT reproduces that exit-time behavior here. */ + if (OPENSSL_init_crypto(OPENSSL_INIT_NO_ATEXIT, NULL) != 1) { + PRINT_ERR_MSG("OPENSSL_init_crypto(NO_ATEXIT) failed"); + err = 1; + } + + if (err == 0) { + ctx = OSSL_LIB_CTX_new(); + if (ctx == NULL) { + PRINT_ERR_MSG("OSSL_LIB_CTX_new failed"); + err = 1; + } + } + + if (err == 0) { + err = load_provider_into_ctx(ctx, providerDir, providerName, &provider); + } + + if (err == 0 && RAND_bytes_ex(ctx, buf, sizeof(buf), 0) != 1) { + PRINT_ERR_MSG("single-context RAND_bytes_ex failed"); + err = 1; + } + + /* T1: one SEED-SRC read, no teardown, so the fd is still open at exit. */ + if (err == 0) { + err = run_exit_cleanup_case("T1 single-context SEED-SRC"); + } + + OSSL_PROVIDER_unload(provider); + OSSL_LIB_CTX_free(ctx); + + return err; +} + +static int seccomp_helper_leak_route(const char *providerDir, + const char *providerName) +{ + OSSL_LIB_CTX *ctx2 = NULL; + OSSL_PROVIDER *provider1 = NULL; + OSSL_PROVIDER *provider2 = NULL; + unsigned char buf[32]; + int err = 0; + + if (load_provider_into_ctx(NULL, providerDir, providerName, &provider1) + != 0) { + err = 1; + } + + if (err == 0 && RAND_bytes(buf, sizeof(buf)) != 1) { + PRINT_ERR_MSG("default-context RAND_bytes failed"); + err = 1; + } + + if (err == 0) { + ctx2 = OSSL_LIB_CTX_new(); + if (ctx2 == NULL) { + PRINT_ERR_MSG("second OSSL_LIB_CTX_new failed"); + err = 1; + } + } + + if (err == 0) { + err = load_provider_into_ctx(ctx2, providerDir, providerName, + &provider2); + } + + if (err == 0 && RAND_bytes_ex(ctx2, buf, sizeof(buf), 0) != 1) { + PRINT_ERR_MSG("second-context RAND_bytes_ex failed"); + err = 1; + } + + /* T2: a second provider init shares the same fd via the refcount rather + * than orphaning a buffered stream for exit-time cleanup to rewind. */ + if (err == 0) { + err = run_exit_cleanup_case("T2 second-context SEED-SRC re-init"); + } + + OSSL_PROVIDER_unload(provider2); + OSSL_LIB_CTX_free(ctx2); + OSSL_PROVIDER_unload(provider1); + + return err; +} + +static int child_rand_under_filter(OSSL_LIB_CTX *ctx) +{ + EVP_RAND_CTX *rctx; + WC_RNG rng; + unsigned char buf[32]; + int rngInit = 0; + int err = 0; + + if (apply_seccomp_sandbox() != 0) { + return WP_SECCOMP_CHILD_FILTER_ERR; + } + + /* Force fresh entropy after the sandbox is installed; a cached public DRBG + * could otherwise hide a teardown that closed the inherited fd. */ + rctx = RAND_get0_public(ctx); + if (rctx == NULL) { + PRINT_ERR_MSG("multi-context RAND_get0_public failed under sandbox"); + err = WP_SECCOMP_CHILD_OPENSSL_RAND_ERR; + } + else if (EVP_RAND_reseed(rctx, 0, NULL, 0, NULL, 0) != 1) { + PRINT_ERR_MSG("multi-context EVP_RAND_reseed failed under sandbox"); + err = WP_SECCOMP_CHILD_OPENSSL_RAND_ERR; + } + + if (err == 0 && RAND_bytes_ex(ctx, buf, sizeof(buf), 0) != 1) { + PRINT_ERR_MSG("multi-context RAND_bytes_ex failed under sandbox"); + err = WP_SECCOMP_CHILD_OPENSSL_RAND_ERR; + } + + /* Also exercise wolfSSL's global seed callback: the second provider init + * resets it before wp_urandom_init(), covering the stale-callback bug. */ + if (err == 0) { + if (wc_InitRng(&rng) != 0) { + PRINT_ERR_MSG("multi-context wc_InitRng failed under sandbox"); + err = WP_SECCOMP_CHILD_WC_INIT_ERR; + } + else { + rngInit = 1; + } + } + if (err == 0 && wc_RNG_GenerateBlock(&rng, buf, sizeof(buf)) != 0) { + PRINT_ERR_MSG( + "multi-context wc_RNG_GenerateBlock failed under sandbox"); + err = WP_SECCOMP_CHILD_WC_RAND_ERR; + } + if (rngInit) { + wc_FreeRng(&rng); + } + + return err; +} + +static int run_multi_context_survivor_child(OSSL_LIB_CTX *ctx, + const char *name) +{ + pid_t pid; + int status; + + pid = fork(); + if (pid == -1) { + PRINT_ERR_MSG("%s: fork() failed: %s", name, strerror(errno)); + return 1; + } + + if (pid == 0) { + _exit(child_rand_under_filter(ctx)); + } + + if (waitpid(pid, &status, 0) == -1) { + PRINT_ERR_MSG("%s: waitpid() failed: %s", name, strerror(errno)); + return 1; + } + + if (WIFEXITED(status) && WEXITSTATUS(status) == 0) { + PRINT_MSG("%s: child obtained entropy under OpenSSH preauth filter", + name); + return 0; + } + if (WIFEXITED(status)) { + int exitCode = WEXITSTATUS(status); + + if (exitCode == WP_SECCOMP_CHILD_OPENSSL_RAND_ERR) { + PRINT_ERR_MSG("%s: child failed OpenSSL RAND reseed under " + "sandbox (exit code %d)", name, exitCode); + } + else if (exitCode == WP_SECCOMP_CHILD_WC_INIT_ERR) { + PRINT_ERR_MSG("%s: child failed wolfCrypt RNG init under " + "sandbox (exit code %d)", name, exitCode); + } + else if (exitCode == WP_SECCOMP_CHILD_WC_RAND_ERR) { + PRINT_ERR_MSG("%s: child failed wolfCrypt RNG generate under " + "sandbox (exit code %d)", name, exitCode); + } + else { + PRINT_ERR_MSG("%s: child failed under sandbox (exit code %d)", + name, exitCode); + } + return 1; + } + if (WIFSIGNALED(status)) { + PRINT_ERR_MSG("%s: child killed by signal %d", name, + WTERMSIG(status)); + return 1; + } + + PRINT_ERR_MSG("%s: child exited abnormally", name); + return 1; +} + +static int seccomp_helper_multi_context_lifecycle(const char *providerDir, + const char *providerName) +{ + OSSL_LIB_CTX *ctx1 = NULL; + OSSL_LIB_CTX *ctx2 = NULL; + OSSL_PROVIDER *provider1 = NULL; + OSSL_PROVIDER *provider2 = NULL; + unsigned char buf[32]; + int err = 0; + + ctx1 = OSSL_LIB_CTX_new(); + if (ctx1 == NULL) { + PRINT_ERR_MSG("first OSSL_LIB_CTX_new failed"); + err = 1; + } + + if (err == 0) { + err = load_provider_into_ctx(ctx1, providerDir, providerName, + &provider1); + } + + if (err == 0 && RAND_bytes_ex(ctx1, buf, sizeof(buf), 0) != 1) { + PRINT_ERR_MSG("first-context RAND_bytes_ex failed"); + err = 1; + } + + if (err == 0) { + ctx2 = OSSL_LIB_CTX_new(); + if (ctx2 == NULL) { + PRINT_ERR_MSG("second OSSL_LIB_CTX_new failed"); + err = 1; + } + } + + if (err == 0) { + err = load_provider_into_ctx(ctx2, providerDir, providerName, + &provider2); + } + + if (err == 0 && RAND_bytes_ex(ctx2, buf, sizeof(buf), 0) != 1) { + PRINT_ERR_MSG("second-context RAND_bytes_ex failed"); + err = 1; + } + + /* + * Unload only the first provider/libctx. The second context is still live, + * so its child must be able to reseed under OpenSSH's filter using the + * inherited urandom fd and the wolfSSL seed callback. + */ + if (provider1 != NULL) { + OSSL_PROVIDER_unload(provider1); + provider1 = NULL; + } + OSSL_LIB_CTX_free(ctx1); + ctx1 = NULL; + + if (err == 0) { + err = run_multi_context_survivor_child(ctx2, + "T3 multi-context survivor after one unload"); + } + + OSSL_PROVIDER_unload(provider2); + OSSL_LIB_CTX_free(ctx2); + + return err; +} + /* - * Child process test function. + * Re-exec entry point for the seccomp helper child: dispatches to the + * single/leak/multi sub-case by mode. Returns 0 on success, non-zero on + * failure (2 on bad arguments or unknown mode). + */ +int test_seccomp_sandbox_helper(const char *mode, const char *providerDir, + const char *providerName) +{ + int err; + + if (mode == NULL || providerDir == NULL || providerName == NULL) { + PRINT_ERR_MSG("seccomp helper missing arguments"); + return 2; + } + + if (strcmp(mode, "single") == 0) { + err = seccomp_helper_single_stream(providerDir, providerName); + } + else if (strcmp(mode, "leak") == 0) { + err = seccomp_helper_leak_route(providerDir, providerName); + } + else if (strcmp(mode, "multi") == 0) { + err = seccomp_helper_multi_context_lifecycle(providerDir, + providerName); + } + else { + PRINT_ERR_MSG("unknown seccomp helper mode: %s", mode); + err = 2; + } + + return err; +} + +static int run_seccomp_helper_case(const char *mode, const char *name) +{ + char exePath[PATH_MAX]; + ssize_t exeLen; + pid_t pid; + int status; + int err = 0; + const char *providerDir = wpUnitProviderDir; + const char *providerName = wpUnitProviderName; + + if (providerDir == NULL) { + providerDir = ".libs"; + } + if (providerName == NULL) { + providerName = wolfprovider_id; + } + + exeLen = readlink("/proc/self/exe", exePath, sizeof(exePath) - 1); + if (exeLen < 0) { + PRINT_ERR_MSG("%s: readlink(/proc/self/exe) failed: %s", name, + strerror(errno)); + return 1; + } + if (exeLen >= (ssize_t)sizeof(exePath) - 1) { + PRINT_ERR_MSG("%s: /proc/self/exe path too long", name); + return 1; + } + exePath[exeLen] = '\0'; + + pid = fork(); + if (pid == -1) { + PRINT_ERR_MSG("%s: fork() failed: %s", name, strerror(errno)); + return 1; + } + + if (pid == 0) { + execl(exePath, exePath, "--seccomp-sandbox-helper", mode, providerDir, + providerName, (char *)NULL); + PRINT_ERR_MSG("%s: execl helper failed: %s", name, strerror(errno)); + _exit(127); + } + + if (waitpid(pid, &status, 0) == -1) { + PRINT_ERR_MSG("%s: waitpid(helper) failed: %s", name, strerror(errno)); + return 1; + } + + if (WIFEXITED(status) && WEXITSTATUS(status) == 0) { + PRINT_MSG("%s: PASSED", name); + } + else if (WIFEXITED(status)) { + PRINT_ERR_MSG("%s: FAILED, helper exited with status %d", name, + WEXITSTATUS(status)); + err = 1; + } + else if (WIFSIGNALED(status)) { + PRINT_ERR_MSG("%s: helper killed by signal %d", name, + WTERMSIG(status)); + err = 1; + } + else { + PRINT_ERR_MSG("%s: helper exited abnormally", name); + err = 1; + } + + return err; +} + +/* + * Child process regression test function. * Applies sandbox and attempts RAND_bytes operations. * Returns 0 on success, non-zero on failure. */ @@ -253,24 +950,18 @@ static int child_test_rand_under_sandbox(OSSL_LIB_CTX *libCtx) return 1; } - /* - * Now try to generate random bytes under sandbox. - * This is the critical test - the DRBG may need to reseed, - * but it cannot open /dev/urandom because openat() is blocked. - */ + /* Generate random bytes under the real preauth filter. The caller uses + * _exit(), so this stays a fork-safety guard without libc stdio cleanup. */ if (RAND_bytes(buf, sizeof(buf)) != 1) { PRINT_ERR_MSG("RAND_bytes failed under sandbox"); err = 1; } - /* Try a second call to potentially trigger reseed */ if (err == 0 && RAND_bytes(buf, sizeof(buf)) != 1) { PRINT_ERR_MSG("Second RAND_bytes failed under sandbox"); err = 1; } - /* Reseed under the sandbox with NULL entropy: drives the fresh-entropy - * path, which must use the cached SEED-SRC fd, not open() /dev/urandom. */ if (err == 0) { EVP_RAND_CTX *rctx = RAND_get0_public(libCtx); if (rctx == NULL) { @@ -283,7 +974,6 @@ static int child_test_rand_under_sandbox(OSSL_LIB_CTX *libCtx) } } - /* Confirm the DRBG is still usable after the sandboxed reseed. */ if (err == 0 && RAND_bytes(buf, sizeof(buf)) != 1) { PRINT_ERR_MSG("RAND_bytes after reseed failed under sandbox"); err = 1; @@ -294,7 +984,7 @@ static int child_test_rand_under_sandbox(OSSL_LIB_CTX *libCtx) } /* - * Run the fork+sandbox test for a given library context. + * Run the fork+sandbox regression test for a given library context. * Returns 0 on success, non-zero on failure. */ static int run_fork_sandbox_test(OSSL_LIB_CTX *libCtx, const char *provName) @@ -306,7 +996,6 @@ static int run_fork_sandbox_test(OSSL_LIB_CTX *libCtx, const char *provName) PRINT_MSG("Testing %s provider with fork+sandbox", provName); - /* Pre-fork: Initialize DRBG by generating some random bytes */ origCtx = OSSL_LIB_CTX_set0_default(libCtx); if (RAND_bytes(buf, sizeof(buf)) != 1) { PRINT_ERR_MSG("Pre-fork RAND_bytes failed for %s", provName); @@ -315,9 +1004,6 @@ static int run_fork_sandbox_test(OSSL_LIB_CTX *libCtx, const char *provName) } OSSL_LIB_CTX_set0_default(origCtx); - PRINT_MSG("Pre-fork RAND_bytes succeeded, forking child..."); - - /* Fork child process - mimics OpenSSH privsep_preauth() */ pid = fork(); if (pid == -1) { PRINT_ERR_MSG("fork() failed: %s", strerror(errno)); @@ -325,24 +1011,12 @@ static int run_fork_sandbox_test(OSSL_LIB_CTX *libCtx, const char *provName) } if (pid == 0) { - /* Child process */ int child_err; - /* - * Note: In OpenSSH, reseed_prngs() is called here BEFORE sandbox. - * We intentionally skip that step to test the failure case. - * Once wolfProvider has proper fork-safe DRBG, the reseed - * should happen automatically or the DRBG should work without - * needing to open /dev/urandom. - */ - child_err = child_test_rand_under_sandbox(libCtx); - - /* Exit with status indicating success (0) or failure (1) */ _exit(child_err); } - /* Parent process - wait for child */ if (waitpid(pid, &status, 0) == -1) { PRINT_ERR_MSG("waitpid() failed: %s", strerror(errno)); return 1; @@ -354,15 +1028,14 @@ static int run_fork_sandbox_test(OSSL_LIB_CTX *libCtx, const char *provName) PRINT_MSG("%s: Child succeeded under sandbox", provName); return 0; } - else { - PRINT_MSG("%s: Child failed under sandbox (exit code %d)", - provName, exit_code); - return 1; - } + + PRINT_ERR_MSG("%s: Child failed under sandbox (exit code %d)", + provName, exit_code); + return 1; } else if (WIFSIGNALED(status)) { PRINT_ERR_MSG("%s: Child killed by signal %d", provName, - WTERMSIG(status)); + WTERMSIG(status)); return 1; } @@ -370,67 +1043,57 @@ static int run_fork_sandbox_test(OSSL_LIB_CTX *libCtx, const char *provName) return 1; } -/* - * Main test function for seccomp sandbox DRBG behavior. - * - * This test verifies: - * 1. Default OpenSSL provider works under fork+sandbox (baseline) - * 2. wolfProvider behavior under fork+sandbox - * - * Currently wolfProvider is EXPECTED TO FAIL because the DRBG - * tries to open /dev/urandom after fork, which is blocked by seccomp. - */ int test_seccomp_sandbox(void *data) { int err = 0; - int wp_err; (void)data; - PRINT_MSG("=== Seccomp Sandbox DRBG Test ==="); - PRINT_MSG("This test mimics OpenSSH's fork+sandbox behavior"); + PRINT_MSG("=== OpenSSH seccomp sandbox SEED-SRC test ==="); - /* - * Test 1: Default OpenSSL provider (baseline - should pass) - * This verifies our test harness works correctly. - */ - PRINT_MSG(""); - PRINT_MSG("--- Test with OpenSSL default provider (baseline) ---"); - err = run_fork_sandbox_test(osslLibCtx, "OpenSSL default"); - if (err != 0) { - PRINT_ERR_MSG("BASELINE FAILED: OpenSSL default provider failed"); - PRINT_ERR_MSG("This indicates a problem with the test itself"); - return err; + if (run_seccomp_helper_case("single", + "T1 customer-faithful single SEED-SRC read") != 0) { + err = 1; } - PRINT_MSG("OpenSSL default provider: PASSED (baseline verified)"); - /* - * Test 2: wolfProvider - * Currently expected to fail because DRBG tries to open /dev/urandom - * after fork, which is blocked by seccomp. - */ - PRINT_MSG(""); - PRINT_MSG("--- Test with wolfProvider ---"); - wp_err = run_fork_sandbox_test(wpLibCtx, "wolfProvider"); + if (run_seccomp_helper_case("leak", + "T2 second-libctx SEED-SRC re-init") != 0) { + err = 1; + } - if (wp_err != 0) { - PRINT_MSG("wolfProvider: FAILED (expected - DRBG cannot reseed)"); - PRINT_MSG("This is expected until fork-safe DRBG is implemented"); - /* - * Return the error so the test is marked as failed. - * Once the fix is implemented, this test should pass. - */ - return wp_err; + if (run_seccomp_helper_case("multi", + "T3 multi-context survivor after one unload") != 0) { + err = 1; } - PRINT_MSG("wolfProvider: PASSED"); - PRINT_MSG("=== All seccomp sandbox tests passed ==="); + PRINT_MSG("--- Regression: child reads entropy under sandbox ---"); + if (run_fork_sandbox_test(osslLibCtx, "OpenSSL default") != 0) { + err = 1; + } + if (run_fork_sandbox_test(wpLibCtx, "wolfProvider") != 0) { + err = 1; + } - return 0; + if (err == 0) { + PRINT_MSG("=== All seccomp sandbox tests passed ==="); + } + + return err; } #else /* !WP_HAVE_SECCOMP */ +int test_seccomp_sandbox_helper(const char *mode, const char *providerDir, + const char *providerName) +{ + (void)mode; + (void)providerDir; + (void)providerName; + + PRINT_MSG("Seccomp sandbox helper skipped - seccomp not available"); + return 1; +} + int test_seccomp_sandbox(void *data) { (void)data; @@ -451,4 +1114,3 @@ int test_seccomp_sandbox(void *data) } #endif /* WP_TEST_SECCOMP_SANDBOX && WP_HAVE_SEED_SRC && WP_HAVE_RANDOM */ - diff --git a/test/unit.c b/test/unit.c index 95f22b5f..7188c8a5 100644 --- a/test/unit.c +++ b/test/unit.c @@ -30,6 +30,15 @@ OSSL_LIB_CTX* wpLibCtx = NULL; OSSL_LIB_CTX* osslLibCtx = NULL; int noKeyLimits = 0; +#if defined(WP_HAVE_SEED_SRC) && defined(WP_HAVE_RANDOM) +/* + * Provider dir/name from the command line, so tests that load wolfProvider into + * their own library contexts (SEED-SRC refcount, seccomp helpers) pick up the + * same --dir/--provider location as the main suite. + */ +const char *wpUnitProviderDir = ".libs"; +const char *wpUnitProviderName = NULL; +#endif #ifdef WOLFPROV_DEBUG void print_buffer(const char *desc, const unsigned char *buffer, size_t len) @@ -303,6 +312,8 @@ TEST_CASE test_case[] = { #endif TEST_DECL(test_rand_seed, NULL), TEST_DECL(test_drbg_reseed, NULL), + TEST_DECL(test_seed_src_refcount, NULL), + TEST_DECL(test_seed_src_reload, NULL), TEST_DECL(test_seccomp_sandbox, NULL), #ifdef WP_HAVE_DH TEST_DECL(test_dh_pgen_pkey, NULL), @@ -557,6 +568,18 @@ static void usage(void) printf(" --valgrind Run wolfSSL only tests for Valgrind where OpenSSL " "has issues\n"); printf(" Run this test case, but not all\n"); +#if defined(WP_TEST_SECCOMP_SANDBOX) && defined(WP_HAVE_SEED_SRC) && \ + defined(WP_HAVE_RANDOM) + printf(" --seccomp-sandbox-helper [provider-dir] " + "[provider-name]\n"); + printf(" INTERNAL: seccomp re-exec protocol, not for " + "direct use.\n"); +#endif +#if defined(WP_HAVE_SEED_SRC) && defined(WP_HAVE_RANDOM) + printf(" --seed-src-reload-helper [provider-dir] [provider-name]\n"); + printf(" INTERNAL: SEED-SRC reload re-exec protocol, not " + "for direct use.\n"); +#endif } #ifdef TEST_MULTITHREADED @@ -770,6 +793,37 @@ int main(int argc, char* argv[]) int runAll = 1; int runTests = 1; +#if defined(WP_TEST_SECCOMP_SANDBOX) && defined(WP_HAVE_SEED_SRC) && \ + defined(WP_HAVE_RANDOM) + /* + * Internal re-exec protocol: the seccomp test re-execs this binary as + * --seccomp-sandbox-helper [provider-dir] [name] + * to run a helper mode in a fresh process. See test_seccomp_sandbox.c. + */ + if (argc >= 2 && strcmp(argv[1], "--seccomp-sandbox-helper") == 0) { + const char *mode = (argc >= 3) ? argv[2] : NULL; + const char *helperDir = (argc >= 4) ? argv[3] : ".libs"; + const char *helperName = (argc >= 5) ? argv[4] : wolfprovider_id; + + return test_seccomp_sandbox_helper(mode, helperDir, helperName); + } +#endif + +#if defined(WP_HAVE_SEED_SRC) && defined(WP_HAVE_RANDOM) + /* + * Internal re-exec protocol: the SEED-SRC reload test re-execs this binary + * as --seed-src-reload-helper [provider-dir] [name] to exercise the shared + * fd/callback teardown-to-zero and reload in a fresh process (refcount starts + * at zero). See test_rand_seed.c. + */ + if (argc >= 2 && strcmp(argv[1], "--seed-src-reload-helper") == 0) { + wpUnitProviderDir = (argc >= 3) ? argv[2] : ".libs"; + wpUnitProviderName = (argc >= 4) ? argv[3] : wolfprovider_id; + + return test_seed_src_reload_helper(); + } +#endif + for (--argc, ++argv; argc > 0; argc--, argv++) { if (strncmp(*argv, "--help", 6) == 0) { usage(); @@ -855,6 +909,11 @@ int main(int argc, char* argv[]) } } +#if defined(WP_HAVE_SEED_SRC) && defined(WP_HAVE_RANDOM) + wpUnitProviderDir = dir; + wpUnitProviderName = name; +#endif + OpenSSL_add_all_ciphers(); OpenSSL_add_all_digests(); diff --git a/test/unit.h b/test/unit.h index e263dfd0..5eff4ff3 100644 --- a/test/unit.h +++ b/test/unit.h @@ -96,6 +96,26 @@ extern OSSL_LIB_CTX* wpLibCtx; extern OSSL_LIB_CTX* osslLibCtx; extern int noKeyLimits; +#if defined(WP_HAVE_SEED_SRC) && defined(WP_HAVE_RANDOM) +/* + * Provider location for the SEED-SRC tests, from --dir/--provider (and forwarded + * to re-exec'd helpers so a fresh helper loads the same provider). + */ +extern const char *wpUnitProviderDir; +extern const char *wpUnitProviderName; +/* + * Fresh-process worker for the SEED-SRC teardown -> reload test, re-exec'd by + * test_seed_src_reload() so the refcount starts from zero. + */ +int test_seed_src_reload_helper(void); +#endif + +#if defined(WP_TEST_SECCOMP_SANDBOX) && defined(WP_HAVE_SEED_SRC) && \ + defined(WP_HAVE_RANDOM) +int test_seccomp_sandbox_helper(const char *mode, const char *providerDir, + const char *providerName); +#endif + #ifdef WP_HAVE_DIGEST @@ -248,6 +268,8 @@ int test_random(void *data); /* DRBG SEED-SRC hierarchy tests */ int test_rand_seed(void *data); int test_drbg_reseed(void *data); +int test_seed_src_refcount(void *data); +int test_seed_src_reload(void *data); /* Seccomp sandbox test - mimics OpenSSH fork+sandbox behavior */ int test_seccomp_sandbox(void *data);