Details
-
Bug
-
Status: Open (View Workflow)
-
Critical
-
Resolution: Unresolved
-
12.3.3
-
None
-
Linux x86_64; Docker; official image
`mariadb@sha256:dd9b303aed4f4890ed09f766d8ca9ddfd176c0c6f6267feff53b3192ec65a979`;
both nodes report `12.3.3-MariaDB-ubu2404`, source revision
`83e909fc2a0dbc394b4b683fb3fa2d7dcf26cc5e`
Description
The shipped `wsrep_notify.sh` constructs client options as shell text and then
passes them through `eval`. A configured password containing an apostrophe and
space is therefore parsed a second time. In a real two-node Galera state
transition, the normal password is delivered as one `-pplainpass` argument,
but `x' y='z` becomes two arguments, `-px` and `y=z`.
The reproducer uses a no-network argv recorder instead of a database client, so
no credential is sent and no command is executed from the crafted value. The
callback itself is invoked by the server after the peer joins.
Steps to reproduce
Run on a disposable Docker host. The embedded helper is a reduced copy of the
affected option-building and `eval` block; the two fixed synthetic values make
the argument boundary visible.
#!/usr/bin/env bash
|
set -euo pipefail
|
IMAGE='mariadb@sha256:dd9b303aed4f4890ed09f766d8ca9ddfd176c0c6f6267feff53b3192ec65a979'
|
OWNER="mdev-mcf202-$$"
|
TMP=$(mktemp -d)
|
NAMES=()
|
cleanup() {
|
for C in "${NAMES[@]}"; do
|
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
|
done
|
rm -rf -- "$TMP"
|
}
|
trap cleanup EXIT
|
|
|
cat >"$TMP/fake_mysql.sh" <<'EOF'
|
#!/bin/sh
|
: > /tmp/notify-argv
|
for arg in "$@"; do printf '%s\n' "$arg" >> /tmp/notify-argv; done
|
cat >/tmp/notify-sql
|
EOF
|
|
|
make_helper() {
|
CASE=$1
|
if [ "$CASE" = control ]; then PASSWORD_LINE="PSWD='plainpass'"; else PASSWORD_LINE="PSWD=\"x' y='z\""; fi
|
{
|
printf '%s\n' '#!/bin/sh' "USER='root'" "$PASSWORD_LINE" \
|
"CLIENT='/edbf/fake_mysql.sh'" "STATUS=''"
|
cat <<'EOF'
|
while [ "$#" -gt 0 ]; do
|
case "$1" in --status) STATUS=$2; shift;; esac
|
shift
|
done
|
case "$STATUS" in
|
joined|donor|synced)
|
printf '%s\n' 'SELECT 1' | eval "$CLIENT" -B "-u'$USER'"${PSWD:+" -p'$PSWD'"} "-h'127.0.0.1'" "-P3306"
|
;;
|
esac
|
EOF
|
} >"$TMP/notify-$CASE.sh"
|
cat >"$TMP/invoke-$CASE.sh" <<EOF
|
#!/bin/sh
|
printf '%s\n' "\$*" >> /tmp/notify-invocations-$CASE
|
exec /bin/sh /edbf/notify-$CASE.sh "\$@"
|
EOF
|
}
|
make_helper control
|
make_helper trigger
|
chmod 755 "$TMP" "$TMP"/*.sh
|
|
|
run_case() {
|
CASE=$1
|
NODE="mdev-mcf202-$CASE-node-$$"
|
PEER="mdev-mcf202-$CASE-peer-$$"
|
NAMES+=("$NODE" "$PEER")
|
COMMON=(--label "io.encryptiondbfuzz.owner=$OWNER" --read-only --cap-drop ALL
|
--security-opt no-new-privileges=true --pids-limit 224 --memory 1700m
|
--cpus 1 --user 999:999
|
--tmpfs /var/lib/mysql:rw,nosuid,nodev,size=700m,uid=999,gid=999
|
--tmpfs /run/mysqld:rw,nosuid,nodev,size=16m,uid=999,gid=999
|
--tmpfs /tmp:rw,nosuid,nodev,size=256m,uid=999,gid=999
|
--env MARIADB_ALLOW_EMPTY_ROOT_PASSWORD=1
|
--mount "type=bind,src=$TMP,dst=/edbf,readonly")
|
|
|
docker run -d --name "$NODE" --network none "${COMMON[@]}" "$IMAGE" \
|
--wsrep-on=ON --wsrep-provider=/usr/lib/galera/libgalera_smm.so \
|
--wsrep-cluster-address=gcomm:// --wsrep-new-cluster \
|
--wsrep-node-address=127.0.0.1:4567 --wsrep-node-name="$CASE-node" \
|
--wsrep-provider-options=gmcast.listen_addr=tcp://127.0.0.1:4567 \
|
--wsrep-notify-cmd="/bin/sh /edbf/invoke-$CASE.sh" \
|
--bind-address=127.0.0.1 >/dev/null
|
docker run -d --name "$PEER" --network "container:$NODE" "${COMMON[@]}" "$IMAGE" \
|
--port=3307 --bind-address=127.0.0.1 --wsrep-on=ON \
|
--wsrep-provider=/usr/lib/galera/libgalera_smm.so \
|
--wsrep-cluster-address=gcomm://127.0.0.1:4567 \
|
--wsrep-node-address=127.0.0.1:4568 --wsrep-node-name="$CASE-peer" \
|
--wsrep-provider-options=gmcast.listen_addr=tcp://127.0.0.1:4568 >/dev/null
|
|
|
READY=0
|
for I in $(seq 1 120); do
|
STATE=$(docker exec --user 999:999 "$NODE" mariadb --protocol=socket \
|
--socket=/run/mysqld/mysqld.sock -uroot -NBe \
|
"SELECT CONCAT(@@wsrep_on,'|',(SELECT VARIABLE_VALUE FROM INFORMATION_SCHEMA.GLOBAL_STATUS WHERE VARIABLE_NAME='WSREP_READY'),'|',(SELECT VARIABLE_VALUE FROM INFORMATION_SCHEMA.GLOBAL_STATUS WHERE VARIABLE_NAME='WSREP_LOCAL_STATE_COMMENT'),'|',(SELECT VARIABLE_VALUE FROM INFORMATION_SCHEMA.GLOBAL_STATUS WHERE VARIABLE_NAME='WSREP_CLUSTER_SIZE'))" 2>/dev/null || true)
|
if [ "$STATE" = 'ON|ON|Synced|2' ]; then READY=1; break; fi
|
sleep 1
|
done
|
test "$READY" -eq 1
|
echo "$CASE cluster=$STATE"
|
docker exec --user 999:999 "$NODE" cat /tmp/notify-argv
|
docker rm -f "$PEER" "$NODE" >/dev/null
|
NAMES=()
|
}
|
|
|
run_case control
|
run_case trigger
|
Actual result
control cluster=ON|ON|Synced|2
|
-B
|
-uroot
|
-pplainpass
|
-h127.0.0.1
|
-P3306
|
trigger cluster=ON|ON|Synced|2
|
-B
|
-uroot
|
-px
|
y=z
|
-h127.0.0.1
|
-P3306
|
The trigger password is no longer one client argument. Both servers remain
running; no server crash was observed.
Expected result
Credential and TLS configuration values should be passed to the client without
shell re-evaluation. A password containing an apostrophe or whitespace must
remain one `-p...` argument, or the helper should reject it before invoking the
client.