/* * Copyright 2024-2025 The OpenSSL Project Authors. All Rights Reserved. * * Licensed under the Apache License 2.0 (the "License"). You may not use * this file except in compliance with the License. You can obtain a copy * in the file LICENSE in the source distribution or at * https://www.openssl.org/source/license.html */ #include #include #include #include #include #include "../testutil.h" #include "internal/numbers.h" /* UINT64_C */ #include "internal/time.h" /* OSSL_TIME */ static const char *cert_file, *key_file; /* * TERP - Test Executive Script Interpreter * ======================================== */ typedef struct gen_ctx_st GEN_CTX; typedef void (*script_gen_t)(GEN_CTX *ctx); typedef struct script_info_st { /* name: A symbolic name, like simple_conn. */ const char *name; /* desc: A short, one-line description. */ const char *desc; const char *file; int line; /* gen_func: The script generation function. */ script_gen_t gen_func; } SCRIPT_INFO; struct gen_ctx_st { SCRIPT_INFO *script_info; const char *cur_file; int error, cur_line; const char *first_error_msg, *first_error_file; int first_error_line; uint8_t *build_buf_beg, *build_buf_cur, *build_buf_end; }; static int GEN_CTX_init(GEN_CTX *ctx, SCRIPT_INFO *script_info) { ctx->script_info = script_info; ctx->error = 0; ctx->cur_file = NULL; ctx->cur_line = 0; ctx->first_error_msg = NULL; ctx->first_error_line = 0; ctx->build_buf_beg = NULL; ctx->build_buf_cur = NULL; ctx->build_buf_end = NULL; return 1; } static void GEN_CTX_cleanup(GEN_CTX *ctx) { OPENSSL_free(ctx->build_buf_beg); ctx->build_buf_beg = ctx->build_buf_cur = ctx->build_buf_end = NULL; } typedef struct terp_st TERP; #define F_RET_SPIN_AGAIN 2 #define F_RET_SKIP_REST 3 #define F_SPIN_AGAIN() \ do { \ ok = F_RET_SPIN_AGAIN; \ fctx->spin_again = 1; \ goto err; \ } while (0) #define F_SKIP_REST() \ do { \ ok = F_RET_SKIP_REST; \ fctx->skip_rest = 1; \ goto err; \ } while (0) typedef struct func_ctx_st { TERP *terp; /* * Set to 1 inside a user function if the function should spin again. * Cleared automatically after the user function returns. */ int spin_again; /* * Immediately exit script successfully. Useful for skipping. */ int skip_rest; } FUNC_CTX; static ossl_inline int TERP_stk_pop(TERP *terp, void *buf, size_t buf_len); #define TERP_STK_PUSH(terp, v) \ do { \ if (!TEST_true(TERP_stk_push((terp), &(v), sizeof(v)))) \ goto err; \ } while (0) #define TERP_STK_POP(terp, v) \ do { \ memset(&(v), 0, sizeof(v)); /* quiet warnings */ \ if (!TEST_true(TERP_stk_pop((terp), &(v), sizeof(v)))) \ goto err; \ } while (0) #define TERP_STK_POP2(terp, a, b) \ do { \ TERP_STK_POP((terp), (b)); \ TERP_STK_POP((terp), (a)); \ } while (0) #define F_PUSH(v) TERP_STK_PUSH(fctx->terp, (v)) #define F_POP(v) TERP_STK_POP (fctx->terp, (v)) #define F_POP2(a, b) TERP_STK_POP2(fctx->terp, (a), (b)) typedef int (*helper_func_t)(FUNC_CTX *fctx); #define DEF_FUNC(name) ossl_unused static int name(FUNC_CTX *fctx) #define DEF_SCRIPT(name, desc) \ static void script_gen_##name(GEN_CTX *ctx); \ static SCRIPT_INFO script_info_##name = { \ #name, desc, __FILE__, __LINE__, \ script_gen_##name \ }; \ static void script_gen_##name(GEN_CTX *ctx) enum { OPK_INVALID, OPK_END, OPK_PUSH_P, /* * This is exactly like PUSH_P, but the script dumper knows the pointer * points to a static NUL-terminated string and can therefore print it. */ OPK_PUSH_PZ, OPK_PUSH_U64, /* * Could use OPK_PUSH_U64 for this but it's annoying to have to avoid using * size_t in case it is a different size. */ OPK_PUSH_SIZE, OPK_FUNC, OPK_LABEL }; static void *openc_alloc_space(GEN_CTX *ctx, size_t num_bytes); #define DEF_ENCODER(name, type) \ static void name(GEN_CTX *ctx, type v) \ { \ void *dst = openc_alloc_space(ctx, sizeof(v)); \ if (dst == NULL) \ return; \ \ memcpy(dst, &v, sizeof(v)); \ } DEF_ENCODER(openc_u64, uint64_t) DEF_ENCODER(openc_size, size_t) DEF_ENCODER(openc_p, void *) DEF_ENCODER(openc_fp, helper_func_t) #define openc_opcode openc_u64 static void opgen_END(GEN_CTX *ctx) { openc_opcode(ctx, OPK_END); } static ossl_unused void opgen_PUSH_P(GEN_CTX *ctx, void *p) { openc_opcode(ctx, OPK_PUSH_P); openc_p(ctx, p); } static void opgen_PUSH_PZ(GEN_CTX *ctx, void *p) { openc_opcode(ctx, OPK_PUSH_PZ); openc_p(ctx, p); } static void opgen_PUSH_U64(GEN_CTX *ctx, uint64_t v) { openc_opcode(ctx, OPK_PUSH_U64); openc_u64(ctx, v); } ossl_unused static void opgen_PUSH_SIZE(GEN_CTX *ctx, size_t v) { openc_opcode(ctx, OPK_PUSH_SIZE); openc_size(ctx, v); } ossl_unused static void opgen_FUNC(GEN_CTX *ctx, helper_func_t f, const char *f_name) { openc_opcode(ctx, OPK_FUNC); openc_fp(ctx, f); openc_p(ctx, (void *)f_name); } ossl_unused static void opgen_LABEL(GEN_CTX *ctx, const char *name) { openc_opcode(ctx, OPK_LABEL); openc_p(ctx, (void *)name); } static void opgen_set_line(GEN_CTX *ctx, const char *file, int line) { ctx->cur_file = file; ctx->cur_line = line; } static ossl_unused void opgen_fail(GEN_CTX *ctx, const char *msg) { if (!ctx->error) { ctx->first_error_file = ctx->cur_file; ctx->first_error_line = ctx->cur_line; ctx->first_error_msg = msg; } ctx->error = 1; } #define OPGEN(n) (opgen_set_line(ctx, __FILE__, __LINE__), \ opgen_##n) #define OP_END() OPGEN(END) (ctx) #define OP_PUSH_P(v) OPGEN(PUSH_P) (ctx, (v)) #define OP_PUSH_PZ(v) OPGEN(PUSH_PZ) (ctx, (v)) #define OP_PUSH_U64(v) OPGEN(PUSH_U64) (ctx, (v)) #define OP_PUSH_SIZE(v) OPGEN(PUSH_SIZE) (ctx, (v)) #define OP_PUSH_BUFP(p, l) (OP_PUSH_P(p), OP_PUSH_SIZE(l)) #define OP_PUSH_BUF(v) OP_PUSH_BUFP(&(v), sizeof(v)) #define OP_PUSH_LREF(v) OPGEN(PUSH_LREF)(ctx, (lref)) #define OP_FUNC(f) OPGEN(FUNC) (ctx, (f), #f) #define OP_LABEL(name) OPGEN(LABEL) (ctx, (name)) #define GEN_FAIL(msg) OPGEN(fail) (ctx, (msg)) static void *openc_alloc_space(GEN_CTX *ctx, size_t num_bytes) { void *p; size_t cur_spare, old_size, new_size, off; cur_spare = ctx->build_buf_end - ctx->build_buf_cur; if (cur_spare < num_bytes) { off = ctx->build_buf_cur - ctx->build_buf_beg; old_size = ctx->build_buf_end - ctx->build_buf_beg; new_size = (old_size == 0) ? 1024 : old_size * 2; p = OPENSSL_realloc(ctx->build_buf_beg, new_size); if (!TEST_ptr(p)) return NULL; ctx->build_buf_beg = p; ctx->build_buf_cur = ctx->build_buf_beg + off; ctx->build_buf_end = ctx->build_buf_beg + new_size; } p = ctx->build_buf_cur; ctx->build_buf_cur += num_bytes; return p; } /* * Script Interpreter * ============================================================================ */ typedef struct gen_script_st { const uint8_t *buf; size_t buf_len; } GEN_SCRIPT; static int GEN_CTX_finish(GEN_CTX *ctx, GEN_SCRIPT *script) { script->buf = ctx->build_buf_beg; script->buf_len = ctx->build_buf_cur - ctx->build_buf_beg; ctx->build_buf_beg = ctx->build_buf_cur = ctx->build_buf_end = NULL; return 1; } static void GEN_SCRIPT_cleanup(GEN_SCRIPT *script) { OPENSSL_free((char *)script->buf); script->buf = NULL; script->buf_len = 0; } static int GEN_SCRIPT_init(GEN_SCRIPT *gen_script, SCRIPT_INFO *script_info) { int ok = 0; GEN_CTX gctx; if (!TEST_true(GEN_CTX_init(&gctx, script_info))) return 0; script_info->gen_func(&gctx); opgen_END(&gctx); if (!TEST_false(gctx.error)) goto err; if (!TEST_true(GEN_CTX_finish(&gctx, gen_script))) goto err; ok = 1; err: if (!ok) { if (gctx.error) TEST_error("script generation failed: %s (at %s:%d)", gctx.first_error_msg, gctx.first_error_file, gctx.first_error_line); GEN_CTX_cleanup(&gctx); } return ok; } typedef struct srdr_st { const uint8_t *beg, *cur, *end, *save_cur; } SRDR; static void SRDR_init(SRDR *rdr, const uint8_t *buf, size_t buf_len) { rdr->beg = rdr->cur = buf; rdr->end = rdr->beg + buf_len; rdr->save_cur = NULL; } static ossl_inline int SRDR_get_operand(SRDR *srdr, void *buf, size_t buf_len) { if (!TEST_size_t_ge(srdr->end - srdr->cur, buf_len)) return 0; /* malformed script */ memcpy(buf, srdr->cur, buf_len); srdr->cur += buf_len; return 1; } static ossl_inline void SRDR_save(SRDR *srdr) { srdr->save_cur = srdr->cur; } static ossl_inline void SRDR_restore(SRDR *srdr) { srdr->cur = srdr->save_cur; } #define GET_OPERAND(srdr, v) \ do { \ memset(&(v), 0, sizeof(v)); /* quiet uninitialized warn */ \ if (!TEST_true(SRDR_get_operand(srdr, &(v), sizeof(v)))) \ goto err; \ } while (0) static void print_opc(BIO *bio, size_t op_num, size_t offset, const char *name) { if (op_num != SIZE_MAX) BIO_printf(bio, "%3zu- %4zx>\t%-8s \t", op_num, offset, name); else BIO_printf(bio, " %4zx>\t%-8s \t", offset, name); } static int SRDR_print_one(SRDR *srdr, BIO *bio, size_t i, int *was_end) { int ok = 0; const uint8_t *opc_start; uint64_t opc; if (was_end != NULL) *was_end = 0; opc_start = srdr->cur; GET_OPERAND(srdr, opc); #define PRINT_OPC(name) print_opc(bio, i, (size_t)(opc_start - srdr->beg), #name) switch (opc) { case OPK_END: PRINT_OPC(END); opc_start = srdr->cur; if (was_end != NULL) *was_end = 1; break; case OPK_PUSH_P: { void *v; GET_OPERAND(srdr, v); PRINT_OPC(PUSH_P); BIO_printf(bio, "%20p", v); } break; case OPK_PUSH_PZ: { void *v; GET_OPERAND(srdr, v); PRINT_OPC(PUSH_PZ); if (v != NULL && strlen((const char *)v) == 1) BIO_printf(bio, "%20p (%s)", v, (const char *)v); else BIO_printf(bio, "%20p (\"%s\")", v, (const char *)v); } break; case OPK_PUSH_U64: { uint64_t v; GET_OPERAND(srdr, v); PRINT_OPC(PUSH_U64); BIO_printf(bio, "%#20llx (%llu)", (unsigned long long)v, (unsigned long long)v); } break; case OPK_PUSH_SIZE: { size_t v; GET_OPERAND(srdr, v); PRINT_OPC(PUSH_SIZE); BIO_printf(bio, "%#20llx (%llu)", (unsigned long long)v, (unsigned long long)v); } break; case OPK_FUNC: { helper_func_t v; void *f_name = NULL, *x = NULL; GET_OPERAND(srdr, v); GET_OPERAND(srdr, f_name); PRINT_OPC(FUNC); memcpy(&x, &v, sizeof(x) < sizeof(v) ? sizeof(x) : sizeof(v)); BIO_printf(bio, "%s", (const char *)f_name); } break; case OPK_LABEL: { void *l_name; GET_OPERAND(srdr, l_name); BIO_printf(bio, "\n%s:\n", (const char *)l_name); PRINT_OPC(LABEL); } break; default: TEST_error("unsupported opcode while printing: %llu", (unsigned long long)opc); goto err; } ok = 1; err: return ok; } static int GEN_SCRIPT_print(GEN_SCRIPT *gen_script, BIO *bio, const SCRIPT_INFO *script_info) { int ok = 0; size_t i; SRDR srdr_v, *srdr = &srdr_v; int was_end = 0; SRDR_init(srdr, gen_script->buf, gen_script->buf_len); if (script_info != NULL) { BIO_printf(bio, "\nGenerated script for '%s':\n", script_info->name); BIO_printf(bio, "\n--GENERATED-------------------------------------" "----------------------\n"); BIO_printf(bio, " # NAME:\n # %s\n", script_info->name); BIO_printf(bio, " # SOURCE:\n # %s:%d\n", script_info->file, script_info->line); BIO_printf(bio, " # DESCRIPTION:\n # %s\n", script_info->desc); } for (i = 0; !was_end; ++i) { BIO_printf(bio, "\n"); if (!TEST_true(SRDR_print_one(srdr, bio, i, &was_end))) goto err; } if (script_info != NULL) { const unsigned char *opc_start = srdr->cur; BIO_printf(bio, "\n"); PRINT_OPC(+++); BIO_printf(bio, "\n------------------------------------------------" "----------------------\n\n"); } ok = 1; err: return ok; } static void SCRIPT_INFO_print(SCRIPT_INFO *script_info, BIO *bio, int error, const char *msg) { if (error) TEST_error("%s: script '%s' (%s)", msg, script_info->name, script_info->desc); else TEST_info("%s: script '%s' (%s)", msg, script_info->name, script_info->desc); } typedef struct terp_config_st { BIO *debug_bio; OSSL_TIME (*now_cb)(void *arg); void *now_cb_arg; int (*per_op_cb)(TERP *terp, void *arg); void *per_op_cb_arg; OSSL_TIME max_execution_time; /* duration */ } TERP_CONFIG; #define TERP_DEFAULT_MAX_EXECUTION_TIME (ossl_ms2time(3000)) struct terp_st { TERP_CONFIG cfg; const SCRIPT_INFO *script_info; const GEN_SCRIPT *gen_script; SRDR srdr; uint8_t *stk_beg, *stk_cur, *stk_end, *stk_save_cur; FUNC_CTX fctx; uint64_t ops_executed; int log_execute; OSSL_TIME start_time, deadline_time; }; static int TERP_init(TERP *terp, const TERP_CONFIG *cfg, const SCRIPT_INFO *script_info, const GEN_SCRIPT *gen_script) { if (!TEST_true(cfg->now_cb != NULL)) return 0; terp->cfg = *cfg; terp->script_info = script_info; terp->gen_script = gen_script; terp->fctx.terp = terp; terp->fctx.spin_again = 0; terp->fctx.skip_rest = 0; terp->stk_beg = NULL; terp->stk_cur = NULL; terp->stk_end = NULL; terp->stk_save_cur = NULL; terp->ops_executed = 0; terp->log_execute = 1; if (ossl_time_is_zero(terp->cfg.max_execution_time)) terp->cfg.max_execution_time = TERP_DEFAULT_MAX_EXECUTION_TIME; return 1; } static void TERP_cleanup(TERP *terp) { if (terp->script_info == NULL) return; OPENSSL_free(terp->stk_beg); terp->stk_beg = terp->stk_cur = terp->stk_end = NULL; terp->script_info = NULL; } static int TERP_stk_ensure_capacity(TERP *terp, size_t spare) { uint8_t *p; size_t old_size, new_size, off; old_size = terp->stk_end - terp->stk_beg; if (old_size >= spare) return 1; off = terp->stk_end - terp->stk_cur; new_size = old_size != 0 ? old_size * 2 : 256; p = OPENSSL_realloc(terp->stk_beg, new_size); if (!TEST_ptr(p)) return 0; terp->stk_beg = p; terp->stk_end = terp->stk_beg + new_size; terp->stk_cur = terp->stk_end - off; return 1; } static ossl_inline int TERP_stk_push(TERP *terp, const void *buf, size_t buf_len) { if (!TEST_true(TERP_stk_ensure_capacity(terp, buf_len))) return 0; terp->stk_cur -= buf_len; memcpy(terp->stk_cur, buf, buf_len); return 1; } static ossl_inline int TERP_stk_pop(TERP *terp, void *buf, size_t buf_len) { if (!TEST_size_t_ge(terp->stk_end - terp->stk_cur, buf_len)) return 0; memcpy(buf, terp->stk_cur, buf_len); terp->stk_cur += buf_len; return 1; } static void TERP_print_stack(TERP *terp, BIO *bio, const char *header) { test_output_memory(header, terp->stk_cur, terp->stk_end - terp->stk_cur); BIO_printf(bio, " (%zu bytes)\n", (size_t)(terp->stk_end - terp->stk_cur)); BIO_printf(bio, "\n"); } #define TERP_GET_OPERAND(v) GET_OPERAND(&terp->srdr, (v)) #define TERP_SPIN_AGAIN() \ do { \ SRDR_restore(&terp->srdr); \ terp->stk_cur = terp->stk_save_cur; \ ++spin_count; \ goto spin_again; \ } while (0) static OSSL_TIME TERP_now(TERP *terp) { return terp->cfg.now_cb(terp->cfg.now_cb_arg); } static void TERP_log_spin(TERP *terp, size_t spin_count) { if (spin_count > 0) BIO_printf(terp->cfg.debug_bio, " \t\t(span %zu times)\n", spin_count); } static int TERP_execute(TERP *terp) { int ok = 0; uint64_t opc; size_t op_num = 0; int in_debug_output = 0; size_t spin_count = 0; BIO *debug_bio = terp->cfg.debug_bio; SRDR_init(&terp->srdr, terp->gen_script->buf, terp->gen_script->buf_len); terp->start_time = TERP_now(terp); terp->deadline_time = ossl_time_add(terp->start_time, terp->cfg.max_execution_time); for (;;) { if (terp->log_execute) { SRDR srdr_copy = terp->srdr; if (!in_debug_output) { BIO_printf(debug_bio, "\n--EXECUTION-----------------------------" "------------------------------\n"); in_debug_output = 1; } TERP_log_spin(terp, spin_count); if (!TEST_true(SRDR_print_one(&srdr_copy, debug_bio, SIZE_MAX, NULL))) goto err; BIO_printf(debug_bio, "\n"); } TERP_GET_OPERAND(opc); ++op_num; SRDR_save(&terp->srdr); terp->stk_save_cur = terp->stk_cur; spin_count = 0; ++terp->ops_executed; spin_again: if (ossl_time_compare(TERP_now(terp), terp->deadline_time) >= 0) { TEST_error("timed out while executing op %zu", op_num); if (terp->log_execute) TERP_log_spin(terp, spin_count); goto err; } if (terp->cfg.per_op_cb != NULL) if (!TEST_true(terp->cfg.per_op_cb(terp, terp->cfg.per_op_cb_arg))) { TEST_error("pre-operation processing failed at op %zu", op_num); if (terp->log_execute) TERP_log_spin(terp, spin_count); goto err; } switch (opc) { case OPK_END: goto stop; case OPK_PUSH_P: case OPK_PUSH_PZ: { void *v; TERP_GET_OPERAND(v); TERP_STK_PUSH(terp, v); } break; case OPK_PUSH_U64: { uint64_t v; TERP_GET_OPERAND(v); TERP_STK_PUSH(terp, v); } break; case OPK_PUSH_SIZE: { size_t v; TERP_GET_OPERAND(v); TERP_STK_PUSH(terp, v); } break; case OPK_LABEL: { const char *l_name; TERP_GET_OPERAND(l_name); /* no-op */ } break; case OPK_FUNC: { helper_func_t v; const void *f_name; int ret; TERP_GET_OPERAND(v); TERP_GET_OPERAND(f_name); if (!TEST_true(v != NULL)) goto err; ret = v(&terp->fctx); if (terp->fctx.skip_rest) { if (!TEST_int_eq(ret, F_RET_SKIP_REST)) goto err; if (terp->log_execute) BIO_printf(terp->cfg.debug_bio, " \t\t(skipping)\n"); terp->fctx.skip_rest = 0; goto stop; } else if (terp->fctx.spin_again) { if (!TEST_int_eq(ret, F_RET_SPIN_AGAIN)) goto err; terp->fctx.spin_again = 0; TERP_SPIN_AGAIN(); } else { if (!TEST_false(terp->fctx.spin_again)) goto err; if (ret != 1) { TEST_error("op %zu (FUNC %s) failed with return value %d", op_num, (const char *)f_name, ret); goto err; } } } break; default: TEST_error("unknown opcode: %llu", (unsigned long long)opc); goto err; } } stop: ok = 1; err: if (in_debug_output) BIO_printf(debug_bio, "----------------------------------------" "------------------------------\n"); if (!ok) { TEST_error("FAILED while executing script: %s at op %zu, error stack:", terp->script_info->name, op_num); ERR_print_errors(terp->cfg.debug_bio); BIO_printf(debug_bio, "\n"); } else if (ERR_peek_last_error() != 0) { TEST_info("WARNING: errors on error stack despite success:"); ERR_print_errors(terp->cfg.debug_bio); BIO_printf(debug_bio, "\n"); } return ok; } static int TERP_run(SCRIPT_INFO *script_info, TERP_CONFIG *cfg) { int ok = 0, have_terp = 0; TERP terp; GEN_SCRIPT gen_script = {0}; BIO *debug_bio = cfg->debug_bio; SCRIPT_INFO_print(script_info, debug_bio, /*error=*/0, "generating script"); /* Generate the script by calling the generator function. */ if (!TEST_true(GEN_SCRIPT_init(&gen_script, script_info))) { SCRIPT_INFO_print(script_info, debug_bio, /*error=*/1, "error while generating script"); goto err; } /* Output the script for debugging purposes. */ if (!TEST_true(GEN_SCRIPT_print(&gen_script, debug_bio, script_info))) { SCRIPT_INFO_print(script_info, debug_bio, /*error=*/1, "error while printing script"); goto err; } /* Execute the script. */ if (!TEST_true(TERP_init(&terp, cfg, script_info, &gen_script))) goto err; have_terp = 1; SCRIPT_INFO_print(script_info, debug_bio, /*error=*/0, "executing script"); if (!TERP_execute(&terp)) goto err; if (terp.stk_end - terp.stk_cur != 0) { TEST_error("stack not empty: %zu bytes left", (size_t)(terp.stk_end - terp.stk_cur)); goto err; } ok = 1; err: if (have_terp) { TERP_print_stack(&terp, debug_bio, "Final state of stack"); TERP_cleanup(&terp); } GEN_SCRIPT_cleanup(&gen_script); BIO_printf(debug_bio, "Stats:\n Ops executed: %16llu\n\n", (unsigned long long)terp.ops_executed); SCRIPT_INFO_print(script_info, debug_bio, /*error=*/!ok, ok ? "completed" : "failed, exiting"); return ok; } #define SCRIPT(name) (&script_info_##name) #define USE(name) SCRIPT(name),