From e97e8b1d330102a94506884df3140337cb503328 Mon Sep 17 00:00:00 2001 From: Florian Egger Date: Mon, 7 Sep 2026 12:37:38 +0200 Subject: [PATCH] whisper-server: validate model downloads (size + ggml magic), self-heal poisoned files The first server deployment downloaded only 1101 bytes (a redirect/error page) and the entrypoint promoted it to ggml-large-v3.bin, after which every restart skipped the download and the server ran without a valid model. Now both fresh downloads and existing files are validated (minimum 50 MB + the ggml magic bytes); invalid files are logged, deleted, and re-downloaded, and a failed download dumps the first 400 bytes of what was actually received for diagnosis before exiting. Validated locally: a poisoned model file is detected, removed, and a valid one re-downloaded; inference served correctly afterwards. --- server/whisper-server/entrypoint.sh | 44 +++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/server/whisper-server/entrypoint.sh b/server/whisper-server/entrypoint.sh index ec97168..122feec 100644 --- a/server/whisper-server/entrypoint.sh +++ b/server/whisper-server/entrypoint.sh @@ -2,6 +2,11 @@ # Downloads the configured GGML model on first start, then runs whisper-server. # Uses the GPU (Vulkan) automatically when one is present; set NO_GPU=1 # to force CPU. +# +# Both the freshly downloaded file and any existing file are validated +# (minimum size + ggml magic bytes) so a truncated download or a saved +# error/redirect page can never be mistaken for a model — a poisoned +# file is deleted and re-downloaded instead. set -eu MODEL="${MODEL:-small}" @@ -10,14 +15,43 @@ HOST="${HOST:-0.0.0.0}" PORT="${PORT:-8085}" MODEL_DIR="${MODEL_DIR:-/models}" FILE="$MODEL_DIR/ggml-$MODEL.bin" +URL="https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-$MODEL.bin" + +# smallest supported model (ggml-tiny.bin) is ~75 MB +MIN_SIZE=50000000 +GGML_MAGIC="$(printf 'lmgg')" # GGML_FILE_MAGIC 0x67676d6c, little-endian + +is_valid_model() { + [ -s "$1" ] || return 1 + [ "$(stat -c%s "$1")" -ge "$MIN_SIZE" ] || return 1 + [ "$(head -c 4 "$1")" = "$GGML_MAGIC" ] || return 1 + return 0 +} mkdir -p "$MODEL_DIR" -if [ ! -s "$FILE" ]; then - echo "downloading ggml-$MODEL.bin to $FILE ..." - curl -fL --retry 3 -C - -o "$FILE.part" \ - "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-$MODEL.bin" + +if ! is_valid_model "$FILE" && [ -e "$FILE" ]; then + echo "existing $FILE is invalid ($(stat -c%s "$FILE") bytes) — " \ + "deleting and re-downloading" +fi + +if ! is_valid_model "$FILE"; then + rm -f "$FILE" "$FILE.part" + echo "downloading $URL to $FILE ..." + curl -fLS --retry 3 --retry-delay 5 -C - -o "$FILE.part" "$URL" + + if ! is_valid_model "$FILE.part"; then + size="$(stat -c%s "$FILE.part" 2>/dev/null || echo 0)" + echo "ERROR: download is not a valid model ($size bytes)." + echo "------- first 400 bytes of the response (probably an error/redirect page):" + head -c 400 "$FILE.part" + echo "" + echo "------- end of response" + rm -f "$FILE.part" + exit 1 + fi mv "$FILE.part" "$FILE" - echo "done." + echo "downloaded $(stat -c%s "$FILE") bytes, magic ok." fi GPU_FLAGS=""