Details
-
Bug
-
Status: Open (View Workflow)
-
Major
-
Resolution: Unresolved
-
12.3.3
-
None
-
Linux x86_64; Docker; official image
`mariadb@sha256:dd9b303aed4f4890ed09f766d8ca9ddfd176c0c6f6267feff53b3192ec65a979`;
server reports `12.3.3-MariaDB-ubu2404`, source revision
`83e909fc2a0dbc394b4b683fb3fa2d7dcf26cc5e`.
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.