Skip to content

Commit 9f2322c

Browse files
authored
Merge branch 'main' into penpal-openssl-cleanse-sweep
2 parents c398edb + 4ab2b63 commit 9f2322c

5 files changed

Lines changed: 286 additions & 9 deletions

File tree

crypto/fipsmodule/rand/internal.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,15 @@ OPENSSL_EXPORT uint64_t get_private_thread_reseed_calls_since_initialization(voi
2828
OPENSSL_EXPORT uint64_t get_public_thread_generate_calls_since_seed(void);
2929
OPENSSL_EXPORT uint64_t get_public_thread_reseed_calls_since_initialization(void);
3030

31+
// rand_thread_local_state_clear_all_FOR_TESTING runs the same shutdown
32+
// zeroization sequence that is registered via |atexit|. It is exposed for tests
33+
// that need to verify shutdown behavior (e.g. that a thread exiting after
34+
// shutdown does not deadlock). After this is called, no further |RAND_bytes|
35+
// calls in the test process will return output, so the caller is responsible
36+
// for arranging that no other code paths in the same process require
37+
// randomness afterwards.
38+
OPENSSL_EXPORT void rand_thread_local_state_clear_all_FOR_TESTING(void);
39+
3140
// CTR_DRBG_STATE contains the state of a CTR_DRBG based on AES-256. See SP
3241
// 800-90Ar1.
3342
struct ctr_drbg_state_st {

crypto/fipsmodule/rand/rand.c

Lines changed: 86 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,16 @@ OPENSSL_STATIC_ASSERT((sizeof((struct rand_thread_local_state*)0)->generate_call
4747
DEFINE_BSS_GET(struct rand_thread_local_state *, thread_states_list_head)
4848
DEFINE_STATIC_MUTEX(thread_local_states_list_lock)
4949

50+
// thread_local_drbg_shutdown_started is set to a non-zero value during process
51+
// exit by |rand_thread_local_state_clear_all|, while the linked-list lock is
52+
// held. All reads and writes occur under |thread_local_states_list_lock|, so
53+
// no atomic accessors are needed; doing the check under the lock also avoids
54+
// the otherwise-racy window where a TLS destructor could observe the flag as
55+
// unset, then block on the lock while shutdown zeroization runs, and then
56+
// free a state whose |state_clear_lock| has been intentionally write-locked
57+
// forever.
58+
DEFINE_BSS_GET(int, thread_local_drbg_shutdown_started)
59+
5060
#if defined(_MSC_VER)
5161
#pragma section(".CRT$XCU", read)
5262
static void rand_thread_local_state_clear_all(void);
@@ -67,19 +77,42 @@ static void rand_thread_local_state_clear_all(void) __attribute__ ((destructor))
6777
// randomness from a non-valid state. The linked application should obviously
6878
// arrange that all threads are gracefully exited before exiting the process.
6979
// Yet, in cases where such graceful exit does not happen we ensure that no
70-
// output can be returned by locking all thread-local states and deliberately
71-
// not releasing the lock. A synchronization step in the core randomness
72-
// generation routine |RAND_bytes_core| then ensures that no randomness
73-
// generation can occur after a thread-local state has been locked. It also
74-
// ensures |rand_thread_local_state_free| cannot free any thread state while we
75-
// own the lock.
80+
// output can be returned by locking each thread-local state's
81+
// |state_clear_lock| and deliberately not releasing it. A synchronization step
82+
// in the core randomness generation routine |RAND_bytes_core| then ensures
83+
// that no randomness generation can occur after a thread-local state has been
84+
// locked.
85+
//
86+
// We additionally set |thread_local_drbg_shutdown_started| under the
87+
// linked-list lock so that any thread which has not yet registered a
88+
// thread-local state cannot do so after this routine has begun zeroization;
89+
// without this, a fresh thread could allocate a state and bypass zeroization.
90+
// The linked-list lock itself is released at the end of this function. If we
91+
// instead held it forever, |thread_local_list_delete_node| (called from a
92+
// thread's TLS destructor in |rand_thread_local_state_free|) would block
93+
// indefinitely. On Windows, TLS destructors run under the loader lock, so
94+
// blocking there causes |ExitProcess| to hang waiting for the loader lock.
95+
// See https://github.com/aws/aws-lc/issues/3197.
7696
//
77-
// When a thread-local DRBGs is gated from returning output, we can invoke the
97+
// When a thread-local DRBG is gated from returning output, we can invoke the
7898
// entropy source zeroization from |state->entropy_source|. The entropy source
7999
// implementation can assume that any returned seed is never used to generate
80100
// any randomness that is later returned to a consumer.
81101
static void rand_thread_local_state_clear_all(void) {
82102
CRYPTO_STATIC_MUTEX_lock_write(thread_local_states_list_lock_bss_get());
103+
104+
// Idempotency guard. Under normal operation this routine runs exactly once
105+
// (via |atexit|), but it is also exposed for testing via
106+
// |rand_thread_local_state_clear_all_FOR_TESTING|, and re-running would
107+
// attempt to write-lock per-state |state_clear_lock|s that are already held
108+
// write-locked by the first invocation -- which is UB for a non-recursive
109+
// rwlock.
110+
if (*thread_local_drbg_shutdown_started_bss_get() != 0) {
111+
CRYPTO_STATIC_MUTEX_unlock_write(thread_local_states_list_lock_bss_get());
112+
return;
113+
}
114+
*thread_local_drbg_shutdown_started_bss_get() = 1;
115+
83116
for (struct rand_thread_local_state *state = *thread_states_list_head_bss_get();
84117
state != NULL; state = state->next) {
85118
CRYPTO_MUTEX_lock_write(&state->state_clear_lock);
@@ -90,13 +123,34 @@ static void rand_thread_local_state_clear_all(void) {
90123
state != NULL; state = state->next) {
91124
state->entropy_source->methods->zeroize_thread(state->entropy_source);
92125
}
126+
127+
CRYPTO_STATIC_MUTEX_unlock_write(thread_local_states_list_lock_bss_get());
128+
}
129+
130+
void rand_thread_local_state_clear_all_FOR_TESTING(void) {
131+
rand_thread_local_state_clear_all();
93132
}
94133

95-
static void thread_local_list_delete_node(
134+
// thread_local_list_delete_node removes |node_delete| from the global
135+
// linked list and returns 1. If process-wide shutdown zeroization has already
136+
// begun, the node is left in place and 0 is returned -- the caller must not
137+
// free the node in that case because |rand_thread_local_state_clear_all| has
138+
// write-locked its |state_clear_lock| and intentionally never releases it.
139+
static int thread_local_list_delete_node(
96140
struct rand_thread_local_state *node_delete) {
97141

98142
// Mutating the global linked list. Need to synchronize over all threads.
99143
CRYPTO_STATIC_MUTEX_lock_write(thread_local_states_list_lock_bss_get());
144+
145+
// Re-check the shutdown flag under the lock. This makes the
146+
// "free vs. shutdown-zeroize" decision atomic with respect to
147+
// |rand_thread_local_state_clear_all|: either we delete and free before
148+
// shutdown begins, or shutdown wins and we leak the node deliberately.
149+
if (*thread_local_drbg_shutdown_started_bss_get() != 0) {
150+
CRYPTO_STATIC_MUTEX_unlock_write(thread_local_states_list_lock_bss_get());
151+
return 0;
152+
}
153+
100154
struct rand_thread_local_state *node_head = *thread_states_list_head_bss_get();
101155

102156
// We have [node_delete->previous] <--> [node_delete] <--> [node_delete->next]
@@ -127,6 +181,7 @@ static void thread_local_list_delete_node(
127181
}
128182

129183
CRYPTO_STATIC_MUTEX_unlock_write(thread_local_states_list_lock_bss_get());
184+
return 1;
130185
}
131186

132187
// thread_local_list_add adds the state |node_add| to the linked list. Note that
@@ -141,6 +196,19 @@ static void thread_local_list_add_node(
141196
// Mutating the global linked list. Need to synchronize over all threads.
142197
CRYPTO_STATIC_MUTEX_lock_write(thread_local_states_list_lock_bss_get());
143198

199+
// If process-wide zeroization has already started, do not add a new state to
200+
// the list -- it would not have been zeroized by
201+
// |rand_thread_local_state_clear_all|. Instead, write-lock the state's
202+
// |state_clear_lock| and never release it. This causes any subsequent
203+
// |RAND_bytes_core| call on this thread to block forever on the read lock,
204+
// matching the FIPS-derived guarantee that no output is returned after
205+
// shutdown zeroization.
206+
if (*thread_local_drbg_shutdown_started_bss_get() != 0) {
207+
CRYPTO_MUTEX_lock_write(&node_add->state_clear_lock);
208+
CRYPTO_STATIC_MUTEX_unlock_write(thread_local_states_list_lock_bss_get());
209+
return;
210+
}
211+
144212
// First get a reference to the pointer of the head of the linked list.
145213
// That is, the pointer to the head node node_head is *thread_states_head.
146214
struct rand_thread_local_state **thread_states_head = thread_states_list_head_bss_get();
@@ -171,7 +239,16 @@ static void rand_thread_local_state_free(void *state_in) {
171239
return;
172240
}
173241

174-
thread_local_list_delete_node(state);
242+
// If process-wide shutdown zeroization has begun, the per-state
243+
// |state_clear_lock| has been write-locked and is intentionally never
244+
// released (so any in-flight |RAND_bytes| call cannot return output from a
245+
// zeroized state). |thread_local_list_delete_node| therefore detects this
246+
// case under the linked-list lock and refuses to delete; we must then leak
247+
// the node, since freeing it would destroy a held mutex. The OS reclaims
248+
// the memory at process exit. See issue #3197.
249+
if (thread_local_list_delete_node(state) == 0) {
250+
return;
251+
}
175252

176253
// Potentially, something could kill the thread before an entropy source has
177254
// been associated to the thread-local randomness generator object.

crypto/fipsmodule/rand/rand_test.cc

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,15 @@
2121

2222
#if defined(OPENSSL_THREADS)
2323
#include <array>
24+
#include <condition_variable>
25+
#include <mutex>
2426
#include <thread>
2527
#include <vector>
2628
#endif
2729

2830
#if !defined(OPENSSL_WINDOWS)
2931
#include <errno.h>
32+
#include <signal.h>
3033
#include <sys/types.h>
3134
#include <sys/wait.h>
3235
#include <unistd.h>
@@ -524,6 +527,88 @@ TEST_F(randTest, MixedUsageMultiThreaded) {
524527
}
525528
#endif // OPENSSL_THREADS
526529

530+
#if defined(OPENSSL_THREADS) && !defined(OPENSSL_WINDOWS) && \
531+
!defined(OPENSSL_IOS) && !defined(OPENSSL_WASM) && \
532+
!defined(BORINGSSL_UNSAFE_DETERMINISTIC_MODE)
533+
// Regression test for https://github.com/aws/aws-lc/issues/3197.
534+
//
535+
// Pre-fix, |rand_thread_local_state_clear_all| acquired
536+
// |thread_local_states_list_lock| and intentionally never released it. A
537+
// thread that had registered a thread-local state and exited *after* shutdown
538+
// zeroization would deadlock in its TLS destructor while waiting for that
539+
// lock. On Windows that destructor runs under the loader lock, which
540+
// indirectly hangs |ExitProcess|. This test reproduces the same lock cycle
541+
// (the lock is the same on POSIX -- only the loader-lock fallout is
542+
// Windows-specific) and verifies the child exits cleanly.
543+
//
544+
// We cannot run this in-process because shutdown zeroization permanently
545+
// disables the RNG for the whole process, breaking any later test. We fork a
546+
// child, run the scenario, and impose a hard timeout in the parent so a
547+
// regression manifests as a test failure rather than a hung suite.
548+
TEST_F(randTest, ShutdownDoesNotDeadlockExitingThread) {
549+
pid_t child = fork();
550+
ASSERT_GE(child, 0) << "fork failed: " << strerror(errno);
551+
552+
if (child == 0) {
553+
// Register a thread-local DRBG state on a worker thread, then trigger
554+
// shutdown zeroization on the main thread, then let the worker exit.
555+
std::mutex m;
556+
std::condition_variable cv;
557+
bool shutdown_done = false;
558+
bool worker_registered = false;
559+
560+
std::thread worker([&] {
561+
uint8_t buf[16];
562+
RAND_bytes(buf, sizeof(buf));
563+
{
564+
std::lock_guard<std::mutex> lk(m);
565+
worker_registered = true;
566+
}
567+
cv.notify_one();
568+
569+
std::unique_lock<std::mutex> lk(m);
570+
cv.wait(lk, [&] { return shutdown_done; });
571+
// Worker now exits. Pre-fix this exit would block forever in the TLS
572+
// destructor; post-fix it returns immediately.
573+
});
574+
575+
{
576+
std::unique_lock<std::mutex> lk(m);
577+
cv.wait(lk, [&] { return worker_registered; });
578+
}
579+
rand_thread_local_state_clear_all_FOR_TESTING();
580+
{
581+
std::lock_guard<std::mutex> lk(m);
582+
shutdown_done = true;
583+
}
584+
cv.notify_one();
585+
worker.join();
586+
_exit(0);
587+
}
588+
589+
// Parent: enforce a timeout so a regression doesn't hang the suite.
590+
constexpr int kTimeoutSeconds = 30;
591+
for (int i = 0; i < kTimeoutSeconds * 10; i++) {
592+
int status;
593+
pid_t ret = waitpid(child, &status, WNOHANG);
594+
ASSERT_GE(ret, 0) << "waitpid failed: " << strerror(errno);
595+
if (ret == child) {
596+
ASSERT_TRUE(WIFEXITED(status))
597+
<< "child terminated abnormally, status=" << status;
598+
ASSERT_EQ(WEXITSTATUS(status), 0);
599+
return;
600+
}
601+
usleep(100 * 1000);
602+
}
603+
604+
kill(child, SIGKILL);
605+
waitpid(child, nullptr, 0);
606+
FAIL() << "child did not exit within " << kTimeoutSeconds
607+
<< "s (likely a deadlock in TLS destructor after shutdown)";
608+
}
609+
#endif // OPENSSL_THREADS && !OPENSSL_WINDOWS && !OPENSSL_IOS &&
610+
// !OPENSSL_WASM && !BORINGSSL_UNSAFE_DETERMINISTIC_MODE
611+
527612
#if defined(OPENSSL_X86_64) && defined(SUPPORTS_ABI_TEST)
528613
TEST_F(randTest, RdrandABI) {
529614
if (!have_hw_rng_x86_64_for_testing()) {

crypto/x509/v3_ncons.c

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -686,6 +686,21 @@ static int nc_uri(const ASN1_IA5STRING *uri, const ASN1_IA5STRING *base) {
686686
return X509_V_ERR_UNSUPPORTED_NAME_SYNTAX;
687687
}
688688

689+
// RFC 3986 §3.2 defines authority = [userinfo "@"] host [":" port].
690+
// Certificate SAN URIs have no standards-defined use for userinfo. If present,
691+
// the '@' delimiter would cause the host extraction below to parse the
692+
// userinfo as the host, matching against attacker-chosen bytes instead of
693+
// the URI's actual host.
694+
for (size_t i = 0; i < CBS_len(&uri_cbs); i++) {
695+
uint8_t c = CBS_data(&uri_cbs)[i];
696+
if (c == '/' || c == '?' || c == '#') {
697+
break;
698+
}
699+
if (c == '@') {
700+
return X509_V_ERR_UNSUPPORTED_NAME_SYNTAX;
701+
}
702+
}
703+
689704
// Look for a port indicator as end of hostname first. Otherwise look for
690705
// trailing slash, or the end of the string.
691706
CBS host;
@@ -698,6 +713,35 @@ static int nc_uri(const ASN1_IA5STRING *uri, const ASN1_IA5STRING *base) {
698713
return X509_V_ERR_UNSUPPORTED_NAME_SYNTAX;
699714
}
700715

716+
// Normalize absolute DNS names by removing the trailing dot, if any.
717+
// RFC 1034 §3.1 defines that a trailing dot denotes the DNS root; names with
718+
// and without it refer to the same host. This matches the normalization in
719+
// nc_dns().
720+
if (ends_with_byte(&host, '.')) {
721+
uint8_t unused;
722+
CBS_get_last_u8(&host, &unused);
723+
}
724+
if (ends_with_byte(&base_cbs, '.')) {
725+
uint8_t unused;
726+
CBS_get_last_u8(&base_cbs, &unused);
727+
}
728+
729+
// RFC 5280 §4.2.1.10 requires the host to be a fully qualified domain name.
730+
// Validate that the host contains only characters valid in a DNS name
731+
// (RFC 1034 §3.5): letters, digits, hyphens, and dots. This rejects
732+
// percent-encoded hosts and any other non-FQDN syntax, preventing
733+
// equivalence bypasses (e.g., "b%61d.com" evading ".bad.com").
734+
if (CBS_len(&host) == 0) {
735+
return X509_V_ERR_UNSUPPORTED_NAME_SYNTAX;
736+
}
737+
for (size_t i = 0; i < CBS_len(&host); i++) {
738+
uint8_t c = CBS_data(&host)[i];
739+
if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
740+
(c >= '0' && c <= '9') || c == '-' || c == '.')) {
741+
return X509_V_ERR_UNSUPPORTED_NAME_SYNTAX;
742+
}
743+
}
744+
701745
// Special case: initial '.' is RHS match
702746
if (starts_with(&base_cbs, '.')) {
703747
if (has_suffix_case(&host, &base_cbs)) {

crypto/x509/x509_test.cc

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2640,6 +2640,68 @@ TEST(X509Test, NameConstraints) {
26402640
// An incomplete IPv6 literal is also rejected.
26412641
{GEN_URI, "foo://[2001:db8::1", "[2001:db8::1]",
26422642
X509_V_ERR_UNSUPPORTED_NAME_SYNTAX, X509_V_ERR_UNSUPPORTED_NAME_SYNTAX},
2643+
2644+
// RFC 3986 §3.2 defines authority = [userinfo "@"] host [":" port].
2645+
// URIs with userinfo are rejected to prevent host confusion: without
2646+
// this, "spiffe://x.team-a.corp:x@team-b.corp/admin" would be matched
2647+
// against "x.team-a.corp" instead of the actual host "team-b.corp",
2648+
// bypassing permittedSubtrees or evading excludedSubtrees.
2649+
//
2650+
// Basic userinfo before host.
2651+
{GEN_URI, "foo://user@example.com", "example.com",
2652+
X509_V_ERR_UNSUPPORTED_NAME_SYNTAX, X509_V_ERR_UNSUPPORTED_NAME_SYNTAX},
2653+
// Userinfo with colon (user:password style) — the colon in userinfo
2654+
// would previously be mistaken for a port delimiter, extracting the
2655+
// userinfo prefix as the host.
2656+
{GEN_URI, "spiffe://x.team-a.corp:x@team-b.corp/admin", "team-a.corp",
2657+
X509_V_ERR_UNSUPPORTED_NAME_SYNTAX, X509_V_ERR_UNSUPPORTED_NAME_SYNTAX},
2658+
{GEN_URI, "spiffe://x.team-a.corp:x@team-b.corp/admin", "team-b.corp",
2659+
X509_V_ERR_UNSUPPORTED_NAME_SYNTAX, X509_V_ERR_UNSUPPORTED_NAME_SYNTAX},
2660+
// Userinfo with path, query, and fragment components.
2661+
{GEN_URI, "foo://user@example.com/path", "example.com",
2662+
X509_V_ERR_UNSUPPORTED_NAME_SYNTAX, X509_V_ERR_UNSUPPORTED_NAME_SYNTAX},
2663+
{GEN_URI, "foo://user@example.com?query", "example.com",
2664+
X509_V_ERR_UNSUPPORTED_NAME_SYNTAX, X509_V_ERR_UNSUPPORTED_NAME_SYNTAX},
2665+
{GEN_URI, "foo://user@example.com#fragment", "example.com",
2666+
X509_V_ERR_UNSUPPORTED_NAME_SYNTAX, X509_V_ERR_UNSUPPORTED_NAME_SYNTAX},
2667+
// '@' in path (after '/') is not userinfo and should not be rejected.
2668+
{GEN_URI, "foo://example.com/@user", "example.com", X509_V_OK,
2669+
X509_V_ERR_EXCLUDED_VIOLATION},
2670+
{GEN_URI, "foo://example.com/path@thing", ".example.com",
2671+
X509_V_ERR_PERMITTED_VIOLATION, X509_V_OK},
2672+
// '@' after '?' or '#' is not in the authority and is not rejected.
2673+
// However, since the host parser doesn't treat '?' or '#' as authority
2674+
// terminators, the extracted host contains invalid FQDN characters and
2675+
// is rejected by FQDN validation.
2676+
{GEN_URI, "foo://example.com?x@y", "example.com",
2677+
X509_V_ERR_UNSUPPORTED_NAME_SYNTAX, X509_V_ERR_UNSUPPORTED_NAME_SYNTAX},
2678+
{GEN_URI, "foo://example.com#x@y", "example.com",
2679+
X509_V_ERR_UNSUPPORTED_NAME_SYNTAX, X509_V_ERR_UNSUPPORTED_NAME_SYNTAX},
2680+
// Empty userinfo and user:pass userinfo are both rejected.
2681+
{GEN_URI, "foo://@example.com", "example.com",
2682+
X509_V_ERR_UNSUPPORTED_NAME_SYNTAX, X509_V_ERR_UNSUPPORTED_NAME_SYNTAX},
2683+
{GEN_URI, "foo://user:pass@example.com", "example.com",
2684+
X509_V_ERR_UNSUPPORTED_NAME_SYNTAX, X509_V_ERR_UNSUPPORTED_NAME_SYNTAX},
2685+
2686+
// Trailing dot normalization: "host." and "host" are the same FQDN.
2687+
// This matches the normalization in nc_dns().
2688+
{GEN_URI, "spiffe://admin.team-a.corp./admin", ".team-a.corp",
2689+
X509_V_OK, X509_V_ERR_EXCLUDED_VIOLATION},
2690+
{GEN_URI, "spiffe://admin.team-a.corp/admin", ".team-a.corp.",
2691+
X509_V_OK, X509_V_ERR_EXCLUDED_VIOLATION},
2692+
{GEN_URI, "spiffe://team-a.corp./admin", "team-a.corp",
2693+
X509_V_OK, X509_V_ERR_EXCLUDED_VIOLATION},
2694+
2695+
// FQDN validation: hosts must contain only a-z, A-Z, 0-9, '-', '.'.
2696+
// Percent-encoded hosts are rejected, preventing equivalence bypasses
2697+
// (e.g., "b%61d.com" should not evade an exclusion for ".bad.com").
2698+
{GEN_URI, "spiffe://evil.b%61d.com/resource", ".bad.com",
2699+
X509_V_ERR_UNSUPPORTED_NAME_SYNTAX, X509_V_ERR_UNSUPPORTED_NAME_SYNTAX},
2700+
{GEN_URI, "foo://ex%61mple.com/path", "example.com",
2701+
X509_V_ERR_UNSUPPORTED_NAME_SYNTAX, X509_V_ERR_UNSUPPORTED_NAME_SYNTAX},
2702+
// Underscores are not valid in FQDNs.
2703+
{GEN_URI, "foo://my_host.example.com/path", ".example.com",
2704+
X509_V_ERR_UNSUPPORTED_NAME_SYNTAX, X509_V_ERR_UNSUPPORTED_NAME_SYNTAX},
26432705
};
26442706
for (const auto &t : kTests) {
26452707
SCOPED_TRACE(t.type);

0 commit comments

Comments
 (0)