mirror of
https://github.com/unslothai/unsloth.git
synced 2026-07-09 15:58:41 +00:00
Studio: stop handing CI/user secrets to downloaded llama.cpp binaries (#6696)
Some checks are pending
Core / Core (HF=default + TRL=default) (push) Waiting to run
Core / Core (HF=4.57.6 + TRL<1) (push) Waiting to run
Core / Core (HF=latest + TRL=latest) (push) Waiting to run
Core / llama.cpp build + smoke (push) Waiting to run
Lint CI / Source lint (Python + shell + YAML + JSON + safety nets) (push) Waiting to run
MLX CI on Mac M1 / dispatch (push) Waiting to run
Security audit / pip scan-packages :: extras (push) Waiting to run
Security audit / pip scan-packages :: studio (push) Waiting to run
Security audit / pip scan-packages :: hf-stack (push) Waiting to run
Security audit / advisory audit (pip + npm + cargo) (push) Waiting to run
Security audit / npm scan-packages (Studio frontend tarballs) (push) Waiting to run
Security audit / workflow-trigger lint (pull_request_target / cache-poisoning) (push) Waiting to run
Security audit / pytest tests/security (push) Waiting to run
Security audit / npm provenance + new install-script diff (push) Waiting to run
Studio API CI / Studio API & Auth Tests (push) Waiting to run
Backend CI / (Python 3.10) (push) Waiting to run
Backend CI / (Python 3.11) (push) Waiting to run
Backend CI / (Python 3.12) (push) Waiting to run
Backend CI / (Python 3.13) (push) Waiting to run
Backend CI / Repo tests (CPU) (push) Waiting to run
Frontend CI / Frontend build + bundle sanity (push) Waiting to run
Studio GGUF CI / OpenAI, Anthropic API tests (push) Waiting to run
Studio GGUF CI / Tool calling Tests (push) Waiting to run
Studio GGUF CI / JSON, images (push) Waiting to run
Mac Studio API CI / Studio API & Auth Tests (push) Waiting to run
Mac Studio GGUF CI / OpenAI, Anthropic API tests (push) Waiting to run
Mac Studio GGUF CI / Tool calling Tests (push) Waiting to run
Mac Studio GGUF CI / JSON, images (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-14) (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-15) (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-26) (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-15-intel) (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-26-intel) (push) Waiting to run
Mac Studio UI CI / Chat UI Tests (push) Waiting to run
Mac Studio Update CI / Studio Updating Tests (push) Waiting to run
Studio Tauri CI / Tauri Linux debug build (no codesign) (push) Waiting to run
Studio UI CI / Chat UI Tests (push) Waiting to run
Studio Update CI / Studio Updating Tests (push) Waiting to run
Windows Studio API CI / Studio API & Auth Tests (push) Waiting to run
Windows Studio GGUF CI / OpenAI, Anthropic API tests (push) Waiting to run
Windows Studio GGUF CI / Tool calling Tests (push) Waiting to run
Windows Studio GGUF CI / JSON, images (push) Waiting to run
Windows Studio GGUF CI / Studio install + inference without Visual Studio (push) Waiting to run
Windows Studio GGUF CI / GPU prebuilt resolves without Visual Studio (push) Waiting to run
Windows Studio GGUF CI / setup.ps1 unit tests (VS 2026 / CMake guard) (push) Waiting to run
Windows Studio GGUF CI / real-VS detection (VS 2022) (push) Waiting to run
Windows Studio GGUF CI / real-VS detection (VS 2026) (push) Waiting to run
Windows Studio GGUF CI / VC++ runtime detect + install round-trip (windows-2025-vs2026) (push) Waiting to run
Windows Studio GGUF CI / VC++ runtime detect + install round-trip (windows-latest) (push) Waiting to run
Windows Studio UI CI / Chat UI Tests (push) Waiting to run
Windows Studio Update CI / Studio Updating Tests (push) Waiting to run
Wheel CI / Wheel build + content sanity + import smoke (push) Waiting to run
Some checks are pending
Core / Core (HF=default + TRL=default) (push) Waiting to run
Core / Core (HF=4.57.6 + TRL<1) (push) Waiting to run
Core / Core (HF=latest + TRL=latest) (push) Waiting to run
Core / llama.cpp build + smoke (push) Waiting to run
Lint CI / Source lint (Python + shell + YAML + JSON + safety nets) (push) Waiting to run
MLX CI on Mac M1 / dispatch (push) Waiting to run
Security audit / pip scan-packages :: extras (push) Waiting to run
Security audit / pip scan-packages :: studio (push) Waiting to run
Security audit / pip scan-packages :: hf-stack (push) Waiting to run
Security audit / advisory audit (pip + npm + cargo) (push) Waiting to run
Security audit / npm scan-packages (Studio frontend tarballs) (push) Waiting to run
Security audit / workflow-trigger lint (pull_request_target / cache-poisoning) (push) Waiting to run
Security audit / pytest tests/security (push) Waiting to run
Security audit / npm provenance + new install-script diff (push) Waiting to run
Studio API CI / Studio API & Auth Tests (push) Waiting to run
Backend CI / (Python 3.10) (push) Waiting to run
Backend CI / (Python 3.11) (push) Waiting to run
Backend CI / (Python 3.12) (push) Waiting to run
Backend CI / (Python 3.13) (push) Waiting to run
Backend CI / Repo tests (CPU) (push) Waiting to run
Frontend CI / Frontend build + bundle sanity (push) Waiting to run
Studio GGUF CI / OpenAI, Anthropic API tests (push) Waiting to run
Studio GGUF CI / Tool calling Tests (push) Waiting to run
Studio GGUF CI / JSON, images (push) Waiting to run
Mac Studio API CI / Studio API & Auth Tests (push) Waiting to run
Mac Studio GGUF CI / OpenAI, Anthropic API tests (push) Waiting to run
Mac Studio GGUF CI / Tool calling Tests (push) Waiting to run
Mac Studio GGUF CI / JSON, images (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-14) (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-15) (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-26) (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-15-intel) (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-26-intel) (push) Waiting to run
Mac Studio UI CI / Chat UI Tests (push) Waiting to run
Mac Studio Update CI / Studio Updating Tests (push) Waiting to run
Studio Tauri CI / Tauri Linux debug build (no codesign) (push) Waiting to run
Studio UI CI / Chat UI Tests (push) Waiting to run
Studio Update CI / Studio Updating Tests (push) Waiting to run
Windows Studio API CI / Studio API & Auth Tests (push) Waiting to run
Windows Studio GGUF CI / OpenAI, Anthropic API tests (push) Waiting to run
Windows Studio GGUF CI / Tool calling Tests (push) Waiting to run
Windows Studio GGUF CI / JSON, images (push) Waiting to run
Windows Studio GGUF CI / Studio install + inference without Visual Studio (push) Waiting to run
Windows Studio GGUF CI / GPU prebuilt resolves without Visual Studio (push) Waiting to run
Windows Studio GGUF CI / setup.ps1 unit tests (VS 2026 / CMake guard) (push) Waiting to run
Windows Studio GGUF CI / real-VS detection (VS 2022) (push) Waiting to run
Windows Studio GGUF CI / real-VS detection (VS 2026) (push) Waiting to run
Windows Studio GGUF CI / VC++ runtime detect + install round-trip (windows-2025-vs2026) (push) Waiting to run
Windows Studio GGUF CI / VC++ runtime detect + install round-trip (windows-latest) (push) Waiting to run
Windows Studio UI CI / Chat UI Tests (push) Waiting to run
Windows Studio Update CI / Studio Updating Tests (push) Waiting to run
Wheel CI / Wheel build + content sanity + import smoke (push) Waiting to run
* Studio: stop handing CI/user secrets to downloaded llama.cpp binaries The macOS prebuilt path installs llama.cpp from the unslothai/llama.cpp fork's latest (unpinned, mutable) release and then executes the downloaded llama-server / llama-quantize binaries during install-time validation. binary_env() built that child environment from a full os.environ.copy(), so a compromised or tampered prebuilt would inherit every secret in the process: HF_TOKEN and the workflow GitHub tokens in CI, and HF / cloud credentials for end users running install.sh / setup.sh. We publish prebuilts daily, so pinning a release tag is not workable. Instead, neutralise the impact: these binaries have no reason to read any token, so strip secret-bearing variables (exact names plus TOKEN/SECRET/PASSWORD/CREDENTIAL/PRIVATE_KEY/API_KEY markers) before handing the env to a downloaded binary. The installer's own GitHub and Hugging Face API calls read os.environ directly, so authentication and release-API rate limiting are unaffected; PATH, LD_LIBRARY_PATH, DYLD_LIBRARY_PATH and CUDA/ROCm vars are preserved. One change covers the install-time validation path for all six macOS workflows and end users. Follow-up (separate, sequenced): publish build-provenance attestations from the fork's prebuilt workflows and verify them in CI, so a forged release is rejected rather than merely starved of secrets. * Strip KUBECONFIG, SSH_AUTH_SOCK, and PASSPHRASE-marked vars from binary env Extend the deny-list per PR review: KUBECONFIG and SSH_AUTH_SOCK are credential pointers/capabilities a downloaded binary never needs, and a PASSPHRASE marker catches SSH_PASSPHRASE / GPG_PASSPHRASE. Tests updated. * Studio: also scrub proxy/index env vars and URL-embedded credentials before running prebuilt binaries * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Scope mlx-ci secrets to the install + download commands for PR #6696 Drop the ambient step-level env block and pass GH/GITHUB/HF tokens only on the installer and GGUF-download commands, so the directly invoked llama-quantize / llama-server smoke runs see no secrets. The installer still reads tokens from os.environ for the releases API and probe fetch. * Trim verbose comments around the secret-env scrubber for PR #6696 Comment-only: condense the block comments added across this PR. Logic unchanged (comment_tools.py check confirms code-only signature equal). * Redirect HOME / cache pointers to an empty dir for prebuilt binaries (PR #6696) Address Codex P2: stripping token env vars still let a tampered binary read on-disk token stores (~/.cache/huggingface/token, ~/.aws/credentials, ~/.config/gh) through $HOME and the cache/config pointers. Point HOME plus the HF / XDG / Windows home pointers at a single empty throwaway dir for the downloaded-binary env. Defense in depth: a binary resolving the real home via getpwuid is out of scope and needs OS sandboxing. * Close residual credential-probe gaps for PR #6696 Address the latest Codex review: - Strip token-only URL userinfo too (scheme://ghp_token@host), not just the user:pass form. - Redirect HOMEDRIVE/HOMEPATH alongside USERPROFILE so a Windows binary cannot reconstruct the real profile from %HOMEDRIVE%%HOMEPATH%. - Drop explicit credential-file pointers (NETRC, PIP_CONFIG_FILE, DOCKER_CONFIG, GIT_CONFIG_GLOBAL) that live outside HOME. - Probe ldd with a secret-free env: linux_runtime_dirs ran ldd on the untrusted prebuilt with the inherited os.environ, and ldd may execute the binary, so it could observe HF_TOKEN/GITHUB_TOKEN during the probe. Factored the shared scrub into secret_free_environ(). * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Separate token-bearing install from binary smoke; drop CI command files (PR #6696) Address the two P1s in the latest review: - mlx-ci: GitHub bakes secrets into the run-script text, so inline token assignments in a step that later runs the prebuilt let a tampered binary read them from the script. Split into a token-bearing install + download step that never launches a binary, and a secret-free smoke step that runs llama-quantize / llama-server. - secret_free_environ now drops the GitHub Actions command files (GITHUB_ENV, GITHUB_PATH, GITHUB_OUTPUT, GITHUB_STEP_SUMMARY, BASH_ENV) and the smoke step unsets them, so a tampered prebuilt cannot inject PATH/env into the later token-bearing MLX steps. * Run the prebuilt smoke last, after all token-bearing steps (PR #6696) Address the P1 workspace-poisoning vector: even with no secrets in its env, a tampered prebuilt could edit the checkout or installed modules, and the later HF_TOKEN MLX steps would then execute that poisoned code on push builds. Move the prebuilt install + smoke to the end of the job so the untrusted binary runs after every token-bearing step, leaving nothing for it to corrupt. The MLX GGUF reload uses a source-built llama-cli, not this prebuilt, so nothing depends on the earlier position. * Trim comments around the secret-env scrubber and prebuilt CI steps (PR #6696) Comment-only: condense the security-rationale block comments and merge the duplicated prebuilt-step description in mlx-ci. Logic unchanged (comment_tools.py check confirms the code-only signature is equal; install suite still passes). * Authenticate the GGUF export release-API lookup with the read-only GITHUB_TOKEN (PR #6696) * Rename env scrubber off the secret-named identifier CodeQL flags as a clear-text sink (PR #6696) --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
98a01e70cd
commit
4c72e09480
3 changed files with 482 additions and 95 deletions
181
.github/workflows/mlx-ci.yml
vendored
181
.github/workflows/mlx-ci.yml
vendored
|
|
@ -231,99 +231,6 @@ jobs:
|
|||
tests/studio/test_is_mlx_dispatch_gate.py \
|
||||
tests/studio/test_mlx_training_worker_behaviors.py
|
||||
|
||||
# Studio prebuilt llama.cpp install + GGUF inference. Mirrors the
|
||||
# path Studio's setup.sh takes on macOS since #5963: plan against
|
||||
# the unslothai/llama.cpp fork's latest release, which ships the
|
||||
# bin-macos-arm64 bundle plus the llama-prebuilt-manifest.json the
|
||||
# default policy reads. After install, downloads a small published
|
||||
# GGUF (unsloth/gemma-3-270m-it-GGUF, Q4_K_M) and validates
|
||||
# llama-server /completion end to end. An install failure or a
|
||||
# non-zero binary exit is an Unsloth/Studio bug.
|
||||
- name: Studio prebuilt llama.cpp install + GGUF inference (Mac M1)
|
||||
env:
|
||||
# Withheld on PR: this step runs checked-out PR code; public GGUF still downloads.
|
||||
HF_TOKEN: ${{ github.event_name != 'pull_request' && secrets.HF_TOKEN || '' }}
|
||||
# install_llama_prebuilt.py hits the GitHub releases API to
|
||||
# resolve the asset URL. Anonymous calls share the runner-IP
|
||||
# rate-limit bucket and 403 quickly -- pass the workflow's
|
||||
# automatic GITHUB_TOKEN to bump us to the 5000/hr authenticated
|
||||
# bucket.
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
INSTALL_DIR="$HOME/.unsloth-studio-prebuilt-test/llama.cpp"
|
||||
rm -rf "$INSTALL_DIR"
|
||||
# Mirror studio/setup.sh on macOS (the install.sh user path):
|
||||
# it plans against the unslothai/llama.cpp fork's latest
|
||||
# release with no policy or tag flags.
|
||||
python studio/install_llama_prebuilt.py \
|
||||
--install-dir "$INSTALL_DIR" \
|
||||
--published-repo unslothai/llama.cpp
|
||||
|
||||
# Studio bundles only llama-server + llama-quantize from the
|
||||
# prebuilt (not llama-cli) -- inference goes through
|
||||
# llama-server's HTTP /completion endpoint. Validate both:
|
||||
# llama-quantize --help proves the dynamic libs link, then
|
||||
# spin up llama-server and POST a /completion request on a
|
||||
# tiny published GGUF.
|
||||
LLAMA_SERVER="$INSTALL_DIR/build/bin/llama-server"
|
||||
LLAMA_QUANT="$INSTALL_DIR/build/bin/llama-quantize"
|
||||
[ -x "$LLAMA_SERVER" ] || { echo "::error::llama-server missing at $LLAMA_SERVER"; find "$INSTALL_DIR/build" -type f | head -40; exit 1; }
|
||||
[ -x "$LLAMA_QUANT" ] || { echo "::error::llama-quantize missing at $LLAMA_QUANT"; exit 1; }
|
||||
echo "llama-server : $LLAMA_SERVER"
|
||||
echo "llama-quantize: $LLAMA_QUANT"
|
||||
"$LLAMA_QUANT" --help >/dev/null && echo " llama-quantize loads OK"
|
||||
|
||||
mkdir -p /tmp/ggufs
|
||||
bash .github/scripts/hf-download-with-retry.sh \
|
||||
'unsloth/gemma-3-270m-it-GGUF' \
|
||||
'gemma-3-270m-it-Q4_K_M.gguf' \
|
||||
/tmp/ggufs
|
||||
|
||||
PORT=18080
|
||||
echo "=== starting llama-server on 127.0.0.1:$PORT ==="
|
||||
"$LLAMA_SERVER" \
|
||||
-m /tmp/ggufs/gemma-3-270m-it-Q4_K_M.gguf \
|
||||
--host 127.0.0.1 \
|
||||
--port "$PORT" \
|
||||
-c 256 \
|
||||
-n 16 \
|
||||
--no-warmup \
|
||||
> /tmp/llama-server.log 2>&1 &
|
||||
SERVER_PID=$!
|
||||
trap 'kill "$SERVER_PID" 2>/dev/null || true' EXIT
|
||||
|
||||
# Wait for /health to come up
|
||||
for i in $(seq 1 30); do
|
||||
if curl -sf "http://127.0.0.1:$PORT/health" >/dev/null 2>&1; then
|
||||
echo " server up after ${i}s"
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
if ! curl -sf "http://127.0.0.1:$PORT/health" >/dev/null 2>&1; then
|
||||
echo "::error::llama-server never became healthy"
|
||||
tail -40 /tmp/llama-server.log
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PROMPT="Hello, my name is"
|
||||
echo "=== POST /completion ==="
|
||||
RESP=$(curl -sf -X POST "http://127.0.0.1:$PORT/completion" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "{\"prompt\":\"$PROMPT\",\"n_predict\":16,\"temperature\":0,\"seed\":3407}")
|
||||
echo "raw response (head): $(echo "$RESP" | head -c 600)"
|
||||
CONTENT=$(echo "$RESP" | python -c "import json,sys; print(json.loads(sys.stdin.read()).get('content',''))")
|
||||
echo "completion content: $CONTENT"
|
||||
|
||||
if [ -z "$CONTENT" ]; then
|
||||
echo "::error::llama-server /completion returned empty content"
|
||||
tail -40 /tmp/llama-server.log
|
||||
exit 1
|
||||
fi
|
||||
echo "OK: Studio prebuilt llama.cpp on Mac M1 + GGUF /completion works"
|
||||
|
||||
# Real MLX training + inference smoke test. Trains
|
||||
# unsloth/gemma-3-270m-it for 7 deterministic LoRA steps
|
||||
# (batch_size=2, gradient_accumulation_steps=3) on a single
|
||||
|
|
@ -338,6 +245,9 @@ jobs:
|
|||
UNSLOTH_COMPILE_DISABLE: '1'
|
||||
run: |
|
||||
mkdir -p mlx_workdir
|
||||
# Authenticate llama.cpp's release-API lookup (anonymous 403s on rate-limit);
|
||||
# read-only GITHUB_TOKEN scoped here only, never to steps that run binaries.
|
||||
GH_TOKEN="${{ secrets.GITHUB_TOKEN }}" GITHUB_TOKEN="${{ secrets.GITHUB_TOKEN }}" \
|
||||
python tests/studio/run_real_mlx_smoke.py train \
|
||||
--workdir "$PWD/mlx_workdir"
|
||||
|
||||
|
|
@ -406,3 +316,88 @@ jobs:
|
|||
cat "$f" 2>/dev/null || echo "(missing)"
|
||||
echo
|
||||
done
|
||||
|
||||
# Validates the macOS prebuilt path Studio's setup.sh uses (#5963): install the
|
||||
# unslothai/llama.cpp fork's latest release, download a small public GGUF, and
|
||||
# check llama-server /completion end to end. Split and placed last so the
|
||||
# untrusted binary runs only in the final smoke step, after every HF_TOKEN step,
|
||||
# leaving no token-bearing step or shared workspace for a tampered prebuilt to
|
||||
# corrupt. GH_TOKEN: releases API; HF_TOKEN (withheld on PR): probe + GGUF fetch.
|
||||
- name: Studio prebuilt llama.cpp install + GGUF download (Mac M1)
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
HF_TOKEN: ${{ github.event_name != 'pull_request' && secrets.HF_TOKEN || '' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
INSTALL_DIR="$HOME/.unsloth-studio-prebuilt-test/llama.cpp"
|
||||
rm -rf "$INSTALL_DIR"
|
||||
# Download only -- no llama-quantize / llama-server launch in this step.
|
||||
python studio/install_llama_prebuilt.py \
|
||||
--install-dir "$INSTALL_DIR" \
|
||||
--published-repo unslothai/llama.cpp
|
||||
mkdir -p /tmp/ggufs
|
||||
bash .github/scripts/hf-download-with-retry.sh \
|
||||
'unsloth/gemma-3-270m-it-GGUF' \
|
||||
'gemma-3-270m-it-Q4_K_M.gguf' \
|
||||
/tmp/ggufs
|
||||
|
||||
# Final step: runs the downloaded binaries with no secrets present, and clears
|
||||
# the GitHub Actions command files so a tampered prebuilt cannot influence the job.
|
||||
- name: Studio prebuilt llama.cpp GGUF inference smoke (Mac M1)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
unset GITHUB_ENV GITHUB_PATH GITHUB_OUTPUT GITHUB_STEP_SUMMARY
|
||||
INSTALL_DIR="$HOME/.unsloth-studio-prebuilt-test/llama.cpp"
|
||||
# Studio bundles only llama-server + llama-quantize (not llama-cli);
|
||||
# inference goes through llama-server's HTTP /completion endpoint.
|
||||
LLAMA_SERVER="$INSTALL_DIR/build/bin/llama-server"
|
||||
LLAMA_QUANT="$INSTALL_DIR/build/bin/llama-quantize"
|
||||
[ -x "$LLAMA_SERVER" ] || { echo "::error::llama-server missing at $LLAMA_SERVER"; find "$INSTALL_DIR/build" -type f | head -40; exit 1; }
|
||||
[ -x "$LLAMA_QUANT" ] || { echo "::error::llama-quantize missing at $LLAMA_QUANT"; exit 1; }
|
||||
echo "llama-server : $LLAMA_SERVER"
|
||||
echo "llama-quantize: $LLAMA_QUANT"
|
||||
"$LLAMA_QUANT" --help >/dev/null && echo " llama-quantize loads OK"
|
||||
|
||||
PORT=18080
|
||||
echo "=== starting llama-server on 127.0.0.1:$PORT ==="
|
||||
"$LLAMA_SERVER" \
|
||||
-m /tmp/ggufs/gemma-3-270m-it-Q4_K_M.gguf \
|
||||
--host 127.0.0.1 \
|
||||
--port "$PORT" \
|
||||
-c 256 \
|
||||
-n 16 \
|
||||
--no-warmup \
|
||||
> /tmp/llama-server.log 2>&1 &
|
||||
SERVER_PID=$!
|
||||
trap 'kill "$SERVER_PID" 2>/dev/null || true' EXIT
|
||||
|
||||
# Wait for /health to come up
|
||||
for i in $(seq 1 30); do
|
||||
if curl -sf "http://127.0.0.1:$PORT/health" >/dev/null 2>&1; then
|
||||
echo " server up after ${i}s"
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
if ! curl -sf "http://127.0.0.1:$PORT/health" >/dev/null 2>&1; then
|
||||
echo "::error::llama-server never became healthy"
|
||||
tail -40 /tmp/llama-server.log
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PROMPT="Hello, my name is"
|
||||
echo "=== POST /completion ==="
|
||||
RESP=$(curl -sf -X POST "http://127.0.0.1:$PORT/completion" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "{\"prompt\":\"$PROMPT\",\"n_predict\":16,\"temperature\":0,\"seed\":3407}")
|
||||
echo "raw response (head): $(echo "$RESP" | head -c 600)"
|
||||
CONTENT=$(echo "$RESP" | python -c "import json,sys; print(json.loads(sys.stdin.read()).get('content',''))")
|
||||
echo "completion content: $CONTENT"
|
||||
|
||||
if [ -z "$CONTENT" ]; then
|
||||
echo "::error::llama-server /completion returned empty content"
|
||||
tail -40 /tmp/llama-server.log
|
||||
exit 1
|
||||
fi
|
||||
echo "OK: Studio prebuilt llama.cpp on Mac M1 + GGUF /completion works"
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import atexit
|
||||
import errno
|
||||
import fnmatch
|
||||
import hashlib
|
||||
|
|
@ -5203,7 +5204,8 @@ def ldconfig_runtime_dirs(required_libraries: Iterable[str]) -> list[str]:
|
|||
|
||||
|
||||
def linux_runtime_dirs(binary_path: Path) -> list[str]:
|
||||
missing = linux_missing_libraries(binary_path)
|
||||
# ldd may execute the binary, so probe it with a secret-free env.
|
||||
missing = linux_missing_libraries(binary_path, env = scrubbed_environ())
|
||||
if not missing:
|
||||
return []
|
||||
return linux_runtime_dirs_for_required_libraries(missing)
|
||||
|
|
@ -5499,6 +5501,140 @@ def _wsl_system_rocm_lib_dirs() -> list[str]:
|
|||
return out
|
||||
|
||||
|
||||
# Secrets a downloaded llama.cpp binary never needs; keep them out of binary_env().
|
||||
# The installer's own API calls read os.environ directly, so auth is unaffected.
|
||||
_SECRET_ENV_EXACT_NAMES = frozenset(
|
||||
{
|
||||
"HF_TOKEN",
|
||||
"HUGGING_FACE_HUB_TOKEN",
|
||||
"GH_TOKEN",
|
||||
"GITHUB_TOKEN",
|
||||
"WANDB_API_KEY",
|
||||
"OPENAI_API_KEY",
|
||||
"ANTHROPIC_API_KEY",
|
||||
"AWS_ACCESS_KEY_ID",
|
||||
"AWS_SECRET_ACCESS_KEY",
|
||||
"AWS_SESSION_TOKEN",
|
||||
"GOOGLE_APPLICATION_CREDENTIALS",
|
||||
"AZURE_CLIENT_SECRET",
|
||||
"ACTIONS_ID_TOKEN_REQUEST_TOKEN",
|
||||
"ACTIONS_ID_TOKEN_REQUEST_URL",
|
||||
"ACTIONS_RUNTIME_TOKEN",
|
||||
# Credential pointers (cluster / remote-host access).
|
||||
"KUBECONFIG",
|
||||
"SSH_AUTH_SOCK",
|
||||
}
|
||||
)
|
||||
# Case-insensitive substring markers for names we do not enumerate (no bare "KEY",
|
||||
# which would hit benign runtime vars).
|
||||
_SECRET_ENV_MARKERS = (
|
||||
"TOKEN",
|
||||
"SECRET",
|
||||
"PASSWORD",
|
||||
"PASSWD",
|
||||
"PASSPHRASE",
|
||||
"CREDENTIAL",
|
||||
"PRIVATE_KEY",
|
||||
"API_KEY",
|
||||
)
|
||||
# Proxy / index URLs embed creds in their value; the offline binaries never need them.
|
||||
_SECRET_ENV_URL_NAMES = frozenset(
|
||||
{
|
||||
"HTTP_PROXY",
|
||||
"HTTPS_PROXY",
|
||||
"ALL_PROXY",
|
||||
"FTP_PROXY",
|
||||
"RSYNC_PROXY",
|
||||
"PIP_INDEX_URL",
|
||||
"PIP_EXTRA_INDEX_URL",
|
||||
"UV_INDEX_URL",
|
||||
"UV_DEFAULT_INDEX",
|
||||
"UV_EXTRA_INDEX_URL",
|
||||
}
|
||||
)
|
||||
# Also drop values with URL userinfo creds (scheme://user:secret@host or token@host).
|
||||
_URL_USERINFO_CREDENTIAL_RE = re.compile(r"://[^/@\s]+@")
|
||||
|
||||
|
||||
def is_secret_env_name(name: str) -> bool:
|
||||
upper = name.upper()
|
||||
return (
|
||||
upper in _SECRET_ENV_EXACT_NAMES
|
||||
or upper in _SECRET_ENV_URL_NAMES
|
||||
or any(marker in upper for marker in _SECRET_ENV_MARKERS)
|
||||
)
|
||||
|
||||
|
||||
def scrub_env(env: dict[str, str]) -> dict[str, str]:
|
||||
"""Drop secret-bearing variables before handing an env to a downloaded binary."""
|
||||
return {
|
||||
key: value
|
||||
for key, value in env.items()
|
||||
if not is_secret_env_name(key) and not _URL_USERINFO_CREDENTIAL_RE.search(value or "")
|
||||
}
|
||||
|
||||
|
||||
# Home / cache pointers to on-disk token stores (~/.cache/huggingface/token,
|
||||
# ~/.aws/credentials, ...). Stripping env tokens is not enough; point these at an
|
||||
# empty home so the binary cannot read those files via $HOME.
|
||||
_RUNTIME_HOME_POINTER_VARS = (
|
||||
"HOME",
|
||||
"USERPROFILE",
|
||||
"APPDATA",
|
||||
"LOCALAPPDATA",
|
||||
"XDG_CACHE_HOME",
|
||||
"XDG_CONFIG_HOME",
|
||||
"XDG_DATA_HOME",
|
||||
"HF_HOME",
|
||||
"HUGGINGFACE_HUB_CACHE",
|
||||
"HF_HUB_CACHE",
|
||||
)
|
||||
# Credential / config file pointers outside HOME; drop so lookups fall back to the
|
||||
# empty home.
|
||||
_CREDENTIAL_FILE_POINTER_VARS = (
|
||||
"NETRC",
|
||||
"PIP_CONFIG_FILE",
|
||||
"DOCKER_CONFIG",
|
||||
"GIT_CONFIG_GLOBAL",
|
||||
)
|
||||
# GitHub Actions command files: appending to these injects PATH/env into later steps.
|
||||
_CI_COMMAND_FILE_VARS = (
|
||||
"GITHUB_ENV",
|
||||
"GITHUB_PATH",
|
||||
"GITHUB_OUTPUT",
|
||||
"GITHUB_STEP_SUMMARY",
|
||||
"BASH_ENV",
|
||||
)
|
||||
|
||||
_isolated_runtime_home_dir: str | None = None
|
||||
|
||||
|
||||
def isolated_runtime_home() -> str:
|
||||
# Empty dir, created lazily and removed at exit. (A binary resolving the real
|
||||
# home via getpwuid is out of scope; that needs OS sandboxing.)
|
||||
global _isolated_runtime_home_dir
|
||||
if _isolated_runtime_home_dir is None:
|
||||
path = tempfile.mkdtemp(prefix = "unsloth-prebuilt-home-")
|
||||
atexit.register(shutil.rmtree, path, ignore_errors = True)
|
||||
_isolated_runtime_home_dir = path
|
||||
return _isolated_runtime_home_dir
|
||||
|
||||
|
||||
def scrubbed_environ() -> dict[str, str]:
|
||||
# os.environ minus secrets, with home / credential pointers neutralised. Used for
|
||||
# the binary env and any probe (e.g. ldd) that runs the untrusted binary.
|
||||
env = scrub_env(os.environ.copy())
|
||||
runtime_home = isolated_runtime_home()
|
||||
for pointer in _RUNTIME_HOME_POINTER_VARS:
|
||||
env[pointer] = runtime_home
|
||||
# Windows rebuilds the profile from %HOMEDRIVE%%HOMEPATH% (no-op pair on POSIX).
|
||||
drive, tail = os.path.splitdrive(runtime_home)
|
||||
env["HOMEDRIVE"], env["HOMEPATH"] = drive, tail or runtime_home
|
||||
for pointer in (*_CREDENTIAL_FILE_POINTER_VARS, *_CI_COMMAND_FILE_VARS):
|
||||
env.pop(pointer, None)
|
||||
return env
|
||||
|
||||
|
||||
def binary_env(
|
||||
binary_path: Path,
|
||||
install_dir: Path,
|
||||
|
|
@ -5506,7 +5642,7 @@ def binary_env(
|
|||
*,
|
||||
runtime_line: str | None = None,
|
||||
) -> dict[str, str]:
|
||||
env = os.environ.copy()
|
||||
env = scrubbed_environ()
|
||||
if host.is_windows:
|
||||
path_dirs = [
|
||||
str(binary_path.parent),
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@ SPEC.loader.exec_module(INSTALL_LLAMA_PREBUILT)
|
|||
PrebuiltFallback = INSTALL_LLAMA_PREBUILT.PrebuiltFallback
|
||||
extract_archive = INSTALL_LLAMA_PREBUILT.extract_archive
|
||||
binary_env = INSTALL_LLAMA_PREBUILT.binary_env
|
||||
is_secret_env_name = INSTALL_LLAMA_PREBUILT.is_secret_env_name
|
||||
scrub_env = INSTALL_LLAMA_PREBUILT.scrub_env
|
||||
isolated_runtime_home = INSTALL_LLAMA_PREBUILT.isolated_runtime_home
|
||||
HostInfo = INSTALL_LLAMA_PREBUILT.HostInfo
|
||||
AssetChoice = INSTALL_LLAMA_PREBUILT.AssetChoice
|
||||
ApprovedArtifactHash = INSTALL_LLAMA_PREBUILT.ApprovedArtifactHash
|
||||
|
|
@ -779,6 +782,259 @@ def test_binary_env_linux_includes_binary_parent_in_ld_library_path(
|
|||
assert str(install_dir) in ld_dirs
|
||||
|
||||
|
||||
def test_scrub_env_drops_secrets_and_keeps_runtime_vars():
|
||||
raw = {
|
||||
# secrets
|
||||
"HF_TOKEN": "hf_x",
|
||||
"HUGGING_FACE_HUB_TOKEN": "hf_y",
|
||||
"GH_TOKEN": "gh_x",
|
||||
"GITHUB_TOKEN": "gh_y",
|
||||
"WANDB_API_KEY": "wandb_x",
|
||||
"AWS_SECRET_ACCESS_KEY": "aws_x",
|
||||
"ACTIONS_ID_TOKEN_REQUEST_TOKEN": "oidc_x",
|
||||
"ACTIONS_ID_TOKEN_REQUEST_URL": "https://oidc",
|
||||
"SOME_VENDOR_API_KEY": "vendor_x",
|
||||
"DB_PASSWORD": "pw",
|
||||
"MY_PRIVATE_KEY": "pk",
|
||||
"KUBECONFIG": "/home/runner/.kube/config",
|
||||
"SSH_AUTH_SOCK": "/tmp/ssh-agent.sock",
|
||||
"SSH_PASSPHRASE": "ssh_pass",
|
||||
# runtime vars to keep
|
||||
"PATH": "/usr/bin",
|
||||
"LD_LIBRARY_PATH": "/opt/lib",
|
||||
"DYLD_LIBRARY_PATH": "/opt/dyld",
|
||||
"HOME": "/home/runner",
|
||||
"TMPDIR": "/tmp",
|
||||
"CUDA_VISIBLE_DEVICES": "0",
|
||||
"HSA_OVERRIDE_GFX_VERSION": "11.0.0",
|
||||
}
|
||||
|
||||
cleaned = scrub_env(raw)
|
||||
|
||||
for secret in (
|
||||
"HF_TOKEN",
|
||||
"HUGGING_FACE_HUB_TOKEN",
|
||||
"GH_TOKEN",
|
||||
"GITHUB_TOKEN",
|
||||
"WANDB_API_KEY",
|
||||
"AWS_SECRET_ACCESS_KEY",
|
||||
"ACTIONS_ID_TOKEN_REQUEST_TOKEN",
|
||||
"ACTIONS_ID_TOKEN_REQUEST_URL",
|
||||
"SOME_VENDOR_API_KEY",
|
||||
"DB_PASSWORD",
|
||||
"MY_PRIVATE_KEY",
|
||||
"KUBECONFIG",
|
||||
"SSH_AUTH_SOCK",
|
||||
"SSH_PASSPHRASE",
|
||||
):
|
||||
assert secret not in cleaned, f"{secret} must be stripped from binary env"
|
||||
|
||||
for keep in (
|
||||
"PATH",
|
||||
"LD_LIBRARY_PATH",
|
||||
"DYLD_LIBRARY_PATH",
|
||||
"HOME",
|
||||
"TMPDIR",
|
||||
"CUDA_VISIBLE_DEVICES",
|
||||
"HSA_OVERRIDE_GFX_VERSION",
|
||||
):
|
||||
assert cleaned[keep] == raw[keep], f"{keep} must be preserved for the binary"
|
||||
|
||||
# no bare "KEY" marker: benign KEY-containing names survive
|
||||
assert is_secret_env_name("API_KEY") is True
|
||||
assert is_secret_env_name("SSH_KEYFILE_PATH") is False
|
||||
assert is_secret_env_name("PATH") is False
|
||||
|
||||
|
||||
def test_scrub_env_drops_proxy_index_and_embedded_url_credentials():
|
||||
raw = {
|
||||
# proxy / package-index URLs whose values commonly embed credentials
|
||||
"HTTPS_PROXY": "https://user:secret@proxy:8080",
|
||||
"https_proxy": "https://user:secret@proxy:8080", # lower-case variant
|
||||
"ALL_PROXY": "socks5://user:secret@proxy:1080",
|
||||
"PIP_INDEX_URL": "https://u:p@pypi.internal/simple",
|
||||
"UV_INDEX_URL": "https://u:p@index.internal/simple",
|
||||
# credentials embedded in an otherwise benign-named variable's value
|
||||
"MY_DB_DSN": "postgres://admin:secret@db:5432/app",
|
||||
# benign vars the binary needs, including a URL with no userinfo
|
||||
"PATH": "/usr/bin",
|
||||
"CUDA_VISIBLE_DEVICES": "0",
|
||||
"NO_PROXY": "localhost,127.0.0.1",
|
||||
"SOME_ENDPOINT": "https://example.com:8080/v1",
|
||||
}
|
||||
|
||||
cleaned = scrub_env(raw)
|
||||
|
||||
for secret in (
|
||||
"HTTPS_PROXY",
|
||||
"https_proxy",
|
||||
"ALL_PROXY",
|
||||
"PIP_INDEX_URL",
|
||||
"UV_INDEX_URL",
|
||||
"MY_DB_DSN",
|
||||
):
|
||||
assert secret not in cleaned, f"{secret} must be stripped from binary env"
|
||||
for keep in ("PATH", "CUDA_VISIBLE_DEVICES", "NO_PROXY", "SOME_ENDPOINT"):
|
||||
assert cleaned[keep] == raw[keep], f"{keep} must be preserved for the binary"
|
||||
|
||||
assert is_secret_env_name("HTTPS_PROXY") is True
|
||||
assert is_secret_env_name("https_proxy") is True
|
||||
assert is_secret_env_name("NO_PROXY") is False
|
||||
|
||||
|
||||
def test_binary_env_strips_secrets_from_downloaded_binary_environment(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
):
|
||||
install_dir = tmp_path / "llama.cpp"
|
||||
bin_dir = install_dir / "build" / "bin"
|
||||
bin_dir.mkdir(parents = True)
|
||||
binary_path = bin_dir / "llama-server"
|
||||
binary_path.write_bytes(b"fake")
|
||||
|
||||
host = HostInfo(
|
||||
system = "Linux",
|
||||
machine = "x86_64",
|
||||
is_windows = False,
|
||||
is_linux = True,
|
||||
is_macos = False,
|
||||
is_x86_64 = True,
|
||||
is_arm64 = False,
|
||||
nvidia_smi = None,
|
||||
driver_cuda_version = None,
|
||||
compute_caps = [],
|
||||
visible_cuda_devices = None,
|
||||
has_physical_nvidia = False,
|
||||
has_usable_nvidia = False,
|
||||
)
|
||||
monkeypatch.setattr(INSTALL_LLAMA_PREBUILT, "linux_runtime_dirs", lambda _bp: [])
|
||||
|
||||
monkeypatch.setenv("HF_TOKEN", "hf_secret_from_ci")
|
||||
monkeypatch.setenv("GITHUB_TOKEN", "gh_secret_from_ci")
|
||||
monkeypatch.setenv("GH_TOKEN", "gh_secret_from_ci")
|
||||
monkeypatch.setenv("WANDB_API_KEY", "wandb_secret_from_ci")
|
||||
monkeypatch.setenv("CUDA_VISIBLE_DEVICES", "1")
|
||||
|
||||
env = binary_env(binary_path, install_dir, host)
|
||||
|
||||
assert "HF_TOKEN" not in env
|
||||
assert "GITHUB_TOKEN" not in env
|
||||
assert "GH_TOKEN" not in env
|
||||
assert "WANDB_API_KEY" not in env
|
||||
# library/runtime resolution unaffected
|
||||
assert str(bin_dir) in env["LD_LIBRARY_PATH"].split(os.pathsep)
|
||||
assert env["CUDA_VISIBLE_DEVICES"] == "1"
|
||||
|
||||
|
||||
def test_binary_env_redirects_home_away_from_real_credential_stores(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
):
|
||||
install_dir = tmp_path / "llama.cpp"
|
||||
bin_dir = install_dir / "build" / "bin"
|
||||
bin_dir.mkdir(parents = True)
|
||||
binary_path = bin_dir / "llama-server"
|
||||
binary_path.write_bytes(b"fake")
|
||||
|
||||
host = HostInfo(
|
||||
system = "Linux",
|
||||
machine = "x86_64",
|
||||
is_windows = False,
|
||||
is_linux = True,
|
||||
is_macos = False,
|
||||
is_x86_64 = True,
|
||||
is_arm64 = False,
|
||||
nvidia_smi = None,
|
||||
driver_cuda_version = None,
|
||||
compute_caps = [],
|
||||
visible_cuda_devices = None,
|
||||
has_physical_nvidia = False,
|
||||
has_usable_nvidia = False,
|
||||
)
|
||||
monkeypatch.setattr(INSTALL_LLAMA_PREBUILT, "linux_runtime_dirs", lambda _bp: [])
|
||||
|
||||
real_home = str(tmp_path / "real_home")
|
||||
monkeypatch.setenv("HOME", real_home)
|
||||
monkeypatch.setenv("HF_HOME", real_home + "/.cache/huggingface")
|
||||
|
||||
env = binary_env(binary_path, install_dir, host)
|
||||
|
||||
# HOME and the cache pointers are redirected to a single empty, existing dir.
|
||||
assert env["HOME"] != real_home
|
||||
assert env["HF_HOME"] == env["HOME"]
|
||||
assert env["HOME"] == isolated_runtime_home()
|
||||
assert os.path.isdir(env["HOME"])
|
||||
assert os.listdir(env["HOME"]) == []
|
||||
# Windows reconstructs the profile from HOMEDRIVE + HOMEPATH.
|
||||
assert env["HOMEDRIVE"] + env["HOMEPATH"] == env["HOME"]
|
||||
|
||||
|
||||
def test_scrub_env_drops_token_only_url_userinfo():
|
||||
raw = {
|
||||
"GENERIC_REPO": "https://ghp_tokenonly@github.com/org/repo",
|
||||
"GENERIC_OK": "https://example.com:8080/v1",
|
||||
}
|
||||
cleaned = scrub_env(raw)
|
||||
assert "GENERIC_REPO" not in cleaned
|
||||
assert cleaned["GENERIC_OK"] == raw["GENERIC_OK"]
|
||||
|
||||
|
||||
def test_binary_env_drops_explicit_credential_file_pointers(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
):
|
||||
host = HostInfo(
|
||||
system = "Linux",
|
||||
machine = "x86_64",
|
||||
is_windows = False,
|
||||
is_linux = True,
|
||||
is_macos = False,
|
||||
is_x86_64 = True,
|
||||
is_arm64 = False,
|
||||
nvidia_smi = None,
|
||||
driver_cuda_version = None,
|
||||
compute_caps = [],
|
||||
visible_cuda_devices = None,
|
||||
has_physical_nvidia = False,
|
||||
has_usable_nvidia = False,
|
||||
)
|
||||
monkeypatch.setattr(INSTALL_LLAMA_PREBUILT, "linux_runtime_dirs", lambda _bp: [])
|
||||
dropped = (
|
||||
"NETRC",
|
||||
"PIP_CONFIG_FILE",
|
||||
"DOCKER_CONFIG",
|
||||
"GIT_CONFIG_GLOBAL",
|
||||
"GITHUB_ENV",
|
||||
"GITHUB_PATH",
|
||||
"GITHUB_OUTPUT",
|
||||
"GITHUB_STEP_SUMMARY",
|
||||
"BASH_ENV",
|
||||
)
|
||||
for var in dropped:
|
||||
monkeypatch.setenv(var, "/home/realuser/secret")
|
||||
|
||||
env = binary_env(tmp_path / "llama-server", tmp_path, host)
|
||||
|
||||
for var in dropped:
|
||||
assert var not in env
|
||||
|
||||
|
||||
def test_linux_runtime_dirs_probes_with_secret_free_env(monkeypatch: pytest.MonkeyPatch):
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
def fake_missing(binary_path, *, env = None):
|
||||
captured["env"] = env
|
||||
return []
|
||||
|
||||
monkeypatch.setattr(INSTALL_LLAMA_PREBUILT, "linux_missing_libraries", fake_missing)
|
||||
monkeypatch.setenv("HF_TOKEN", "hf_secret")
|
||||
monkeypatch.setenv("GITHUB_TOKEN", "gh_secret")
|
||||
|
||||
INSTALL_LLAMA_PREBUILT.linux_runtime_dirs(Path("/fake/llama-server"))
|
||||
|
||||
probe_env = captured["env"]
|
||||
assert probe_env is not None
|
||||
assert "HF_TOKEN" not in probe_env
|
||||
assert "GITHUB_TOKEN" not in probe_env
|
||||
|
||||
|
||||
def test_install_prebuilt_falls_back_to_older_release_plan(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue