Skip to content

Fix Windows stack-overflow recursion between the log sink and the memory-security allocator - #602

Open
moodysalem wants to merge 1 commit into
sqlcipher:masterfrom
moodysalem:fix/windows-log-reentrancy-stack-overflow
Open

Fix Windows stack-overflow recursion between the log sink and the memory-security allocator#602
moodysalem wants to merge 1 commit into
sqlcipher:masterfrom
moodysalem:fix/windows-log-reentrancy-stack-overflow

Conversation

@moodysalem

@moodysalem moodysalem commented Aug 4, 2026

Copy link
Copy Markdown

Summary

On Windows, enabling PRAGMA cipher_memory_security = ON can crash the process with STATUS_STACK_OVERFLOW through unbounded mutual recursion between the logger and the locked allocator:

  1. The default log configuration is level WARN to stderr.
  2. cipher_memory_security routes every sqlite3_malloc through the locked allocator, and sqlcipher_mlock logs a WARN whenever VirtualLock fails — commonly VirtualLock() returned 0 LastError=1453 (ERROR_WORKING_SET_QUOTA), the known-benign condition discussed in https://discuss.zetetic.net/t/errors-with-mlock-virtuallock-in-ci-cd-pipelines/6704.
  3. On Windows the console sink, sqlcipher_fprintf, converts the message to UTF-16 by allocating through sqlite3_vmprintf and sqlite3_malloc — the same locked allocator.
  4. That allocation fails VirtualLock again, logs again, and the cycle repeats until the stack is exhausted.

Unix, Android, and Apple never loop: their sinks write with plain fprintf or the device logger and do not allocate through sqlite3_malloc.

The crash needs an allocation larger than the pre-locked startup heap (4.7.0's 48KB pool absorbs small ones) while the working-set quota is exhausted — for example preparing a moderately large CREATE TABLE on a busy machine or CI runner. Because the trigger is quota exhaustion, raising thread stack sizes does not help; we reproduced the crash with a 256 MiB stack.

Fix

Drop messages emitted while the same thread is already inside sqlcipher_log, via a Windows-only thread-local guard (__declspec(thread) on MSVC, __thread on MinGW). The recursion is same-thread by construction, so a thread-local flag is exactly sufficient, costs nothing on the hot path, and changes no behavior other than suppressing the infinitely-repeating message that was killing the process.

Reproduction

Observed with SQLCipher 4.14.0 community (via Rust libsqlite3-sys 0.38.1, vendored OpenSSL, MSVC) on windows-2025 GitHub runners, and present unchanged at current master. Steps:

  1. Open a file database, PRAGMA key, PRAGMA cipher_memory_security = ON, leave log settings at their defaults.
  2. Prepare a schema batch of a few KB (several CREATE TABLE statements with CHECK constraints).
  3. On a process whose working set is near its quota, the process dies with STATUS_STACK_OVERFLOW (exit 0xc00000fd) inside the prepare.

Under the same conditions the crash disappears with any one of: cipher_memory_security off, PRAGMA cipher_log_level = NONE, or pre-growing the working set with SetProcessWorkingSetSize — the last of which confirmed the quota mechanism.

The patched amalgamation compiles cleanly; the non-Windows paths are unchanged.

… locked allocator

On Windows, sqlcipher_fprintf converts console log output to UTF-16 by
allocating through sqlite3_vmprintf and sqlite3_malloc. When
cipher_memory_security is enabled, those allocations run through the
locked allocator, and sqlcipher_mlock logs a warning whenever VirtualLock
fails ("VirtualLock() returned 0 LastError=1453"), which commonly happens
once the process working-set quota is exhausted. Writing that warning
allocates through the same allocator, fails VirtualLock again, and logs
again, recursing until the process dies with STATUS_STACK_OVERFLOW.

With the default WARN-to-stderr log configuration this crashes any
Windows application that enables cipher_memory_security and performs an
allocation larger than the pre-locked startup heap while the working-set
quota is exhausted, for example preparing a moderately large CREATE TABLE
statement.

Drop log messages emitted while the same thread is already inside
sqlcipher_log. The guard is thread-local and Windows-only: the Unix,
Android, and Apple sinks write with plain fprintf or the device logger
and never allocate through sqlite3_malloc, so they cannot re-enter.
moodysalem added a commit to EkuboProtocol/wallet that referenced this pull request Aug 4, 2026
The root cause is fixed by the cipher_log_level pragma and recorded in
docs/windows-sqlcipher-overflow.md, now linking the upstream report:
sqlcipher/sqlcipher#602. The full library suite
passed on windows-2025 in the final diagnose run, so the temporary probe
tests, the open() instrumentation, and the diagnose workflow are gone. The
stack-floor regression test in lib.rs stays: it guards the RUST_MIN_STACK
plumbing permanently.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@sjlombardo

Copy link
Copy Markdown
Member

Hello @moodysalem - thank you for reporting this issue. We have confirmed the problem and it will be resolved in the next release of SQLCipher. Our fix uses a different approach than this PR by eliminating dynamic allocations in the UTF-16 conversion code, rather than tracking the log state. Even so, I will leave this PR open until the fix is released.

As an alternative workaround, SQLCipher 4.16 or higher should not exhibit this problem with default settings since WARN-level logs on lock failure were removed in that version.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants