Skip to content

[FUZZER/LOW] aes256cbc in legacy mode writes a journal_mode=WAL database that fails to reopen with its own key, after a single VACUUM #279

Closed as not planned

Description

@LucaCappelletti94

This is one of a handful of bugs identified via fuzzing, as described in #277 and as requested there I am opening them as issues. This one I would also label as low priority.

A brand-new, schema-less database keyed with aes256cbc, legacy=1, switched to journal_mode=WAL, and then VACUUMed once, closes without error, but cannot be reopened with the identical passphrase. Every statement on the second connection fails SQLITE_NOTADB, including sqlite_dbpage reads of page 1 itself.

It is similar to #278 as it shares a similar symptom but reproduce through different mechanisms. #278 needs aes128cbc under the default rollback journal, with a specific nine-VACUUM, two-table sequence. This one needs aes256cbc under journal_mode=WAL, reproducing with a single bare VACUUM and no CREATE TABLE at all. Mixing up their configurations to try and find common denominators does not reproduce any error at all.

#include <stdio.h>
#include <string.h>
#include "sqlite3mc_amalgamation.h"

static const unsigned char PASSWORD[] = {
    213, 7, 196, 255, 27, 150, 247, 25, 31, 96, 232, 195, 183, 169, 211, 101, 114, 94, 69, 18, 82,
    231, 140, 76, 154, 42, 148, 226, 255, 57, 48, 93,
};

static void run_sql(sqlite3 *db, const char *sql) {
    char *err = NULL;
    int rc = sqlite3_exec(db, sql, NULL, NULL, &err);
    if (rc != SQLITE_OK) {
        fprintf(stderr, "SQL FAILED (%d): %s -- %s\n", rc, sql, err ? err : "?");
        sqlite3_free(err);
    }
}

static void configure(sqlite3 *db) {
    int cipher = sqlite3mc_cipher_index("aes256cbc");
    sqlite3mc_config(db, "cipher", cipher);
    sqlite3mc_config_cipher(db, "aes256cbc", "legacy", 1);
    sqlite3_key(db, PASSWORD, (int) sizeof(PASSWORD));
}

int main(void) {
    remove("wal_repro.db");
    remove("wal_repro.db-wal");
    remove("wal_repro.db-shm");

    sqlite3 *db;
    sqlite3_open("wal_repro.db", &db);
    configure(db);
    run_sql(db, "PRAGMA journal_mode=WAL");
    run_sql(db, "VACUUM");
    sqlite3_close(db);

    sqlite3 *db2;
    sqlite3_open("wal_repro.db", &db2);
    configure(db2);
    char *err = NULL;
    int rc = sqlite3_exec(db2, "PRAGMA integrity_check", NULL, NULL, &err);
    printf("reopen integrity_check: rc=%d (%s) err=%s\n", rc, sqlite3_errstr(rc), err ? err : "none");
    sqlite3_free(err);
    sqlite3_close(db2);
    return 0;
}
$ cc -O0 -g -DSQLITE_TEMP_STORE=2 -DSQLITE_ENABLE_DBPAGE_VTAB -DSQLITE_DEFAULT_MEMSTATUS=0 -DSQLITE_CORE \
     -DHAVE_CIPHER_AES_128_CBC -DHAVE_CIPHER_AES_256_CBC -DHAVE_CIPHER_CHACHA20 -DHAVE_CIPHER_SQLCIPHER \
     -DHAVE_CIPHER_RC4 -DHAVE_CIPHER_ASCON128 -DHAVE_CIPHER_AEGIS \
     -o repro repro.c sqlite3mc_amalgamation.c -lpthread -ldl -lm
$ ./repro
reopen integrity_check: rc=26 (file is not a database) err=file is not a database
  • Needs journal_mode=WAL. The identical sequence under the default rollback journal never fails.
  • Needs aes256cbc specifically, and legacy=1 specifically. Neither aes128cbc/chacha20 nor legacy=0 reproduce.
  • mc_legacy_wal is irrelevant. The defect reproduces identically whether it is explicitly 0 or 1.
  • Needs exactly one VACUUM and nothing else. No CREATE TABLE, no transaction.
  • No sqlite3_rekey, no cipher change, no passphrase change anywhere in the sequence.

Ciao,
Luca

Activity

  1. utelle commented on Oct 3, 2026

    @utelle
    Owner

    This is one of a handful of bugs identified via fuzzing, as described in #277 and as requested there I am opening them as issues. This one I would also label as low priority.

    The reason for the failure is exactly the same as described for issue #278. It's the curse of the legacy mode.

    BTW, I doubt that any database file encrypted with aes128cbc or aes256cbc in legacy mode is practically in existence anymore nowadays. Back then when this format was replaced by the non-legacy mode there was no way to select the legacy mode other than using an older version of wxSQLite3. - The use of the cipher schemes aes128cbc and aes256cbc is strongly discouraged for new applications, not to mention legacy mode.

    A brand-new, schema-less database keyed with aes256cbc, legacy=1, switched to journal_mode=WAL, and then VACUUMed once, closes without error, but cannot be reopened with the identical passphrase. Every statement on the second connection fails SQLITE_NOTADB, including sqlite_dbpage reads of page 1 itself.

    As I see it the issue is not related to WAL journal mode. It could happen in all configurations. This time SQLite identifies a valid page size of 64 kB (because the bytes in the database file denote the value x0001 (= 64 KB)), but in reality the page size is 4 kB.

    As already said in my response in #278 the cipher schemes sqlcipher and chacha20 in legacy mode are also affected by this curse.

    Unfortunately, there is absolutely nothing that can be done to solve the issue, except for not using legacy mode.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions