hs_pow: Update for equix API to fix issue 40794

This change adapts the hs_pow layer and unit tests to API changes
in hashx and equix which modify the fault recovery responsibilities
and reporting behaivor.

This and the corresponding implementation changes in hashx and equix
form the fix for #40794, both solving the segfault and giving hashx a
way to report those failures up the call chain without them being
mistaken for a different error (unusable seed) that would warrant a
retry.

To handle these new late compiler failures with a minimum of fuss or
inefficiency, the failover is delegated to the internals of hashx and
tor needs only pass in a EQUIX_CTX_TRY_COMPILE flag to get the behavior
that tor was previously responsible for implementing.

Signed-off-by: Micah Elizabeth Scott <beth@torproject.org>
This commit is contained in:
Micah Elizabeth Scott 2023-05-28 16:35:31 -07:00
parent a3513dea54
commit a397a92be2
4 changed files with 115 additions and 93 deletions

View File

@ -27,6 +27,7 @@
#include "lib/cc/ctassert.h"
#include "core/mainloop/cpuworker.h"
#include "lib/evloop/workqueue.h"
#include "lib/time/compat_time.h"
/** Replay cache set up */
/** Cache entry for (nonce, seed) replay protection. */
@ -91,23 +92,6 @@ increment_and_set_nonce(uint8_t *nonce, uint8_t *challenge)
memcpy(challenge + HS_POW_NONCE_OFFSET, nonce, HS_POW_NONCE_LEN);
}
/* Helper: Allocate an EquiX context, using the much faster compiled
* implementation of hashx if it's available on this architecture. */
static equix_ctx *
build_equix_ctx(equix_ctx_flags flags)
{
equix_ctx *ctx = equix_alloc(flags | EQUIX_CTX_COMPILE);
if (ctx == EQUIX_NOTSUPP) {
ctx = equix_alloc(flags);
}
tor_assert_nonfatal(ctx != EQUIX_NOTSUPP);
tor_assert_nonfatal(ctx != NULL);
if (ctx == EQUIX_NOTSUPP) {
ctx = NULL;
}
return ctx;
}
/* Helper: Build EquiX challenge (P || ID || C || N || INT_32(E)) and return
* a newly allocated buffer containing it. */
static uint8_t *
@ -214,36 +198,82 @@ hs_pow_solve(const hs_pow_solver_inputs_t *pow_inputs,
challenge = build_equix_challenge(&pow_inputs->service_blinded_id,
pow_inputs->seed, nonce, effort);
ctx = build_equix_ctx(EQUIX_CTX_SOLVE);
ctx = equix_alloc(EQUIX_CTX_SOLVE | EQUIX_CTX_TRY_COMPILE);
if (!ctx) {
goto end;
}
equix_solution solutions[EQUIX_MAX_SOLS];
uint8_t sol_bytes[HS_POW_EQX_SOL_LEN];
uint8_t sol_bytes[HS_POW_EQX_SOL_LEN];
monotime_t start_time;
monotime_get(&start_time);
log_info(LD_REND, "Solving proof of work (effort %u)", effort);
for (;;) {
/* Calculate solutions to S = equix_solve(C || N || E), */
int count = equix_solve(ctx, challenge, HS_POW_CHALLENGE_LEN, solutions);
for (int i = 0; i < count; i++) {
pack_equix_solution(&solutions[i], sol_bytes);
equix_solutions_buffer buffer;
equix_result result;
result = equix_solve(ctx, challenge, HS_POW_CHALLENGE_LEN, &buffer);
switch (result) {
/* Check an Equi-X solution against the effort threshold */
if (validate_equix_challenge(challenge, sol_bytes, effort)) {
/* Store the nonce N. */
memcpy(pow_solution_out->nonce, nonce, HS_POW_NONCE_LEN);
/* Store the effort E. */
pow_solution_out->effort = effort;
/* We only store the first 4 bytes of the seed C. */
memcpy(pow_solution_out->seed_head, pow_inputs->seed,
case EQUIX_OK:
for (unsigned i = 0; i < buffer.count; i++) {
pack_equix_solution(&buffer.sols[i], sol_bytes);
/* Check an Equi-X solution against the effort threshold */
if (validate_equix_challenge(challenge, sol_bytes, effort)) {
/* Store the nonce N. */
memcpy(pow_solution_out->nonce, nonce, HS_POW_NONCE_LEN);
/* Store the effort E. */
pow_solution_out->effort = effort;
/* We only store the first 4 bytes of the seed C. */
memcpy(pow_solution_out->seed_head, pow_inputs->seed,
sizeof(pow_solution_out->seed_head));
/* Store the solution S */
memcpy(&pow_solution_out->equix_solution, sol_bytes, sizeof sol_bytes);
/* Store the solution S */
memcpy(&pow_solution_out->equix_solution,
sol_bytes, sizeof sol_bytes);
/* Indicate success and we are done. */
ret = 0;
monotime_t end_time;
monotime_get(&end_time);
int64_t duration_usec = monotime_diff_usec(&start_time, &end_time);
log_info(LD_REND, "Proof of work solution (effort %u) found "
"using %s implementation in %u.%06u seconds",
effort,
(EQUIX_SOLVER_DID_USE_COMPILER & buffer.flags)
? "compiled" : "interpreted",
(unsigned)(duration_usec / 1000000),
(unsigned)(duration_usec % 1000000));
/* Indicate success and we are done. */
ret = 0;
goto end;
}
}
break;
case EQUIX_FAIL_CHALLENGE:
/* This happens occasionally due to HashX rejecting some program
* configurations. For our purposes here it's the same as count==0.
* Increment the nonce and try again. */
break;
case EQUIX_FAIL_COMPILE:
/* The interpreter is disabled and the compiler failed */
log_warn(LD_REND, "Proof of work solver failed, "
"compile error with no fallback enabled.");
goto end;
/* These failures are not applicable to equix_solve, but included for
* completeness and to satisfy exhaustive enum warnings. */
case EQUIX_FAIL_ORDER:
case EQUIX_FAIL_PARTIAL_SUM:
case EQUIX_FAIL_FINAL_SUM:
/* And these really should not happen, and indicate
* programming errors if they do. */
case EQUIX_FAIL_NO_SOLVER:
case EQUIX_FAIL_INTERNAL:
default:
tor_assert_nonfatal_unreached();
goto end;
}
}
/* No solutions for this nonce and/or none that passed the effort
@ -309,7 +339,7 @@ hs_pow_verify(const ed25519_public_key_t *service_blinded_id,
goto done;
}
ctx = build_equix_ctx(EQUIX_CTX_VERIFY);
ctx = equix_alloc(EQUIX_CTX_VERIFY | EQUIX_CTX_TRY_COMPILE);
if (!ctx) {
goto done;
}
@ -401,11 +431,7 @@ pow_worker_threadfn(void *state_, void *work_)
log_warn(LD_REND, "Failed to run the proof of work solver");
tor_free(job->pow_solution_out);
job->pow_solution_out = NULL; /* how we signal that we came up empty */
return WQ_RPL_REPLY;
}
/* we have a winner! */
log_info(LD_REND, "cpuworker has a proof of work solution");
return WQ_RPL_REPLY;
}

View File

@ -2982,9 +2982,9 @@ test_crypto_hashx(void *arg)
static const struct {
hashx_type type;
} variations[] = {
{ HASHX_INTERPRETED },
{ HASHX_TYPE_INTERPRETED },
#if defined(_M_X64) || defined(__x86_64__) || defined(__aarch64__)
{ HASHX_COMPILED },
{ HASHX_TYPE_COMPILED },
#endif
};
@ -3013,12 +3013,12 @@ test_crypto_hashx(void *arg)
ctx = hashx_alloc(variations[vari_i].type);
tt_ptr_op(ctx, OP_NE, NULL);
tt_ptr_op(ctx, OP_NE, HASHX_NOTSUPP);
retval = hashx_make(ctx, seed_literal, seed_len);
tt_int_op(retval, OP_EQ, 1);
tt_int_op(retval, OP_EQ, HASHX_OK);
memset(out_actual, 0xa5, sizeof out_actual);
hashx_exec(ctx, hash_input, out_actual);
retval = hashx_exec(ctx, hash_input, out_actual);
tt_int_op(retval, OP_EQ, HASHX_OK);
tt_mem_op(out_actual, OP_EQ, out_expected, sizeof out_actual);
}
}

View File

@ -636,14 +636,21 @@ test_crypto_equix(void *arg)
static const struct {
equix_ctx_flags flags;
equix_result expected;
equix_solution_flags sol_flags;
} variations[] = {
{0, EQUIX_OK},
{0, EQUIX_ORDER},
{0, EQUIX_PARTIAL_SUM},
{0, EQUIX_OK, 0},
{0, EQUIX_FAIL_ORDER, 0},
{0, EQUIX_FAIL_PARTIAL_SUM, 0},
#if defined(_M_X64) || defined(__x86_64__) || defined(__aarch64__)
{EQUIX_CTX_COMPILE, EQUIX_OK},
{EQUIX_CTX_COMPILE, EQUIX_ORDER},
{EQUIX_CTX_COMPILE, EQUIX_PARTIAL_SUM},
{ EQUIX_CTX_MUST_COMPILE, EQUIX_OK,
EQUIX_SOLVER_DID_USE_COMPILER
},
{ EQUIX_CTX_MUST_COMPILE, EQUIX_FAIL_ORDER,
EQUIX_SOLVER_DID_USE_COMPILER
},
{ EQUIX_CTX_MUST_COMPILE, EQUIX_FAIL_PARTIAL_SUM,
EQUIX_SOLVER_DID_USE_COMPILER
},
#endif
};
@ -659,48 +666,42 @@ test_crypto_equix(void *arg)
for (unsigned vari_i = 0; vari_i < num_variations; vari_i++) {
const equix_ctx_flags flags = variations[vari_i].flags;
const equix_solution_flags sol_flags = variations[vari_i].sol_flags;
const equix_result expected = variations[vari_i].expected;
equix_solution sols_actual[EQUIX_MAX_SOLS];
equix_solutions_buffer output;
equix_ctx *solve_ctx = NULL, *verify_ctx = NULL;
solve_ctx = equix_alloc(EQUIX_CTX_SOLVE | flags);
tt_ptr_op(solve_ctx, OP_NE, NULL);
tt_ptr_op(solve_ctx, OP_NE, EQUIX_NOTSUPP);
/* Solve phase: Make sure the test vector matches */
memset(sols_actual, 0xa5, sizeof sols_actual);
int retval = equix_solve(solve_ctx, challenge_literal,
challenge_len, sols_actual);
tt_int_op(retval, OP_EQ, num_sols);
tt_mem_op(sols_actual, OP_EQ, sols_expected,
memset(&output, 0xa5, sizeof output);
equix_result result;
result = equix_solve(solve_ctx, challenge_literal,
challenge_len, &output);
tt_int_op(result, OP_EQ, EQUIX_OK);
tt_int_op(output.count, OP_EQ, num_sols);
tt_int_op(output.flags, OP_EQ, sol_flags);
tt_mem_op(output.sols, OP_EQ, sols_expected,
num_sols * sizeof(equix_solution));
verify_ctx = equix_alloc(EQUIX_CTX_VERIFY | flags);
tt_ptr_op(verify_ctx, OP_NE, NULL);
tt_ptr_op(verify_ctx, OP_NE, EQUIX_NOTSUPP);
/* Use each solution for positive and negative tests of verify */
for (size_t sol_i = 0; sol_i < num_sols; sol_i++) {
equix_result result;
equix_idx tmp_idx;
equix_solution *sol = &sols_actual[sol_i];
equix_solution *sol = &output.sols[sol_i];
switch (expected) {
case EQUIX_ORDER:
/* Swap two otherwise valid indices, to trigger an order error */
tmp_idx = sol->idx[0];
sol->idx[0] = sol->idx[1];
sol->idx[1] = tmp_idx;
break;
case EQUIX_FINAL_SUM:
case EQUIX_PARTIAL_SUM:
/* Most changes to the solution will cause a partial sum error */
sol->idx[0]++;
break;
case EQUIX_OK:
case EQUIX_CHALLENGE:
break;
if (expected == EQUIX_FAIL_ORDER) {
/* Swap two otherwise valid indices, to trigger an order error */
tmp_idx = sol->idx[0];
sol->idx[0] = sol->idx[1];
sol->idx[1] = tmp_idx;
} else if (expected == EQUIX_FAIL_PARTIAL_SUM) {
/* Most changes to the solution will cause a partial sum error */
sol->idx[0]++;
}
result = equix_verify(verify_ctx, challenge_literal,

View File

@ -315,32 +315,27 @@ test_sandbox_crypto_equix(void *arg)
{{ 0x62c5, 0x86d1, 0x5752, 0xe1f0, 0x12da, 0x8f33, 0x7336, 0xf161 }},
};
equix_solution sols_actual[EQUIX_MAX_SOLS] = { 0 };
equix_solutions_buffer output;
equix_ctx *solve_ctx = NULL, *verify_ctx = NULL;
/* TODO: A subsequent change will modify these flags to use an auto fallback
* that will be built into our fork of equix. (This implements a
* performant and low-complexity way to share the generated program
* state during fallback instead of re-generating it.)
*/
solve_ctx = equix_alloc(EQUIX_CTX_SOLVE | EQUIX_CTX_COMPILE);
solve_ctx = equix_alloc(EQUIX_CTX_SOLVE | EQUIX_CTX_TRY_COMPILE);
tt_ptr_op(solve_ctx, OP_NE, NULL);
tt_ptr_op(solve_ctx, OP_NE, EQUIX_NOTSUPP);
int retval = equix_solve(solve_ctx, challenge_literal,
challenge_len, sols_actual);
tt_int_op(retval, OP_EQ, num_sols);
tt_mem_op(sols_actual, OP_EQ, sols_expected,
equix_result result;
memset(&output, 0xEE, sizeof output);
result = equix_solve(solve_ctx, challenge_literal, challenge_len, &output);
tt_int_op(result, OP_EQ, EQUIX_OK);
tt_int_op(output.count, OP_EQ, num_sols);
tt_int_op(output.flags, OP_EQ, 0); /* EQUIX_SOLVER_DID_USE_COMPILER unset */
tt_mem_op(output.sols, OP_EQ, sols_expected,
num_sols * sizeof(equix_solution));
verify_ctx = equix_alloc(EQUIX_CTX_VERIFY | EQUIX_CTX_COMPILE);
verify_ctx = equix_alloc(EQUIX_CTX_VERIFY | EQUIX_CTX_TRY_COMPILE);
tt_ptr_op(verify_ctx, OP_NE, NULL);
tt_ptr_op(verify_ctx, OP_NE, EQUIX_NOTSUPP);
/* Test one of the solutions randomly */
equix_result result;
const unsigned sol_i = crypto_rand_int(num_sols);
equix_solution *sol = &sols_actual[sol_i];
equix_solution *sol = &output.sols[sol_i];
result = equix_verify(verify_ctx, challenge_literal,
challenge_len, sol);