What changed, and why it matters
This commit re-enables a previously disabled security module called sensitive.c in the libjade (non-embedded) build. It also adds proper cleanup of thread-local storage so that sensitive data is cleared when the Jade firmware stops and restarts in the same process. The change appears to be a hardening fix rather than an active exploit patch: it prevents sensitive memory pointers from persisting across simulated reboots on desktop/test builds.
Review the real sensitive.c implementation to confirm it correctly tracks and clears all sensitive stack regions. Verify that libjade_tls_reset() is always called before libjade_start() in all paths, and that no sensitive data leaks through other global state between simulated boots. Consider adding tests that verify sensitive memory is zeroed after libjade_stop().
Security signals we found
Re-enables real sensitive memory tracking/clearing in libjade builds
Adds thread-local storage cleanup callbacks to prevent stale sensitive pointers
Adds startup assertion that TLS is empty, catching missed cleanup
Removes no-op stubs that previously disabled sensitive-stack protections in libjade
Potential memory safety issue: _tls_cleanup calls free(tls) but does not clear the pthread key if pthread_getspecific returns a different pointer, though current code always matches
Evidence from the diff
The commit removes stub no-op implementations of sensitive_init(), sensitive_push(), sensitive_pop(), sensitive_assert_empty(), and sensitive_clear_stack() from libjade.c, allowing the real implementations in sensitive.c to be compiled into both embedded and libjade builds. It adds libjade_tls_init() and libjade_tls_reset() to emulate FreeRTOS task-local storage deletion callbacks on pthreads. libjade_tls_init() asserts that TLS slots are empty at startup, and libjade_tls_reset() invokes registered delete callbacks and frees the TLS block during libjade_stop(). The amalgamated.c change moves sensitive.c inclusion outside the #ifndef CONFIG_LIBJADE guard so it is included in libjade builds too.
Changed components
libjade/libjade.clibjade/task.clibjade/include/libjade_port.hmain/amalgamated.csensitive.c (now enabled in libjade builds)Inspect captured patch +99 / −19
### libjade/include/libjade_port.h
@@ -31,6 +31,13 @@ static inline void libjade_thread_setname(const char* name)
}
}
+// Assert the calling thread's FreeRTOS task-local storage is empty (see libjade/task.c)
+void libjade_tls_init(void);
+
+// Discard the calling thread's FreeRTOS task-local storage (see libjade/task.c),
+// invoking any registered 'delete' callbacks as the RTOS does when a task is deleted
+void libjade_tls_reset(void);
+
#ifdef __APPLE__
#include <stdlib.h>
@@ -50,4 +57,4 @@ static inline ssize_t libjade_getrandom(void* buf, size_t buflen) { return getra
#endif // __APPLE__
-#endif // _LIBJADE_PORT_H_
\ No newline at end of file
+#endif // _LIBJADE_PORT_H_
### libjade/libjade.c
@@ -184,17 +184,6 @@ const uint8_t _binary_pinserver_public_key_pub_start[33]
// Events
volatile bool _libjade_stop_requested = false; // Used to stop the firmware
-// HW: Task API
-void sensitive_init(void) {}
-
-void sensitive_push(const char* file, int line, void* addr, size_t size) {}
-
-void sensitive_pop(const char* file, int line, void* addr) {}
-
-void sensitive_assert_empty(void) {}
-
-void sensitive_clear_stack(void) {}
-
// HW: Random
void get_random(void* bytes_out, size_t len)
{
@@ -264,6 +253,7 @@ int random_mbedtls_cb(void* ctx, uint8_t* buf, const size_t len)
static void* jade_fw_thread_fn(void* arg)
{
libjade_thread_setname("libjade_fw");
+ sensitive_init();
start_dashboard();
return NULL; // Never reached
}
@@ -276,6 +266,7 @@ static __thread bool _libjade_is_internal_msg = false;
void libjade_start(void)
{
+ libjade_tls_init();
ensure_boot_flags();
random_start_collecting();
validate_running_image();
@@ -311,6 +302,8 @@ void libjade_stop(void)
_libjade_is_internal_msg = false;
// clear keychain
keychain_clear();
+ // discard this thread's task-local storage
+ libjade_tls_reset();
}
static uint8_t _libjade_serial_data_in[MAX_INPUT_MSG_SIZE + 1] = { 0 };
### libjade/task.c
@@ -1,4 +1,5 @@
#define _GNU_SOURCE 1 // For extra pthread functions
+#include "freertos/FreeRTOS.h"
#include "freertos/timecvt.h"
#include "jade_assert.h"
#include "libjade_port.h"
@@ -111,19 +112,98 @@ static pthread_once_t _waiter_cleanup_key_once = PTHREAD_ONCE_INIT;
static void _waiter_cleanup_key_init(void) { pthread_key_create(&_waiter_cleanup_key, _waiter_cleanup); }
// HW: TLS/Sensitive
-static void* _tls_ptrs[3];
+//
+// Emulation of the FreeRTOS per-task thread-local-storage pointers
+#define LIBJADE_TLS_POINTERS CONFIG_FREERTOS_THREAD_LOCAL_STORAGE_POINTERS
+
+typedef struct {
+ void* ptrs[LIBJADE_TLS_POINTERS];
+ TlsDeleteCallbackFunction_t cbs[LIBJADE_TLS_POINTERS];
+} libjade_tls_t;
+
+static pthread_key_t _libjade_tls_key;
+static pthread_once_t _libjade_tls_key_once = PTHREAD_ONCE_INIT;
+
+// Invoked by pthread as the thread exits, and by libjade_tls_reset()
+static void _tls_cleanup(void* p)
+{
+ libjade_tls_t* tls = (libjade_tls_t*)p;
+ if (!tls) {
+ return;
+ }
+ if (pthread_getspecific(_libjade_tls_key) == tls) {
+ pthread_setspecific(_libjade_tls_key, NULL);
+ }
+ for (size_t i = 0; i < LIBJADE_TLS_POINTERS; ++i) {
+ void* const ptr = tls->ptrs[i];
+ TlsDeleteCallbackFunction_t const cb = tls->cbs[i];
+ tls->ptrs[i] = NULL;
+ tls->cbs[i] = NULL;
+ if (ptr && cb) {
+ cb((int)i, ptr);
+ }
+ }
+ free(tls);
+}
+
+static void _tls_key_init(void) { pthread_key_create(&_libjade_tls_key, _tls_cleanup); }
+
+// NOTE: 'task' must be NULL at this stage (only the current thread's TLS is accessed)
+static libjade_tls_t* _get_tls(const bool create_if_missing)
+{
+ pthread_once(&_libjade_tls_key_once, _tls_key_init);
+ libjade_tls_t* tls = pthread_getspecific(_libjade_tls_key);
+ if (!tls && create_if_missing) {
+ tls = calloc(1, sizeof(libjade_tls_t));
+ JADE_ASSERT(tls);
+ pthread_setspecific(_libjade_tls_key, tls);
+ }
+ return tls;
+}
void* pvTaskGetThreadLocalStoragePointer(void* task, size_t idx)
{
- JADE_ASSERT(idx <= sizeof(_tls_ptrs) / sizeof(_tls_ptrs[0]));
- return _tls_ptrs[idx];
+ JADE_ASSERT(task == NULL); // Only ever called for the current task
+ JADE_ASSERT(idx < LIBJADE_TLS_POINTERS);
+ const bool create_if_missing = false;
+ const libjade_tls_t* tls = _get_tls(create_if_missing);
+ return tls ? tls->ptrs[idx] : NULL;
}
void vTaskSetThreadLocalStoragePointerAndDelCallback(void* task, size_t idx, void* p, TlsDeleteCallbackFunction_t cb)
{
- JADE_ASSERT(idx <= sizeof(_tls_ptrs) / sizeof(_tls_ptrs[0]));
- _tls_ptrs[idx] = p;
- // FIXME: call cb atexit()/thread exit?
+ JADE_ASSERT(task == NULL); // Only ever called for the current task
+ JADE_ASSERT(idx < LIBJADE_TLS_POINTERS);
+ const bool create_if_missing = true;
+ libjade_tls_t* tls = _get_tls(create_if_missing);
+ JADE_ASSERT(tls);
+ tls->ptrs[idx] = p;
+ tls->cbs[idx] = cb;
+}
+
+// On a device each boot is a distinct task, whereas libjade reuses the calling
+// thread, so simulate a new boot by enforcing that the task-local storage is empty
+void libjade_tls_init(void)
+{
+ pthread_once(&_libjade_tls_key_once, _tls_key_init);
+ const libjade_tls_t* tls = pthread_getspecific(_libjade_tls_key);
+ for (size_t i = 0; tls && i < LIBJADE_TLS_POINTERS; ++i) {
+ JADE_ASSERT_MSG(!tls->ptrs[i],
+ "libjade_start() called with task-local storage still set (index %u) - was libjade_stop() called?",
+ (unsigned)i);
+ }
+}
+
+// Discard the calling thread's task-local storage, invoking any registered
+// delete callbacks
+void libjade_tls_reset(void)
+{
+ pthread_once(&_libjade_tls_key_once, _tls_key_init);
+ libjade_tls_t* tls = pthread_getspecific(_libjade_tls_key);
+ if (!tls) {
+ return;
+ }
+ _tls_cleanup(tls);
}
const char* pcTaskGetName(void* task)
### main/amalgamated.c
@@ -125,8 +125,8 @@ void __wrap_abort(void);
#ifndef CONFIG_LIBJADE
#include "./random.c"
#include "./selfcheck.c"
-#include "./sensitive.c"
#endif // CONFIG_LIBJADE
+#include "./sensitive.c"
#ifdef ESP_PLATFORM
#include "./serial.c"
#endif // ESP_PLATFORMWhy this scored 42/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.