Skip to content

Intermittent "database disk image is malformed" after ROLLBACK TO undoes a DROP TRIGGER (4.12.0 – 4.19.0) #635

Open

Description

@Alpes71

Expected Behavior

After SAVEPOINT s; DROP TRIGGER t; ROLLBACK TO s; RELEASE s; on an encrypted database, the
schema is restored and the connection keeps working. This is what happens with the same
sequence on an unencrypted database, and with a full BEGIN … ROLLBACK on an encrypted one.

Actual Behavior

Roughly once in every 1000 runs, a statement executed after the ROLLBACK TO fails with
database disk image is malformed (SQLITE_CORRUPT, 11). The failing statement is either the
next DROP TRIGGER inside a new savepoint or a plain SELECT … FROM sqlite_schema. It never
fails before the first ROLLBACK TO has run.

The database file is not damaged. After closing the connection and reopening the file with
the same key, PRAGMA integrity_check returns ok and all triggers are present. This held for
every failure we observed. It looks like stale in-memory state (page cache or schema) after
the partial rollback.

The failure depends on the encrypted content. Each page is encrypted with a random IV, so the
same script fails or passes from run to run, and we could not make it deterministic. Results
with the script below (shell built from the release tags, Linux x86_64, OpenSSL 3.5.5):

SQLCipher (SQLite) Variant Iterations Failures
4.19.0 (3.53.4) SAVEPOINT; DROP TRIGGER; SELECT sqlite_schema; ROLLBACK TO; RELEASE 20000 21
4.19.0 (3.53.4) same without the SELECT 20000 7
4.19.0 (3.53.4) BEGIN; DROP TRIGGER; SELECT; ROLLBACK (control) 10000 0
4.19.0 (3.53.4) savepoint sequence without a key (control) 20000 0
4.13.0 (3.51.2) savepoint sequence 10000 7
4.12.0 (3.51.1) savepoint sequence 10000 6

We first saw it through the Python binding sqlcipher3-wheels 0.5.7, which bundles 4.12.0
with OpenSSL 3.6.0. It shows the same rate on Linux and on Windows 11. With that binding, a
ROLLBACK TO that only undoes data changes (UPDATE, PRAGMA user_version) did not fail in
5000 runs: the schema change seems to be required.

Steps to Reproduce

  1. Build the shell from a release tag, as in the README:

    git clone --depth 1 --branch v4.19.0 https://github.com/sqlcipher/sqlcipher.git && cd sqlcipher
    ./configure --with-tempstore=yes CFLAGS="-DSQLITE_HAS_CODEC -DSQLITE_EXTRA_INIT=sqlcipher_extra_init -DSQLITE_EXTRA_SHUTDOWN=sqlcipher_extra_shutdown" LDFLAGS="-lcrypto"
    make sqlite3
    
  2. Run bash repro.sh ./sqlite3 20000 savepoint. The script is below, released under CC0,
    so feel free to use it in your tests.

  3. Each iteration:

    • creates a fresh database with a random raw key (PRAGMA key = "x'…'");
    • creates 10 tables and 13 triggers in one transaction;
    • runs SAVEPOINT s; DROP TRIGGER dN; SELECT count(*) FROM sqlite_schema WHERE type = 'trigger'; ROLLBACK TO s; RELEASE s; for 4 triggers;
    • reads sqlite_schema once more.

    Failures print the error and the result of reopening the file:

    [303] Parse error near line 31: database disk image is malformed (11) | reopened: integrity_check, trigger count = ok 13
    
repro.sh (CC0)
#!/usr/bin/env bash
# Intermittent "database disk image is malformed" after ROLLBACK TO undoes a DROP TRIGGER
# on an encrypted database. Uses only the SQLCipher command-line shell and bash.
#
# Usage: repro.sh <path to the sqlcipher shell> [iterations] [variant]
#   variant: savepoint (default) | no_read | transaction (control) |
#            plaintext (control: the savepoint sequence with no key, i.e. not encrypted)
#
# Each iteration creates a fresh database with a random 256-bit raw key, creates 10 tables
# and 13 triggers in one transaction, then drops and restores 4 triggers. The shell runs
# with -bail, so the first error stops it and is printed. On failure the database is
# reopened with the same key and checked with PRAGMA integrity_check.
#
# License: CC0 1.0 Universal (public domain dedication),
# https://creativecommons.org/publicdomain/zero/1.0/ - use it freely, e.g. in your tests.
set -euo pipefail

SQL_SHELL="${1:?path to the sqlcipher shell}"
N="${2:-10000}"
VARIANT="${3:-savepoint}"
WORKDIR="$(mktemp -d)"
trap 'rm -rf "${WORKDIR:?}"' EXIT

READ="SELECT count(*) FROM sqlite_schema WHERE type = 'trigger';"

schema() {
  echo "BEGIN;"
  for i in $(seq 0 9); do
    echo "CREATE TABLE t$i (id INTEGER PRIMARY KEY, a TEXT NOT NULL, b BLOB, c INTEGER);"
  done
  local events=(UPDATE DELETE INSERT INSERT)
  for n in 0 1 2 3; do
    echo "CREATE TRIGGER d$n BEFORE ${events[$n]} ON t0 WHEN $n >= 0 BEGIN SELECT RAISE(ABORT, 'trigger $n'); END;"
  done
  for i in $(seq 1 9); do
    echo "CREATE TRIGGER e$i BEFORE UPDATE OF a ON t$i BEGIN SELECT RAISE(ABORT, 'table $i'); END;"
  done
  echo "COMMIT;"
}

script() {
  [ "$VARIANT" = plaintext ] || echo "PRAGMA key = \"x'$1'\";"
  schema
  for n in 0 1 2 3; do
    case "$VARIANT" in
      transaction) echo "BEGIN; DROP TRIGGER d$n; $READ ROLLBACK;" ;;
      no_read) echo "SAVEPOINT s; DROP TRIGGER d$n; ROLLBACK TO s; RELEASE s;" ;;
      *) echo "SAVEPOINT s; DROP TRIGGER d$n; $READ ROLLBACK TO s; RELEASE s;" ;;
    esac
  done
  echo "$READ"
}

"$SQL_SHELL" :memory: <<< "PRAGMA key = \"x'00'\"; SELECT 'sqlite ' || sqlite_version(); PRAGMA cipher_version; PRAGMA cipher_provider_version;" | tail -n +2 | tr '\n' ' '
echo "| $(uname -sr)"

failures=0
for i in $(seq 1 "$N"); do
  db="${WORKDIR:?}/test-$i.db"
  key="$(od -An -tx1 -N32 /dev/urandom | tr -d ' \n')"
  if ! err="$(script "$key" | "$SQL_SHELL" -bail "$db" 2>&1 >/dev/null)"; then
    failures=$((failures + 1))
    if [ "$VARIANT" = plaintext ]; then
      reopened="$(printf 'PRAGMA integrity_check;\n%s\n' "$READ" | "$SQL_SHELL" "$db" 2>&1 | tr '\n' ' ')"
    else
      reopened="$(printf 'PRAGMA key = "x%s";\nPRAGMA integrity_check;\n%s\n' "'$key'" "$READ" \
        | "$SQL_SHELL" "$db" 2>&1 | tail -n +2 | tr '\n' ' ')"
    fi
    echo "[$i] ${err//$'\n'/ } | reopened: integrity_check, trigger count = $reopened"
  fi
  rm -f "${db:?}"
done
echo "$VARIANT: $N iterations, $failures failures"

SQLCipher version: 4.19.0 community (also 4.12.0 and 4.13.0); SQLite 3.53.4; OpenSSL 3.5.5;
Linux x86_64 (WSL2, kernel 6.18). Default settings apart from the raw key.

Activity

  1. sjlombardo commented on Nov 10, 2026

    @sjlombardo
    Member

    Hello @Alpes71 - thanks for the clear report and reproduction steps. We're investigating.

  2. sjlombardo commented on Nov 13, 2026

    @sjlombardo
    Member

    Hello @Alpes71 we've identified and resolve the issue here and the fix will be included in the next release

  3. Alpes71 commented on Nov 13, 2026

    @Alpes71
    Author

    Hi @sjlombardo Thanks a lot for the quick fix! We’ll re-run our reproducer once the next release is out and report back here.

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions