Uploaded image for project: 'MariaDB Server'
  1. MariaDB Server
  2. MDEV-41126

Encrypted InnoDB bulk insert exposes BLOB plaintext through an anonymous temporary file

    XMLWordPrintable

Details

    • Bug
    • Status: Open (View Workflow)
    • Major
    • Resolution: Unresolved
    • 12.3.3
    • 11.4, 11.8, 12.3
    • None

    Description

      With `file_key_management` active and `innodb_encrypt_log=ON`, InnoDB encrypts
      the ordinary bulk-merge blocks but writes overflow BLOB bytes through a separate
      raw temporary-file path. During a successful bulk insert I can read the exact
      synthetic BLOB marker from an anonymous `/tmp/#... (deleted)` file descriptor
      owned by the running `mariadbd` process.

      The test below uses a fixed test key and a synthetic marker only. It does not
      use production data. It observes the descriptor while the statement is active,
      because the file is unlinked from the directory immediately after creation.

      Steps to reproduce

      set -eu
      IMAGE='mariadb@sha256:dd9b303aed4f4890ed09f766d8ca9ddfd176c0c6f6267feff53b3192ec65a979'
      C="mdev-mfu01-$$"
      OWNER="edbf-report-mfu01-$$"
      D=$(mktemp -d /tmp/mdev-mfu01.XXXXXX)
      cleanup() {
        if docker inspect --format '{{ index .Config.Labels "io.encryptiondbfuzz.owner" }}' \
             "$C" 2>/dev/null | grep -Fqx "$OWNER"; then
          docker rm -f "$C" >/dev/null 2>&1 || true
        fi
        rm -rf "$D"
      }
      trap cleanup EXIT
       
      printf '%s\n' \
        '1;00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF' \
        >"$D/keys.txt"
      chmod 0644 "$D/keys.txt"
       
      docker run --detach --name "$C" \
        --label "io.encryptiondbfuzz.owner=$OWNER" \
        --network none --read-only --cap-drop ALL \
        --security-opt no-new-privileges=true --pids-limit 160 \
        --memory 2g --cpus 1 --user 999:999 \
        --tmpfs /var/lib/mysql:rw,nosuid,nodev,size=512m,uid=999,gid=999 \
        --tmpfs /run/mysqld:rw,nosuid,nodev,size=16m,uid=999,gid=999 \
        --tmpfs /tmp:rw,nosuid,nodev,size=512m,uid=999,gid=999 \
        --mount "type=bind,src=$D/keys.txt,dst=/edbf/keys.txt,readonly" \
        --env MARIADB_ALLOW_EMPTY_ROOT_PASSWORD=1 "$IMAGE" \
        --plugin-load-add=file_key_management.so \
        --file-key-management-filename=/edbf/keys.txt \
        --innodb-encrypt-log=ON \
        --innodb-sort-buffer-size=1048576
       
      READY=0
      for I in $(seq 1 90); do
        if docker exec --user 999:999 "$C" mariadb --protocol=socket \
             --socket=/run/mysqld/mysqld.sock -uroot -NBe 'SELECT @@port' \
             2>/dev/null | grep -qx 3306; then
          READY=1
          break
        fi
        sleep 1
      done
      test "$READY" -eq 1
       
      db() {
        docker exec --user 999:999 "$C" mariadb --protocol=socket \
          --socket=/run/mysqld/mysqld.sock -uroot --batch --skip-column-names "$@"
      }
       
      echo 'version_and_encryption_state:'
      db -e "SELECT VERSION(),@@innodb_encrypt_log,
        (SELECT PLUGIN_STATUS FROM INFORMATION_SCHEMA.PLUGINS
         WHERE PLUGIN_NAME='file_key_management')"
      db -e 'CREATE DATABASE d;
        CREATE TABLE d.dst(id INT PRIMARY KEY,b LONGBLOB) ENGINE=InnoDB'
       
      # Inspect only anonymous fds owned by this test's mariadbd process.
      docker exec --user 999:999 "$C" sh -c '
      pid=$(pgrep -o mariadbd)
      i=0
      while [ "$i" -lt 1200 ]; do
        for f in /proc/$pid/fd/*; do
          target=$(readlink "$f" 2>/dev/null || true)
          case "$target" in
            *deleted*)
              if timeout 1 head -c 262144 "$f" 2>/dev/null |
                   grep -aFqm1 EDBF_SYNTHETIC_BLOB_SPILL_4C2719; then
                echo "FOUND:$f:$target"
                exit 0
              fi
              ;;
          esac
        done
        i=$((i+1))
        sleep 0.02
      done
      echo NOT_FOUND
      exit 3
      ' >"$D/monitor.out" 2>"$D/monitor.err" &
      MONITOR_PID=$!
       
      echo 'inserted_rows:'
      db -e "SET SESSION unique_checks=0,foreign_key_checks=0,autocommit=0;
        INSERT INTO d.dst
          SELECT seq,CONCAT('EDBF_SYNTHETIC_BLOB_SPILL_4C2719',REPEAT('Q',32768))
          FROM seq_1_to_1000;
        COMMIT;
        SELECT COUNT(*) FROM d.dst"
       
      set +e
      wait "$MONITOR_PID"
      MONITOR_RC=$?
      set -e
      cat "$D/monitor.out"
      cat "$D/monitor.err" >&2
      test "$MONITOR_RC" -eq 0
       
      echo 'rows_after_commit:'
      db -e 'SELECT COUNT(*) FROM d.dst'
      

      Actual result

      The exact descriptor number and temporary filename vary. The authoritative run
      produced:

      version_and_encryption_state:
      12.3.3-MariaDB-ubu2404  1  ACTIVE
      inserted_rows:
      1000
      FOUND:/proc/1/fd/55:/tmp/#48 (deleted)
      rows_after_commit:
      1000
      

      The insert and commit both succeeded, and the server remained healthy. The
      marker was read from the live anonymous backing file, not from SQL output or a
      named file left after the statement.

      Expected result

      When the InnoDB bulk-merge path encrypts its temporary merge blocks under
      `innodb_encrypt_log=ON`, overflow BLOB bytes belonging to the same operation
      should not be written as readable plaintext to a sibling temporary file.

      Impact and boundary

      This can expose inserted BLOB contents to an operating-system principal that
      can inspect the database process's open descriptors or the backing storage.
      The test demonstrates live plaintext exposure only. It does not demonstrate a
      remote SQL read, a persistent named plaintext file, post-crash recovery of the
      file, or a server crash. Only 12.3.3 was tested. The result is
      researcher-reproduced and not yet maintainer-confirmed.

      Implementation observation

      In `storage/innobase/row/row0merge.cc`, `row_merge_write()` encrypts ordinary
      merge blocks when `srv_encrypt_log` is enabled. The BLOB overflow path instead
      uses `row_merge_write_blob_to_tmp_file()` and `os_file_write(IORequestWrite,
      "(bulk insert)", ...)` directly, then reads the bytes back with `os_file_read()`.
      No equivalent encryption/decryption step is present on that BLOB path.

      Attachments

        Activity

          People

            tafzeel Mohammad Tafzeel Shams
            csfuzz csfuzz
            Votes:
            0 Vote for this issue
            Watchers:
            2 Start watching this issue

            Dates

              Created:
              Updated:

              Git Integration

                Error rendering 'com.xiplink.jira.git.jira_git_plugin:git-issue-webpanel'. Please contact your Jira administrators.