Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build/dependencies.lock
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ manifest = "build/legacy-manifest.json"
schema = 1
component_count = 326
file_count = 1967
byte_count = 260659274
byte_count = 260662316
source_build_components = 232
unresolved_components = 94
inherited_baseline = "07505ea58c7bba698d6b9220ff43946a43cac76b"
Expand Down
16 changes: 8 additions & 8 deletions build/legacy-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -475,8 +475,8 @@
"kind": "runtime",
"path": "static/build/.tmp_update/bin/bloom-dev-ssh",
"file_count": 1,
"byte_count": 13539,
"tree_sha256": "967940060ee432fe115d726afef2ad59ce07d07bcf1e11db5e3e7a71e5514b9f",
"byte_count": 15073,
"tree_sha256": "70773fe6f7eaad77eda356ba3ee3b0a37fcd4d8a4d42c9c2c1af2cab8f15bc66",
"source": "https://github.com/boburning/BloomOS",
"source_revision": "release-commit",
"license": "GPL-3.0-only",
Expand Down Expand Up @@ -943,8 +943,8 @@
"kind": "runtime",
"path": "static/build/.tmp_update/bin/bloom-update-rollback",
"file_count": 1,
"byte_count": 3745,
"tree_sha256": "d7be9a6cb1fe4617fa41d6c5f2fce733a117123e4e449614c58ac00f4b8f9da8",
"byte_count": 4117,
"tree_sha256": "0803729354c5b2938547023186619fb40e83270ebbcd376457ce68b22e769889",
"source": "https://github.com/boburning/BloomOS",
"source_revision": "release-commit",
"license": "GPL-3.0-only",
Expand All @@ -969,8 +969,8 @@
"kind": "runtime",
"path": "static/build/.tmp_update/bin/bloom-update-state",
"file_count": 1,
"byte_count": 11295,
"tree_sha256": "282a8c937578e14b76a50e396450447f8a4939aeae4365bde1d110466aea9c3c",
"byte_count": 12469,
"tree_sha256": "41ff306f6421230260a3e2f197d8c7c492b6b6ba90d1179767a104d613a6fde8",
"source": "https://github.com/boburning/BloomOS",
"source_revision": "release-commit",
"license": "GPL-3.0-only",
Expand Down Expand Up @@ -2256,8 +2256,8 @@
"kind": "runtime",
"path": "static/build/.tmp_update/script/network",
"file_count": 5,
"byte_count": 20018,
"tree_sha256": "bd82793eb547e1e1d3a922e39040ce631fb27c0dd3a6d1efe6e90152e6a8db0d",
"byte_count": 19980,
"tree_sha256": "7ba394cd30b420febe2d14de13f0cbc3e4aa053ab643c06137904faa8c7d8ba9",
"source": "https://github.com/OnionUI/Onion",
"source_revision": "07505ea58c7bba698d6b9220ff43946a43cac76b",
"license": "GPL-3.0-only",
Expand Down
51 changes: 50 additions & 1 deletion static/build/.tmp_update/bin/bloom-dev-ssh
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ STATE_FILE="$ROOT/tmp/bloom-dev-ssh.state"
PID_FILE="$ROOT/tmp/bloom-dev-ssh.pid"
SHADOW_STATE_FILE="$ROOT/tmp/bloom-dev-ssh-shadow.state"
SHADOW_PID_FILE="$ROOT/tmp/bloom-dev-ssh-shadow.pid"
LOCK_DIR="${BLOOM_DEV_SSH_LOCK_DIR:-$ROOT/tmp/bloom-dev-ssh.lock}"
LOCK_OWNER="$LOCK_DIR/owner"
PORT="${BLOOM_DEV_SSH_PORT:-22}"
SHADOW_PORT="${BLOOM_DEV_SSH_SHADOW_PORT:-2222}"
DROPBEAR="${BLOOM_DROPBEAR:-$SYS_DIR/bin/bloom-dropbearmulti}"
Expand All @@ -25,6 +27,49 @@ SFTP_CHROOT="$ROOT/tmp/bloom-sftp-root"
TRANSFER_POLICY=${BLOOM_TRANSFER_POLICY_BIN:-$SYS_DIR/bin/bloom-transfer-policy}
DEV_NULL=${BLOOM_DEV_NULL:-$ROOT/dev/null}
NETWORK_TRANSACTION=${BLOOM_NETWORK_TRANSACTION_BIN:-$SYS_DIR/bin/bloom-network-transaction}
LOCK_HELD=0

lock_cleanup() {
[ "$LOCK_HELD" -eq 1 ] || return 0
rm -f "$LOCK_OWNER" 2>/dev/null || true
rmdir "$LOCK_DIR" 2>/dev/null || true
LOCK_HELD=0
}

lock_acquire() {
lock_parent=${LOCK_DIR%/*}
[ -d "$lock_parent" ] && [ ! -L "$lock_parent" ] || return 1
lock_attempt=0
while ! mkdir "$LOCK_DIR" 2>/dev/null; do
[ -d "$LOCK_DIR" ] && [ ! -L "$LOCK_DIR" ] || return 1
lock_pid=''
[ -f "$LOCK_OWNER" ] && [ ! -L "$LOCK_OWNER" ] &&
IFS= read -r lock_pid <"$LOCK_OWNER" || true
case "$lock_pid" in
''|*[!0-9]*) ;;
*)
if [ -d "${BLOOM_PROC_ROOT:-$ROOT/proc}/$lock_pid" ]; then
[ "$lock_attempt" -lt 100 ] || return 1
lock_attempt=$((lock_attempt + 1))
sleep 0.05
continue
fi
;;
esac
rm -f "$LOCK_OWNER" 2>/dev/null || return 1
rmdir "$LOCK_DIR" 2>/dev/null || return 1
done
chmod 700 "$LOCK_DIR" || { rmdir "$LOCK_DIR" 2>/dev/null || true; return 1; }
(umask 077; printf '%s\n' "$$" >"$LOCK_OWNER") || {
rmdir "$LOCK_DIR" 2>/dev/null || true
return 1
}
LOCK_HELD=1
trap 'lock_cleanup' EXIT
trap 'exit 129' HUP
trap 'exit 130' INT
trap 'exit 143' TERM
}

state() {
mkdir -p "$(dirname "$STATE_FILE")"
Expand Down Expand Up @@ -345,8 +390,12 @@ start_ssh() {
}

case "${1:-}" in
start) start_ssh ;;
start)
lock_acquire || exit 1
start_ssh
;;
stop)
lock_acquire || exit 1
stop_dropbear
state stopped
;;
Expand Down
10 changes: 10 additions & 0 deletions static/build/.tmp_update/bin/bloom-update-rollback
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@ fail() { printf 'bloom-update-rollback: %s\n' "$*" >&2; exit 1; }
[ -x "$SNAPSHOT_BIN" ] || fail "save snapshot service is unavailable"
[ -x "$JQ" ] || fail "jq is unavailable"

current="$($STATE_BIN status)" || fail "update state is unavailable"
phase="$(printf '%s\n' "$current" | "$JQ" -er '.phase')" || fail "update state is invalid"
case "$phase" in
testing)
$STATE_BIN reject >/dev/null || fail "cannot reject the unconfirmed update"
;;
recovery_required) ;;
*) fail "no booted update is awaiting rollback" ;;
esac

recovery="$($STATE_BIN recovery)" || fail "signed recovery is unavailable"
version="$(printf '%s\n' "$recovery" | "$JQ" -er '.version')" || fail "recovery version is invalid"
case "$version" in ''|*[!A-Za-z0-9._+-]*) fail "recovery version is unsafe" ;; esac
Expand Down
19 changes: 18 additions & 1 deletion static/build/.tmp_update/bin/bloom-update-state
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,23 @@ case "${1:-}" in
'.phase=$phase | .boot_attempts=$attempts' "$STATE" | atomic_json "$STATE"
"$JQ" -c . "$STATE"
;;
reject)
[ "$#" -eq 1 ] || fail "reject takes no arguments"
[ -f "$STATE" ] && [ ! -L "$STATE" ] || fail "no update is awaiting validation"
phase="$(read_field "$STATE" '.phase')" || fail "current state is invalid"
[ "$phase" = testing ] || fail "no booted update is awaiting rejection"
operation="$("$JQ" -r '.operation // "update"' "$STATE")"
[ "$operation" = update ] || fail "rollback validation cannot be rejected recursively"
pending="$(read_field "$STATE" '.pending_version')" || fail "pending version is unavailable"
safe_version "$pending" || fail "pending version is unsafe"
[ -f "$KNOWN_GOOD" ] && [ ! -L "$KNOWN_GOOD" ] || fail "no known-good update is retained"
recovery_version="$(read_field "$KNOWN_GOOD" '.version')" || fail "known-good state is invalid"
safe_version "$recovery_version" && [ "$recovery_version" != "$pending" ] ||
fail "known-good recovery target is invalid"
"$JQ" -c '.phase="recovery_required" | .recovery_reason="operator_requested"' \
"$STATE" | atomic_json "$STATE"
"$JQ" -c . "$STATE"
;;
activation-start)
[ "$#" -eq 3 ] || fail "activation-start expects VERSION SNAPSHOT_ID"
version="$2"
Expand Down Expand Up @@ -178,5 +195,5 @@ case "${1:-}" in
[ -d "$path" ] && [ ! -L "$path" ] || fail "known-good payload is unavailable"
"$JQ" -c --arg status recovery_available '. + {status:$status}' "$KNOWN_GOOD"
;;
*) fail "expected {status|bootstrap VERSION|arm VERSION|activation-start VERSION SNAPSHOT_ID|rollback-start VERSION SNAPSHOT_ID|boot-attempt|mark-good VERSION|recovery}" ;;
*) fail "expected {status|bootstrap VERSION|arm VERSION|activation-start VERSION SNAPSHOT_ID|rollback-start VERSION SNAPSHOT_ID|boot-attempt|reject|mark-good VERSION|recovery}" ;;
esac
17 changes: 5 additions & 12 deletions static/build/.tmp_update/script/network/update_networking.sh
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,11 @@ check() {
check_services() {
local has_wifi=$(wifi_enabled && echo 1 || echo 0)
lockdown_legacy_listeners
check_sshstate &

# bloom-network-services is the sole SSH/SFTP lifecycle owner. Callers of
# this compatibility path reconcile it after Wi-Fi compatibility work is
# complete; starting bloom-dev-ssh here as well races its /home fallback
# mount and can leave a live listener without its authorized_keys path.

if [ "$is_booting" -eq 1 ] && [ "$has_wifi" -eq 1 ]; then
bootScreen Boot "Waiting for network..."
Expand Down Expand Up @@ -201,17 +205,6 @@ wifi_off() {
/customer/app/axp_test wifioff
}

# Starts only Bloom's explicit developer-mode, key-only SSH service.
check_sshstate() {
if $sysdir/bin/bloom-dev-ssh start; then
log "SSH: Bloom key-only service checked"
else
log "SSH: Bloom key-only service refused its configuration"
fi
disable_flag sshState
disable_flag authsshState
}

# Utility functions

# create/pop the json file for remembering states
Expand Down
48 changes: 46 additions & 2 deletions test/shell/developer_ssh.bats
Original file line number Diff line number Diff line change
Expand Up @@ -201,8 +201,10 @@ EOF
! grep -Fq dropbear "$MOCK_LOG"
}

@test "network initialization delegates developer mode to the secure helper" {
grep -F '$sysdir/bin/bloom-dev-ssh start' /workspace/static/build/.tmp_update/script/network/update_networking.sh
@test "network initialization has one canonical SSH lifecycle owner" {
! grep -F 'bloom-dev-ssh start' /workspace/static/build/.tmp_update/script/network/update_networking.sh
! grep -F 'check_sshstate' /workspace/static/build/.tmp_update/script/network/update_networking.sh
grep -F 'reconcile_services' /workspace/static/build/.tmp_update/bin/bloom-network
grep -F 'service_requested' /workspace/static/build/.tmp_update/bin/bloom-dev-ssh
}

Expand Down Expand Up @@ -244,3 +246,45 @@ EOF
[ "$(cat "$BLOOM_TEST_ROOT/tmp/bloom-dev-ssh.state")" = startup_failed ]
[ ! -e "$BLOOM_TEST_ROOT/tmp/bloom-dev-ssh.pid" ]
}

@test "concurrent reconciles serialize listener and runtime-key ownership" {
enable_flip
install_valid_key
cat >"$MOCK_BIN/dropbear" <<'EOF'
#!/bin/sh
printf '%s\n' "$0 $*" >>"$MOCK_LOG"
if [ "${1:-}" = dropbearkey ]; then
printf '%s\n' fixture-host-key >"$SDCARD/.bloom/ssh/dropbear_ed25519_host_key"
exit 0
fi
if [ "${1:-}" = dropbear ]; then
port_lock="$BLOOM_TEST_ROOT/tmp/mock-dropbear-port"
mkdir "$port_lock" 2>/dev/null || exit 98
cleanup() { rmdir "$port_lock" 2>/dev/null || true; }
trap 'cleanup; exit 0' HUP INT TERM
trap cleanup EXIT
sleep 30 &
wait
fi
EOF
chmod +x "$MOCK_BIN/dropbear"

"$SSH_HELPER" start >"$BATS_TEST_TMPDIR/first.out" 2>&1 &
first=$!
attempt=0
while [ ! -d "$BLOOM_TEST_ROOT/tmp/bloom-dev-ssh.lock" ] && [ "$attempt" -lt 40 ]; do
sleep 0.05
attempt=$((attempt + 1))
done
[ -d "$BLOOM_TEST_ROOT/tmp/bloom-dev-ssh.lock" ]
"$SSH_HELPER" start >"$BATS_TEST_TMPDIR/second.out" 2>&1 &
second=$!

wait "$first"
wait "$second"
server_pid="$(cat "$BLOOM_TEST_ROOT/tmp/bloom-dev-ssh.pid")"
kill -0 "$server_pid"
[ "$(cat "$BLOOM_TEST_ROOT/tmp/bloom-dev-ssh.state")" = running_key_only ]
[ ! -e "$BLOOM_TEST_ROOT/tmp/bloom-dev-ssh.lock" ]
[ "$(grep -Fc ' dropbear -F ' "$MOCK_LOG")" -eq 2 ]
}
21 changes: 21 additions & 0 deletions test/shell/update_rollback.bats
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ setup() {
cat >"$BLOOM_UPDATE_STATE_BIN" <<'EOF'
#!/bin/sh
case "$1" in
status)
printf '{"schema":1,"phase":"%s"}\n' "${ROLLBACK_PHASE:-recovery_required}"
;;
reject)
printf 'state:reject\n' >>"$BLOOM_TEST_ROOT/calls"
;;
recovery)
[ "${RECOVERY_AVAILABLE:-1}" = 1 ] || exit 1
printf '{"schema":1,"version":"1.2.2","status":"recovery_available"}\n'
Expand Down Expand Up @@ -49,6 +55,21 @@ EOF
chmod +x "$BLOOM_UPDATE_STATE_BIN" "$BLOOM_UPDATE_PREPARE_BIN" "$BLOOM_SAVE_SNAPSHOT_BIN" "$BLOOM_MOUNT_BIN" "$BLOOM_UMOUNT_BIN"
}

@test "operator-requested rollback rejects testing before publishing signed recovery" {
export ROLLBACK_PHASE=testing
mkdir -p "$SDCARD/miyoo/app"
printf 'installed MainUI binary\n' >"$SDCARD/miyoo/app/MainUI"

run "$ROLLBACK"

[ "$status" -eq 0 ]
[ "$(sed -n '1p' "$BLOOM_TEST_ROOT/calls")" = 'state:reject' ]
[ "$(sed -n '2p' "$BLOOM_TEST_ROOT/calls")" = 'prepare:--replace:1.2.2' ]
[ "$(sed -n '3p' "$BLOOM_TEST_ROOT/calls")" = 'snapshot:create:pre-rollback' ]
[ "$(sed -n '4p' "$BLOOM_TEST_ROOT/calls")" = 'state:rollback-start:1.2.2:snapshot-rollback' ]
grep -Fx fresh-core "$SDCARD/miyoo/app/.tmp_update/onion.pak"
}

teardown() { teardown_bloom_fixture; }

@test "rebuilds signed known-good payload before publishing rollback trigger" {
Expand Down
23 changes: 23 additions & 0 deletions test/shell/update_state.bats
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,29 @@ PY
printf '%s' "$output" | grep -F '"version":"1.2.3"'
}

@test "operator rejection enters signed recovery without repeated failed boots" {
stage_release 1.2.3
"$STATE" arm 1.2.3 >/dev/null
"$STATE" mark-good 1.2.3 >/dev/null
known_good_before="$(sha256sum "$BLOOM_UPDATE_ROOT/known-good.json" | awk '{print $1}')"
stage_release 1.2.4
"$STATE" arm 1.2.4 >/dev/null
"$STATE" activation-start 1.2.4 snapshot-update >/dev/null
"$STATE" boot-attempt >/dev/null

run "$STATE" reject
[ "$status" -eq 0 ]
printf '%s' "$output" | grep -F '"phase":"recovery_required"'
printf '%s' "$output" | grep -F '"recovery_reason":"operator_requested"'
printf '%s' "$output" | grep -F '"boot_attempts":1'
[ "$(sha256sum "$BLOOM_UPDATE_ROOT/known-good.json" | awk '{print $1}')" = "$known_good_before" ]
run "$STATE" recovery
[ "$status" -eq 0 ]
printf '%s' "$output" | grep -F '"version":"1.2.3"'
run "$STATE" reject
[ "$status" -eq 1 ]
}

@test "tracks rollback separately and preserves the retained known-good record" {
stage_release 1.2.3
"$STATE" arm 1.2.3 >/dev/null
Expand Down
Loading