Description:
`generate_random_password()` calls `RAND_bytes()` but does not check its return
value. It then translates every byte in the output buffer into the plaintext
password returned by `CREATE USER ... IDENTIFIED BY RANDOM PASSWORD`.
With exactly one `RAND_bytes(73)` failure injected after server startup, MySQL
8.4.11 returned a deterministic 73-character password and installed it as a
working account credential. The test injector zero-fills the failed output only
to make this missing error check observable: byte value zero maps to character
`1`, so the returned password is 73 `1` characters. A normal control password
contained 48 distinct characters.
This report does not claim that every OpenSSL failure produces zero bytes. The
security defect is that MySQL consumes and installs the output buffer after the
CSPRNG has explicitly reported failure, instead of aborting password creation.
How to repeat:
Use a disposable host with Docker and GCC. Save the following complete source
as `my16_rand_fail.c`:
```c
#define _GNU_SOURCE
#include <dlfcn.h>
#include <errno.h>
#include <fcntl.h>
#include <stdatomic.h>
#include <stddef.h>
#include <string.h>
#include <unistd.h>
typedef int (*rand_bytes_fn)(unsigned char *, int);
static rand_bytes_fn real_rand_bytes;
static atomic_int delivered;
int RAND_bytes(unsigned char *buffer, int num) {
if (real_rand_bytes == NULL)
real_rand_bytes = (rand_bytes_fn)dlsym(RTLD_NEXT, "RAND_bytes");
if (real_rand_bytes == NULL) {
errno = ENOSYS;
return 0;
}
if (num == 73 && access("/tmp/my16-arm", F_OK) == 0 &&
atomic_exchange(&delivered, 1) == 0) {
static const char message[] = "RAND_bytes failed size=73\n";
memset(buffer, 0, (size_t)num);
int fd = open("/tmp/my16-rand-failed", O_WRONLY | O_CREAT | O_TRUNC, 0600);
if (fd >= 0) {
ssize_t ignored = write(fd, message, sizeof(message) - 1);
(void)ignored;
(void)close(fd);
}
errno = EIO;
return 0;
}
return real_rand_bytes(buffer, num);
}
```
Compile it:
```bash
gcc -shared -fPIC -O2 -Wall -Wextra -Werror \
-o my16_rand_fail.so my16_rand_fail.c -ldl
```
Start a disposable server. The image is pinned to the exact tested digest and
the injector is inactive until `/tmp/my16-arm` exists:
```bash
IMG='mysql@sha256:85b9bf2e29cf836ecb8c2a15a935d4ba0c606631dff1dd79531a11983c638f2a'
C="mysql-my16-poc-$$"
SO="$(pwd)/my16_rand_fail.so"
docker run -d --name "$C" --label "edbf.owner=$C" \
--network none --read-only --cap-drop ALL \
--security-opt no-new-privileges=true --pids-limit 512 \
--memory 2048m --cpus 2 --user 999:999 \
--tmpfs /var/lib/mysql:rw,noexec,nosuid,nodev,size=1024m,uid=999,gid=999 \
--tmpfs /var/run/mysqld:rw,noexec,nosuid,nodev,size=16m,uid=999,gid=999 \
--tmpfs /tmp:rw,noexec,nosuid,nodev,size=128m,uid=999,gid=999 \
--mount "type=bind,src=$SO,dst=/opt/my16_rand_fail.so,readonly" \
-e MYSQL_ALLOW_EMPTY_PASSWORD=yes \
-e LD_PRELOAD=/opt/my16_rand_fail.so \
"$IMG" --skip-name-resolve --log-error=/var/lib/mysql/error.log
trap 'docker rm -f "$C" >/dev/null 2>&1 || true' EXIT
for i in $(seq 1 120); do
test "$(docker exec "$C" mysql -uroot \
--socket=/var/run/mysqld/mysqld.sock -Nse 'SELECT @@port' 2>/dev/null)" = 3306 && break
sleep 1
done
test "$(docker exec "$C" mysql -uroot \
--socket=/var/run/mysqld/mysqld.sock -Nse 'SELECT @@port')" = 3306
```
Create one normal control account, arm the injector, and create the faulted
account. The third tab-separated column is the generated plaintext password:
```bash
CONTROL_LINE=$(docker exec "$C" mysql -uroot \
--socket=/var/run/mysqld/mysqld.sock --batch --raw --skip-column-names \
-e "SET SESSION generated_random_password_length=73;
CREATE USER 'my16_control'@'localhost' IDENTIFIED BY RANDOM PASSWORD;")
IFS=$'\t' read -r _ _ CONTROL_PASSWORD _ <<< "$CONTROL_LINE"
printf 'control length=%s distinct=%s\n' "${#CONTROL_PASSWORD}" \
"$(printf %s "$CONTROL_PASSWORD" | fold -w1 | sort -u | wc -l)"
docker exec "$C" touch /tmp/my16-arm
FAULT_LINE=$(docker exec "$C" mysql -uroot \
--socket=/var/run/mysqld/mysqld.sock --batch --raw --skip-column-names \
-e "SET SESSION generated_random_password_length=73;
CREATE USER 'my16_fault'@'localhost' IDENTIFIED BY RANDOM PASSWORD;")
IFS=$'\t' read -r _ _ FAULT_PASSWORD _ <<< "$FAULT_LINE"
printf 'fault length=%s all_ones=%s\n' "${#FAULT_PASSWORD}" \
"$([ "$FAULT_PASSWORD" = "$(printf '%073d' 0 | tr 0 1)" ] && echo yes || echo no)"
docker exec "$C" cat /tmp/my16-rand-failed
docker exec "$C" mysql -umy16_fault "-p$FAULT_PASSWORD" \
--socket=/var/run/mysqld/mysqld.sock -Nse 'SELECT CURRENT_USER()'
```
## Actual result
The official 8.4.11 run completed successfully and produced:
```text
control length=73 distinct=48
fault length=73 all_ones=yes
RAND_bytes failed size=73
my16_fault@localhost
```
The failed call was delivered exactly once. `CREATE USER` returned success,
the returned password was 73 `1` characters, and that password authenticated.
The server remained healthy. The retained machine record has SHA-256
`f6db4d8d856f22ebede8a168b5507b96e67546ba2cc91ca5981752ef1b1f3e9b`.
## Expected result
If `RAND_bytes()` reports failure, generated-password DDL should fail with an
explicit error. MySQL must not translate, return, hash, or install bytes from
the failed output buffer as an account credential.
Description: `generate_random_password()` calls `RAND_bytes()` but does not check its return value. It then translates every byte in the output buffer into the plaintext password returned by `CREATE USER ... IDENTIFIED BY RANDOM PASSWORD`. With exactly one `RAND_bytes(73)` failure injected after server startup, MySQL 8.4.11 returned a deterministic 73-character password and installed it as a working account credential. The test injector zero-fills the failed output only to make this missing error check observable: byte value zero maps to character `1`, so the returned password is 73 `1` characters. A normal control password contained 48 distinct characters. This report does not claim that every OpenSSL failure produces zero bytes. The security defect is that MySQL consumes and installs the output buffer after the CSPRNG has explicitly reported failure, instead of aborting password creation. How to repeat: Use a disposable host with Docker and GCC. Save the following complete source as `my16_rand_fail.c`: ```c #define _GNU_SOURCE #include <dlfcn.h> #include <errno.h> #include <fcntl.h> #include <stdatomic.h> #include <stddef.h> #include <string.h> #include <unistd.h> typedef int (*rand_bytes_fn)(unsigned char *, int); static rand_bytes_fn real_rand_bytes; static atomic_int delivered; int RAND_bytes(unsigned char *buffer, int num) { if (real_rand_bytes == NULL) real_rand_bytes = (rand_bytes_fn)dlsym(RTLD_NEXT, "RAND_bytes"); if (real_rand_bytes == NULL) { errno = ENOSYS; return 0; } if (num == 73 && access("/tmp/my16-arm", F_OK) == 0 && atomic_exchange(&delivered, 1) == 0) { static const char message[] = "RAND_bytes failed size=73\n"; memset(buffer, 0, (size_t)num); int fd = open("/tmp/my16-rand-failed", O_WRONLY | O_CREAT | O_TRUNC, 0600); if (fd >= 0) { ssize_t ignored = write(fd, message, sizeof(message) - 1); (void)ignored; (void)close(fd); } errno = EIO; return 0; } return real_rand_bytes(buffer, num); } ``` Compile it: ```bash gcc -shared -fPIC -O2 -Wall -Wextra -Werror \ -o my16_rand_fail.so my16_rand_fail.c -ldl ``` Start a disposable server. The image is pinned to the exact tested digest and the injector is inactive until `/tmp/my16-arm` exists: ```bash IMG='mysql@sha256:85b9bf2e29cf836ecb8c2a15a935d4ba0c606631dff1dd79531a11983c638f2a' C="mysql-my16-poc-$$" SO="$(pwd)/my16_rand_fail.so" docker run -d --name "$C" --label "edbf.owner=$C" \ --network none --read-only --cap-drop ALL \ --security-opt no-new-privileges=true --pids-limit 512 \ --memory 2048m --cpus 2 --user 999:999 \ --tmpfs /var/lib/mysql:rw,noexec,nosuid,nodev,size=1024m,uid=999,gid=999 \ --tmpfs /var/run/mysqld:rw,noexec,nosuid,nodev,size=16m,uid=999,gid=999 \ --tmpfs /tmp:rw,noexec,nosuid,nodev,size=128m,uid=999,gid=999 \ --mount "type=bind,src=$SO,dst=/opt/my16_rand_fail.so,readonly" \ -e MYSQL_ALLOW_EMPTY_PASSWORD=yes \ -e LD_PRELOAD=/opt/my16_rand_fail.so \ "$IMG" --skip-name-resolve --log-error=/var/lib/mysql/error.log trap 'docker rm -f "$C" >/dev/null 2>&1 || true' EXIT for i in $(seq 1 120); do test "$(docker exec "$C" mysql -uroot \ --socket=/var/run/mysqld/mysqld.sock -Nse 'SELECT @@port' 2>/dev/null)" = 3306 && break sleep 1 done test "$(docker exec "$C" mysql -uroot \ --socket=/var/run/mysqld/mysqld.sock -Nse 'SELECT @@port')" = 3306 ``` Create one normal control account, arm the injector, and create the faulted account. The third tab-separated column is the generated plaintext password: ```bash CONTROL_LINE=$(docker exec "$C" mysql -uroot \ --socket=/var/run/mysqld/mysqld.sock --batch --raw --skip-column-names \ -e "SET SESSION generated_random_password_length=73; CREATE USER 'my16_control'@'localhost' IDENTIFIED BY RANDOM PASSWORD;") IFS=$'\t' read -r _ _ CONTROL_PASSWORD _ <<< "$CONTROL_LINE" printf 'control length=%s distinct=%s\n' "${#CONTROL_PASSWORD}" \ "$(printf %s "$CONTROL_PASSWORD" | fold -w1 | sort -u | wc -l)" docker exec "$C" touch /tmp/my16-arm FAULT_LINE=$(docker exec "$C" mysql -uroot \ --socket=/var/run/mysqld/mysqld.sock --batch --raw --skip-column-names \ -e "SET SESSION generated_random_password_length=73; CREATE USER 'my16_fault'@'localhost' IDENTIFIED BY RANDOM PASSWORD;") IFS=$'\t' read -r _ _ FAULT_PASSWORD _ <<< "$FAULT_LINE" printf 'fault length=%s all_ones=%s\n' "${#FAULT_PASSWORD}" \ "$([ "$FAULT_PASSWORD" = "$(printf '%073d' 0 | tr 0 1)" ] && echo yes || echo no)" docker exec "$C" cat /tmp/my16-rand-failed docker exec "$C" mysql -umy16_fault "-p$FAULT_PASSWORD" \ --socket=/var/run/mysqld/mysqld.sock -Nse 'SELECT CURRENT_USER()' ``` ## Actual result The official 8.4.11 run completed successfully and produced: ```text control length=73 distinct=48 fault length=73 all_ones=yes RAND_bytes failed size=73 my16_fault@localhost ``` The failed call was delivered exactly once. `CREATE USER` returned success, the returned password was 73 `1` characters, and that password authenticated. The server remained healthy. The retained machine record has SHA-256 `f6db4d8d856f22ebede8a168b5507b96e67546ba2cc91ca5981752ef1b1f3e9b`. ## Expected result If `RAND_bytes()` reports failure, generated-password DDL should fail with an explicit error. MySQL must not translate, return, hash, or install bytes from the failed output buffer as an account credential.