`deploy.sh <engine|subnet> frontend <name> --mode reinstall` forwards the extra canister name to `icp deploy` with the reinstall, because the frontend-only door never got the plain door's `pass_canister_names` check

Đang mở
#95 0 bình luận 0 reaction 0 người được giao Xem trên GitHub

Chưa có ai nhận issue này.

Đánh giá

Độ khó
3/5
Thời gian dự kiến
1-2 ngày
Mức phù hợp với người mới
45/100
Loại issue
Lỗi
Độ rõ ràng
Đặc tả rõ ràng
Mức độ hoạt động
Sôi nổi
Công nghệ
bash
Lĩnh vực
devops

Hướng nghiên cứu

Start with the argument parser at scripts/deploy.sh:709-744, where extra bare words go into PASS and FRONTEND_ONLY is set, then the plain-door name check at :880-893 and the frontend-only block at :1026-1042. The existing pass_canister_names reader at :835-856 may be reusable there. Run tests/test_deploy_hygiene.sh §6bh (around :7118-7224) to see the current cases. Done means a remote frontend-only reinstall that names any canister other than frontend is refused, and the hygiene test pins that refusal.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Mô tả

Severity: Low. This is a robustness gap in an operator safety gate. Only an operator who holds the controller identity can trigger it, by mistyping a command, so no attacker or non-controller principal can reach it. That puts it outside SECURITY.md's attacker-reachable scope, as with #55's tooling notes. If it is hit, every bit of live state on the named canister is wiped. The script does not depend on posture, so this is the same on #play and #production.

Category: Operator tooling. A sibling-door gap in the 1.75 remote-reinstall guard in scripts/deploy.sh.

Summary

Release 1.75 added two guards to scripts/deploy.sh so that no remote door forwards --mode reinstall to a stateful canister:

  • The guided full path refuses any reinstall spelling (:810-813).
  • The plain passthrough refuses a remote reinstall unless pass_canister_names reports that frontend is the only canister named (:880-893).

The third remote door, deploy.sh <engine|subnet> frontend …, skips the first guard on purpose (if ! $FRONTEND_ONLY && …). It never reaches the second guard, because TARGET is not plain, and it has no name check of its own. The argument parser puts any extra bare word (backend, bridge, arb) into PASS, and :1039 forwards PASS verbatim into icp deploy frontend ….

As a result, bash scripts/deploy.sh subnet frontend backend --mode reinstall runs icp deploy frontend -e subnet --subnet <id> --identity <id> backend --mode reinstall. That reinstalls the production backend alongside the frontend. The script then prints "backend, bridge, seed and settings untouched." and exits 0. This is the typo class the guard was built for ("one word short of the frontend-only form", :773). It arrives through the one door that the hygiene test pins as "frontend-only excluded by construction" (tests/test_deploy_hygiene.sh:7118).

Affected surface

This is operator tooling, not a canister method. Both guards were introduced in 1.75 (a225a74, per git log -S), and the code below is unchanged in 1.80.

  • scripts/deploy.sh:709-722: the argument parser. frontend only sets SAW_FRONTEND=true (:713), and every other bare word goes into PASS (:722).
  • scripts/deploy.sh:741-744: <target> frontend sets FRONTEND_ONLY=true without looking at PASS.
  • scripts/deploy.sh:810-813: the full-path reinstall guard, keyed on ! $FRONTEND_ONLY.
  • scripts/deploy.sh:835-856: pass_canister_names, the existing name reader.
  • scripts/deploy.sh:880-893: the plain-door guard. It runs only when TARGET=plain, and it is the only caller of pass_canister_names (:885).
  • scripts/deploy.sh:1026-1042: the frontend-only legacy-wasm probe, which checks the reinstall spelling and never the names. Then icp deploy frontend … ${PASS[@]}, the "untouched" success line and exit 0.
  • tests/test_deploy_hygiene.sh:7118: pins the ! $FRONTEND_ONLY key as safe "by construction". The driven §6bh cases at :7212, :7218 and :7224 exercise <target> frontend --mode reinstall only with no extra name.
  • Canisters at risk are backend, bridge and arb (icp.yaml:2, :44, :55). The backend is persistent actor Uplands with no init args (src/backend/main.mo:140), so a reinstall without --args installs cleanly.

Mechanism

The parser treats frontend as a mode keyword and keeps every other bare word as a forwarded argument (:712-722):

    local|cloud|engine|subnet) if [ -z "$TARGET" ]; then TARGET="$1"; else PASS+=("$1"); fi ;;
    frontend)      SAW_FRONTEND=true ;;
    …
    *)             PASS+=("$1") ;;

FRONTEND_ONLY is set from SAW_FRONTEND and TARGET alone (:743). The full-path guard then steps aside for it (:810):

if ! $FRONTEND_ONLY && { [ "$TARGET" = "engine" ] || [ "$TARGET" = "subnet" ]; }; then

The plain door already has the right check (case "$PLAIN_NAMES" in frontend) : ;; *) die …), but only inside if [ "$TARGET" = "plain" ] (:880). Nothing between :893 and the frontend-only block reads PASS again: gen-did (:908) does not, and the posture gate checks DEPLOY_MODE only. Inside the block, the legacy probe tests only for a literal --mode reinstall or -m reinstall (:1033). After that, everything is forwarded (:1039-1041):

  icp deploy frontend -e "$FO_ENV" ${FO_SUBNET[@]+"${FO_SUBNET[@]}"} ${FO_MODE[@]+"${FO_MODE[@]}"} --identity "$FO_ID" ${PASS[@]+"${PASS[@]}"} \
    || die "Frontend deploy failed."
  ok "Frontend synced — backend, bridge, seed and settings untouched."

icp 1.7.0 parses this as Usage: icp deploy [OPTIONS] [NAMES]... (from icp deploy --help), so NAMES=[frontend, backend], and -m reinstall applies to the whole deploy.

Spelling caveat. On a frontend that still runs the legacy asset-canister wasm, the probe at :1032-1034 accepts only the literal two-word forms. On such a frontend, -ym reinstall, --mode=reinstall and -mreinstall happen to die there. Once the frontend runs the certified-assets wasm, the probe is inert. The documented form --mode reinstall goes through in both states.

Reproduction

Executed: the unmodified scripts/deploy.sh at 7bcdde6, driven inside a throwaway copy of the tree. The setup copies the harness that tests/test_deploy_hygiene.sh §6bh uses: a stub gen-did.sh, a #play fixture main.mo, fake .subnet.conf and .cloud-engine.conf files that exist only in the copy, and a recording icp stub on PATH. The stub answers the identity, status and legacy-probe reads, and it logs every icp deploy instead of running it. The script checks that the copied deploy.sh is byte-identical to the tree's. Nothing remote is contacted. Run it from the repo root with bash repro.sh ..

repro.sh (bash, commit 7bcdde6)
#!/bin/bash
# Drives the UNMODIFIED scripts/deploy.sh inside a throwaway copy, using the
# harness shape of tests/test_deploy_hygiene.sh §6bh (stub gen-did, #play
# fixture main.mo, fake conf files, recording icp stub). No remote touched.
set -u
REPO_SRC="$1"
RI_TMP="$(mktemp -d "${TMPDIR:-/tmp}/fo-names.XXXXXX")"
RI_REPO="$RI_TMP/repo"; RI_LOG="$RI_TMP/calls.log"
mkdir -p "$RI_REPO/scripts/lib" "$RI_REPO/src/backend" "$RI_REPO/src/bridge" "$RI_REPO/.icp/data/mappings" "$RI_TMP/bin"
cp "$REPO_SRC"/scripts/deploy.sh "$REPO_SRC"/scripts/factory_guard.sh "$RI_REPO/scripts/"
cp "$REPO_SRC"/scripts/lib/*.sh "$RI_REPO/scripts/lib/"
cp "$REPO_SRC"/.icp/data/mappings/subnet.ids.json "$REPO_SRC"/.icp/data/mappings/engine.ids.json "$RI_REPO/.icp/data/mappings/"
cmp -s "$REPO_SRC/scripts/deploy.sh" "$RI_REPO/scripts/deploy.sh" && echo "deploy.sh copy is byte-identical to the tree's"
printf '#!/bin/bash\nexit 0\n' > "$RI_REPO/scripts/gen-did.sh"
printf '#!/bin/bash\necho "RI-COLD-START-REACHED $*"\n' > "$RI_REPO/scripts/cold_start.sh"
for f in src/backend/main.mo src/bridge/main.mo; do
  printf 'actor {\n  public type DeployMode = { #dev; #play; #production };\n  transient let DEPLOY_MODE : DeployMode = #play;\n  transient let IS_PRODUCTION : Bool = DEPLOY_MODE == #production;\n  transient let IS_DEV : Bool = DEPLOY_MODE == #dev;\n}\n' > "$RI_REPO/$f"
done
printf "SN_IDENTITY=ri-stub-wallet\nSN_SUBNET=ri-stub-subnet\nSN_FRONTEND_ORIGINS=''\n" > "$RI_REPO/scripts/.subnet.conf"
printf "CE_IDENTITY=ri-stub-engine\nCE_SUBNET=ri-stub-engine-subnet\nCE_CONSOLE=https://console.invalid\nCE_FRONTEND_ORIGINS=''\n" > "$RI_REPO/scripts/.cloud-engine.conf"
cat > "$RI_TMP/bin/icp" <<'RIEOF'
#!/bin/bash
printf '%s\n' "$*" >> "$RI_LOG"
name=""; env=""; prev=""
for a in "$@"; do
  case "$a" in backend|bridge|arb|frontend) name="$a" ;; esac
  case "$prev" in -e|--environment) env="$a" ;; esac
  prev="$a"
done
case "$1 $2" in
  "identity principal") echo "ri-stub-principal-aaaaa-aa" ;;
  "canister status")
    id="$(sed -n "s/.*\"$name\"[[:space:]]*:[[:space:]]*\"\([^\"]*\)\".*/\1/p" "$RI_REPO/.icp/data/mappings/${env:-subnet}.ids.json")"
    extras="$( . "$RI_REPO/scripts/lib/targets.sh" >/dev/null 2>&1; mdx_expected_extra_controllers "${env:-subnet}" "$name" )"
    printf 'Canister Id: %s\nCanister Name: %s\nCanister Status Report:\n  Status: Running\n  Controllers: %s\n  Memory Size: 1\n' "$id" "$name" "ri-stub-principal-aaaaa-aa${extras:+ $extras}" ;;
  "canister call") [ "${RI_LEGACY:-0}" = 1 ] && exit 0; exit 1 ;;
  "deploy "*) [ "${RI_DEPLOY_RC:-9}" = 0 ] && exit 0; echo "icp stub: FORBIDDEN remote act attempted: icp $*" >&2; exit 9 ;;
  *) exit 0 ;;
esac
RIEOF
chmod +x "$RI_TMP/bin/icp"
ri_run() {
  : > "$RI_LOG"
  echo "=== RI_LEGACY=${RI_LEGACY:-0} deploy.sh $*"
  (cd "$RI_REPO" && env -u FACTORY_WORKER -u FACTORY_FORCE -u DEPLOY_TARGET PATH="$RI_TMP/bin:$PATH" RI_LOG="$RI_LOG" RI_REPO="$RI_REPO" \
     RI_DEPLOY_RC=0 RI_LEGACY="${RI_LEGACY:-0}" SEED=false RUN_BOTS=false bash scripts/deploy.sh "$@" </dev/null 2>&1) | grep -E 'REFUSING|Frontend-only deploy|Frontend synced|legacy|failed' | cut -c1-160
  echo "rc=${PIPESTATUS[0]}"
  grep '^deploy ' "$RI_LOG" | sed 's/^/icp /'
}
ri_run subnet frontend backend --mode reinstall
ri_run engine frontend bridge arb -m reinstall
RI_LEGACY=1 ri_run subnet frontend backend --mode reinstall
ri_run subnet --mode reinstall
ri_run frontend backend -e subnet --mode reinstall
ri_run subnet frontend --mode reinstall
rm -rf "$RI_TMP"

Output (ANSI colour codes stripped; the two refusal lines are cut at 160 characters by the script):

deploy.sh copy is byte-identical to the tree's
=== RI_LEGACY=0 deploy.sh subnet frontend backend --mode reinstall
▶ Frontend-only deploy → subnet (env subnet, identity ri-stub-wallet)
  ✓ Frontend synced — backend, bridge, seed and settings untouched.
rc=0
icp deploy frontend -e subnet --subnet ri-stub-subnet --identity ri-stub-wallet backend --mode reinstall
=== RI_LEGACY=0 deploy.sh engine frontend bridge arb -m reinstall
▶ Frontend-only deploy → engine (env engine, identity ri-stub-engine)
  ✓ Frontend synced — backend, bridge, seed and settings untouched.
rc=0
icp deploy frontend -e engine --subnet ri-stub-engine-subnet --identity ri-stub-engine bridge arb -m reinstall
=== RI_LEGACY=1 deploy.sh subnet frontend backend --mode reinstall
▶ the subnet frontend still runs the legacy asset-canister wasm — reinstalling it as requested (--mode reinstall)
▶ Frontend-only deploy → subnet (env subnet, identity ri-stub-wallet)
  ✓ Frontend synced — backend, bridge, seed and settings untouched.
rc=0
icp deploy frontend -e subnet --subnet ri-stub-subnet --identity ri-stub-wallet backend --mode reinstall
=== RI_LEGACY=0 deploy.sh subnet --mode reinstall
✗ REFUSING 'deploy.sh subnet' with '--mode reinstall' among the forwarded arguments: the full path forwards every extra argument to EACH of its icp dep
rc=1
=== RI_LEGACY=0 deploy.sh frontend backend -e subnet --mode reinstall
✗ REFUSING the plain passthrough 'icp deploy frontend backend -e subnet --mode reinstall': it carries '--mode reinstall' against the remote environment
rc=1
=== RI_LEGACY=0 deploy.sh subnet frontend --mode reinstall
▶ Frontend-only deploy → subnet (env subnet, identity ri-stub-wallet)
  ✓ Frontend synced — backend, bridge, seed and settings untouched.
rc=0
icp deploy frontend -e subnet --subnet ri-stub-subnet --identity ri-stub-wallet --mode reinstall

The first three runs are the defect. In each, the frontend-only door hands backend, or bridge arb, to icp deploy together with the reinstall, and then reports success. The fourth and fifth runs are controls: the same mistake on the full path and on the plain door is refused. The sixth run is the intended single-canister frontend reinstall.

Traced, not executed: the icp deploy against a real network. icp deploy --help (1.7.0) documents -y as skipping "confirmation prompts, including the Candid interface compatibility check" and names no reinstall-specific prompt. I did not establish whether icp prompts before a remote reinstall. Even if it does, -ym reinstall skips the prompt on a frontend that runs the certified-assets wasm.

Impact

  • Who is affected: every player on the targeted venue. A backend reinstall wipes all of the actor's state: balances, open orders, margin positions, the pool, insurance and AMM vault state, the hash-chained tape and the archive pointers. Naming bridge wipes the Bridge's per-user ledgers. Naming arb resets the arb bot.
  • Follow-on (traced): the frontend-only block exits at :1042, before the full path re-asserts the production frontend_origins (apply_anti_sybil_settings, :1130, called at :1219 and :1491). Per icp.yaml's own comment, a create-class install re-stamps the local-dev value at icp.yaml:37. Verify-with-Google then fails closed with #FrontendOriginMismatch until someone fixes it by hand, the failure mode deploy.sh:1080-1093 records. The AI keys, which live in backend stable state and are wiped on reinstall (:1048-1049), are not re-injected either.
  • The misleading success line ("backend, bridge, seed and settings untouched.") delays detection.
  • Bounds: the trigger is an honest operator's mistake on the controller identity. No other principal can reach it, which is why this is rated Low and not an incident.

Relationship to existing reports

  • I found no existing report of this. No issue, advisory, PR or triage document mentions FRONTEND_ONLY, pass_canister_names or the 1.75 reinstall guard. The existing reinstall mentions are unrelated: #5 and #25 discuss the ledger divergence after an out-of-band Bridge reinstall, and the pre-mainnet checklist covers re-applying config after a reinstall.
  • #10.5 (OhShii Labs): the closest prior item, and credit to them for the class. A deploy.sh door, there the -e ic and engine vocabulary falling through to the plain passthrough, skipped a step the guided path enforces (apply_anti_sybil_settings). That item has a different mechanism and site, and it predates the reinstall guard.
  • #55 appendix, "silent abort under set -e" and "merge-lock race" (OhShii Labs): different mechanisms. I cite them only as precedent for filing a tooling gap outside SECURITY.md's scope at Low.
  • The 1.75 guard closed the full path and the plain door. This report covers the third remote door, which that fix left open. The pattern is the familiar one of a guard on one entry door and not its siblings.

Suggested fix

The frontend-only door's contract is "rebuild + sync the asset canister, nothing else" (:972). No in-tree caller passes a canister name to it. So refuse any named canister on that door, reusing pass_canister_names exactly as :885-889 does. Place the check next to the existing guards, before gen-did, so that the refusal is the only line printed (the ordering §6bh pins at :7122-7127):

if $FRONTEND_ONLY; then
  FO_NAMES="$(pass_canister_names ${PASS[@]+"${PASS[@]}"})"
  [ -z "$FO_NAMES" ] || die "REFUSING 'deploy.sh $TARGET frontend' naming '$FO_NAMES' as well: the frontend-only door deploys the frontend and nothing else, and every extra argument is forwarded to its icp deploy (with --mode reinstall that wipes the backend, bridge or arb). Nothing was deployed."
fi

pass_canister_names already treats everything after -- as a name, so subnet frontend -- backend --mode reinstall is refused too. In tests/test_deploy_hygiene.sh §6bh, add a driven case ri_run subnet frontend backend --mode reinstall that expects a non-zero exit and no ^deploy line in $RI_LOG, and reword :7118 so it no longer claims "excluded by construction".


Part 28 of 28 from an independent review of MULTI/DEX 1.80 (7bcdde6), carried out with Claude Opus 5.5. Each finding was checked against the existing issues, advisories and fix briefs before filing; the severity is a proposal under CONTRIBUTORS.md. Where a step says "traced", it was read from source and not executed.

Ngôn ngữ chính
Shell
Star
19
Fork
7
Chỉ số merge pull request
Không có pull request nào được merge trong 30 ngày

Chuẩn bị môi trường

Dự án này không cung cấp dev container, Dockerfile hay hướng dẫn đóng góp, nên bạn cần tự thiết lập môi trường: hãy bắt đầu từ README và xem hướng dẫn đóng góp lần đầu của chúng tôi để biết các bước chung.

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Issue khác của dfinity/public-multidex

Tất cả issue của dfinity/public-multidex

Issue tương tự

Thêm issue về Shell/Bash

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.