Skip to content

feat(ostool-server): control axloader boot and OTA through device HTTP - #198

Merged
ZR233 merged 7 commits into
mainfrom
codex/axloader-ota-v4
Sep 29, 2026
Merged

ZR233 merged 7 commits into
mainfrom
codex/axloader-ota-v4

Conversation

@ZR233

@ZR233 ZR233 commented Sep 28, 2026 •

Copy link
Copy Markdown
Member

问题

旧版 axloader 依赖发现、轮询和下载:直连控制与 ostool-server 指派走不同路径,设备无法在没有服务端时用同一套接口完成内核启动。已有 OTA 镜像库和持久任务还需要接入设备拥有 HTTP 入口的协议,同时保持旧装载器可用。

改动与依据

  • 保留原有 v2/v3/v4 发现、轮询、启动和 OTA 响应;新增 v5 广播与设备状态、启动事务模型。服务端收到 v5 广播后,通过真实 HTTP GET 核对 MAC、架构和启动代次,才按板卡 TOML 的 MAC 绑定查找板卡;重复 MAC 的在线地址冲突停止下发。
  • v5 Session 沿用现有板卡租约与会话文件:从会话存储重新校验长度和 SHA-256,将内核及可选 initramfs 推送到设备的启动事务接口,最后请求交接。设备复位后重新观察启动代次并重试;服务端不向 v5 装载器发送文件下载 URL。
  • 继续复用公共 BootPayloadConfig { cmdline, initramfs }。两个字段独立可选,序列化时省略未配置字段,空 cmdline 保持兼容。v5 清单完整保留 cmdline 和 initramfs 元数据,并把兼容入口 httpboot_entry 转换为 __x86_64_efi_pe_entry;v2/v3/v4 继续收到原入口和字段。
  • 沿用独立于 Session 的 EFI 镜像库与持久任务。仅向板卡空闲、设备没有启动事务的设备指派 OTA;待试确认核对绑定、任务 ID、来源与运行摘要。服务端重启后读取任务并重新决策,直连升级不能被服务端确认。
  • 更新中英文入口说明和中文协议文档,并扩展隔离本地脚本:临时目录中的真实 FAT 磁盘、OVMF VARS、板卡与服务器配置;QEMU hostfwd 和广播帧转交。Session 使用真实 ArceOS EFI 内核、可选 cmdline 和 initramfs,服务端必须真正调用 QEMU 客户机 HTTP 接口。

审查反馈修复

  • 允许取消任意非终态 OTA 任务,持久清除对应设备观察记录;管理页面对所有非终态任务提供取消入口,服务重启后可以重新指派。
  • v5 状态缺少 OTA 持久区时记录告警并继续普通 Session 启动;设备 URL 通过 SocketAddr 生成,正确处理 IPv6 地址。
  • 同一启动代次存在旧 boot_id 时,服务端在创建返回 409 后删除状态中观察到的旧事务并只重试一次;删除返回 404 也视为事务已经消失。假设备测试覆盖替换成功、上传摘要拒绝后的重试,以及第二次创建仍冲突时停止。
  • OTA 的 downloading、staged、failed、succeeded 同阶段回报可幂等重试;终态重复回报不再依赖服务进程内观察记录。
  • v5 镜像投递失败会持久记录设备错误和投递次数,连续三次失败后任务进入 failed 并停止重发;管理指派只接受配置为 x86_64 UEFI HTTP 的板卡,避免把 AMD64 PE 指派给 AArch64。
  • 空 EFI 上传返回 400 empty EFI image,仅超过 32 MiB 返回 413,契约测试同时核对错误码和消息。
  • 新增镜像删除接口和管理页操作;非终态任务引用返回 409,终态任务保留摘要、长度和版本审计字段。上传、指派与删除按统一锁序串行,删除可重试清理中断后遗留的 EFI 文件;终态任务的旧 v4 下载返回 404。
  • 管理页面把 EFI 文件名清洗为服务端接受的 ASCII 版本标签,空标签不发送,覆盖空格、中文和 96 字节上限;上传成功后同步清空文件 input,允许再次选择并上传同一文件。
  • 补齐镜像库、任务、旧 v4 上报及 v5 设备接口的状态码契约,修正失效锚点,移除本地联调文档中的开发者绝对路径,并保持 README 中英文一致。
  • boot_arch 省略时与现有客户端一致按 x86_64 处理;AArch64 和非 HTTP Boot 板卡不再显示 OTA 面板,后端仍显式拒绝非 x86_64 指派。
  • 服务端来源、升级 ID 与运行摘要匹配的待试槽即使板卡已经进入活动 Session 也会先确认;直连来源仍等待自己的控制端确认,新镜像下发继续要求板卡空闲。
  • EFI 写入、校验、删除和目录同步移入 spawn_blocking,删除检查在阻塞段前释放任务锁;元数据启动时载入缓存并只在增删后更新,SSE 快照不再逐次扫描目录。异常元数据不会阻止服务启动,部分删除失败也不会留下可继续指派的缓存条目。
  • 恢复内建 QEMU 虚拟板的 [virtual_qemu] 字段、网络前提、绑定约束和六步验收流程,并在 API 文档与中英文 README 恢复入口。
  • 新增真实 loopback Axum 假设备测试,经 reqwest 覆盖启动推送、旧事务替换、摘要拒绝后的幂等重试、旧启动代次冲突、复位后 OTA 重推、确认摘要不匹配及再次确认。

验证

当前精确提交 3621fe8a83f372c28ef9764edeba6821ab11c27b:

  • cargo fmt --all -- --check
  • cargo clippy --target x86_64-unknown-linux-gnu --all-features -- -D warnings
  • cargo build --target x86_64-unknown-linux-gnu --all-features
  • cargo test --target x86_64-unknown-linux-gnu -- --nocapture:workspace 全部通过,ostool-server 164 项通过。
  • pnpm --dir ostool-server/webui test:26 项通过。
  • pnpm --dir ostool-server/webui build:通过。
  • python3 ostool-server/scripts/test-axloader-local.py --tgos /path/to/tgoskits-dev --server-bin target/x86_64-unknown-linux-gnu/debug/ostool-server:真实服务端发现 QEMU 装载器并推送真实内核、cmdline 与 initramfs,内核输出三个预期 marker;服务端重启后恢复 OTA 任务、确认新槽,再次启动仍使用该槽。结果记录位于本地隔离目录 /tmp/axloader-v5-local-k7a1_zy3/result.json。

本 PR 只更新代码与本地测试,不部署到 runner,也不刷写实体设备。接口当前仅用于可信隔离实验网;实体网络的广播可达性与反向路由仍需上线前验证。对应 TGOS 固件改动见 rcore-os/tgoskits#2535。

@ZR233 ZR233 changed the title feat(ostool-server): persist axloader OTA assignments feat(ostool-server): control axloader boot and OTA through device HTTP Sep 29, 2026
@ZR233
ZR233 marked this pull request as ready for review September 29, 2026 03:11

@mai-team-app mai-team-app Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

审查结论:COMMENT

没有阻塞性缺陷;CI 全绿、v2/v3 兼容路径未被破坏。但有 1 项恢复路径问题与若干文档/测试补充建议,详见下。

本 PR 做了什么

  • 新增 v5 设备协议(DEVICE_PROTOCOL_VERSION = 5):装载器在同一网卡广播 MAC/架构/启动代次/HTTP 端口,ostool-server 反向 GET /api/v1/status 核对 MAC、代次、架构后才按板卡 TOML 的 MAC 绑定下发;device::reconcile/push_boot/push_file 从会话存储重新核对长度与 SHA-256,把内核与可选 initramfs 直接推给设备的启动事务接口再请求交接,不再向 v5 装载器发送文件 URL。
  • 新增独立于 Session 的持久 OTA 镜像库与任务(ostool-server/src/ota.rs、api/ota.rs);v4 轮询可收到 update/confirm_update,PROTOCOL_VERSION 由 3 提到 4 并新增 PREVIOUS_PROTOCOL_VERSION = 3 继续兼容 v2/v3;LoaderPollResponse 增加两个变体,LoaderPollRequest 增加可选 ota 字段。
  • 管理面:镜像上传/指派/取消、ota SSE 主题、BoardEditor 内的 BoardOta 面板;文档重写与本地隔离脚本 ostool-server/scripts/test-axloader-local.py。

影响面与是否隔离

  • v2/v3 的发现/offer、poll、状态上报、启动契约保持不变,offer/accept_poll/accept_status 只是把可接受版本集合扩成 2|3|4,v2 仍只接收无 initramfs/cmdline 的启动;ostool CLI 不构造 LoaderPollRequest(只在 ostool/src/board/client.rs 拼 loader-status URL),因此协议常量变更不影响 CLI。
  • 公开契约变化:httpboot-protocol 0.3.0→0.4.0 且 PROTOCOL_VERSION 3→4,v4 poll 必须携带 ota,否则 ota_state_missing(见残余风险)。
  • LoaderRegistry::prune() 增加了 v5 冲突分支,但保留了原先基于 superseded report 的冲突语义(current_registration_id 在 registrations 中时行为与基线一致)。
  • 关键耦合:v4 poll 与 v5 reconcile 的启动下发现在都要先经过 OTA 决策(Decision::Wait 会直接 return),因此 OTA 状态机会影响既有启动路径 —— 见 F-001。
  • 未发现锁序问题:queue/decide/report 统一按 observations→jobs 取锁;decide/report 的持久化只在阶段真正变化时写入;put_image 的 PE 偏移校验有 checked_add(94) 边界,无越界读。

验证

  • CI(head 67ddda2):check (stable, x86_64-unknown-linux-gnu) 两条均 completed/success,覆盖 cargo fmt --all -- --check、clippy、build、cargo test、webui 单测/build/Playwright e2e 与 cargo publish --workspace --dry-run --locked;check run 上 4 条 ENOENT .../target/tests/target 注解指向 action 自身步骤,与本 PR 改动无关,没有由本 PR 引起的 CI 失败。
  • 本地:本审查环境没有 Rust 工具链(cargo/rustc 不存在)与 pnpm,无法复现 PR 描述的 cargo fmt/clippy/test 与前端命令,因此没有本地运行结果,验证结论以 CI 为准。
  • 阅读范围:23 个变更文件全部通读,重点核对 ota.rs / device.rs / api/ota.rs / loader.rs / api/router.rs 与既有 v2-v4 路径的交互、锁序、PE 校验边界与路径拼装。

既有意见

reviews=0、issue comments=0、review comments=0,没有需要跟踪的历史意见或未解决讨论。

建议处理(均非阻塞)

  1. F-001(advisory) ostool-server/src/ota.rs:251:取消只允许 Queued,而任何非终态任务都会阻断该板卡的启动下发,DELETE 与重新指派都不可用 —— 见内联评论。
  2. F-002(advisory) README.md:18 / README.en.md:18:两处仍把 docs/axloader-network-control.md 描述为“v3 宿主启动制品及 v2 兼容、持久 MAC 绑定、Web 管理和内建 QEMU 虚拟板设计”,但该文档已被本 PR 重写(标题改为“axloader 网络控制与本地验证”,删除了内建 QEMU 虚拟板/CLI 行为/HTTP 接口小节),中英描述都不再成立,也未提及 v4/v5。AGENTS.md 要求用户可见工作流变化时同步文档并保持中英一致(同作者 PR #194 就是这么做的),建议一并更新。
  3. F-003(advisory) docs/api.md:673:新增链接的锚点 #装载器升级接口v4 在新文档中不存在,本 PR 新增的 loader-images / loader-updates / loader-updates/{id}/image / loaders/ota-status 端点也没有文档 —— 见内联评论。
  4. F-004(advisory) webui/src/pages/BoardOta.tsx:35:把 file.name 当版本标签,含空格/中文的文件名会被服务端 400 拒绝 —— 见内联评论。
  5. F-005(advisory,测试缺口) 新增的 device.rs(reconcile/push_boot/push_file,338 行)没有仓库内自动化测试,唯一端到端证据是需要 TGOS 工作树 + OVMF + KVM + mtools 的手工脚本,CI 无法执行;router.rs 新增用例只覆盖服务端 poll/update/download/report。建议为推送与确认时序补一个本地假设备用例(hyper/axum 假设备 + 临时 OtaStore 目录),覆盖 epoch 不匹配、摘要不符、复位重推、确认校验失败等分支。
  6. F-006(advisory) device.rs:70:v5 状态里的 ota 是 Option,缺失时服务端静默跳过启动推送(既不下发 OTA 也不下发启动,且没有任何日志)。建议记一次明确 warn 或按空状态继续走启动分支,并在文档中把 ota 写成 v5 必需字段。

残余风险(不阻塞)

  • v5 广播与设备 HTTP 无认证:任何能发 UDP 广播的主机都能让服务端对 peer.ip():http_port 发 GET /api/v1/status,并在有排队任务时把镜像推给它。与文档声明的“可信隔离实验网”一致,但上线前需要网络隔离或后续认证。
  • PROTOCOL_VERSION 3→4 且 v4 poll 必须带 ota:对 0.4.0 crate 的其他消费方是隐性不兼容(不带 ota 的 v4 设备会被 ota_state_missing 拒绝);TGOS 目前锁 0.3.0(版本 3),不受影响。
  • loader_network.bind_addr 若被配置为 IPv6([::]:2998),format!("http://{}:{}", peer.ip(), port) 生成的 URL 不带方括号,reconcile 会持续失败;默认值 0.0.0.0:2998 无此问题。

Powered by deepseek-flash

Comment thread ostool-server/src/ota.rs Outdated
Comment thread docs/api.md Outdated
Comment thread ostool-server/webui/src/pages/BoardOta.tsx Outdated

@mai-team-app mai-team-app Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

审查结论:COMMENT(无阻塞问题)

本 PR 做了什么

  • 新增 v5 设备协议(DEVICE_PROTOCOL_VERSION = 5):装载器在同一 UEFI 网卡广播 MAC/架构/启动代次/HTTP 端口,loader::serve_udp_discovery() 调用 device::reconcile(),先反向 GET /api/v1/status 核对 MAC、启动代次与架构,再按板卡 TOML 的 MAC 绑定决策;device::push_boot/push_file 从会话存储重新核对长度与 SHA-256 后经设备 HTTP 推送,不再给 v5 装载器文件 URL。
  • 新增独立于 Session 的持久 OTA 镜像库与任务(ostool-server/src/ota.rs、api/ota.rs):v4 轮询可收到 update/confirm_update,PROTOCOL_VERSION 3→4 并新增 PREVIOUS_PROTOCOL_VERSION = 3 继续兼容 v2/v3,httpboot-protocol 0.3.0→0.4.0。
  • 管理面:镜像上传/指派/取消、ota SSE 主题、BoardEditor 内的 BoardOta 面板;文档重写(含端点与状态码表)与本地隔离脚本 ostool-server/scripts/test-axloader-local.py。

影响面与是否隔离

  • v2/v3 未被破坏:offer/accept_poll/accept_status 只把可接受版本扩成 2|3|4;LoaderPollRequest.ota 带 serde(default) + skip_serializing_if,旧 v3 JSON 仍可解析;v2 仍只在无 initramfs/cmdline 时接收启动;prune() 的 v5 冲突分支仅在 current_registration_id 不在 registrations 时生效,v2-v4 冲突语义与基线一致。ostool CLI 不构造 LoaderPollRequest,协议常量变更不影响 CLI。
  • 有意的契约变化(已文档化):0.4.0 的 v4 poll 必须携带 ota,否则得到 ota_state_missing;LoaderPollResponse 新增两个变体。TGOS 目前锁 0.3.0(版本 3),不受影响。
  • 关键耦合:v4 poll 与 v5 reconcile 的启动下发现在都先经过 OTA 决策,Decision::Wait 会直接返回,OTA 状态机已成为启动路径的一部分(见下方 F-001 与残余风险)。锁序一致(observations→jobs,queue/decide/report/cancel 均相同),未发现死锁、越界读(put_image 的 PE 偏移有 checked_add(94) 边界)或 RAII 清理缺口。

验证

  • CI(当前 head c06bfcb):两条 check (stable, x86_64-unknown-linux-gnu) 均 completed/success、annotation 为 0,覆盖 cargo fmt --all -- --check、clippy、build、cargo test、webui 单测/build/Playwright e2e 与 cargo publish --workspace --dry-run --locked。没有由本 PR 引起的失败。
  • 本地:本环境缺 C 工具链与 pnpm,cargo test/clippy 无法编译(linker cc not found),前端命令未运行。已成功运行:cargo fmt --all -- --check(通过)、cargo metadata --locked(退出码 0,说明本 PR 的 Cargo.lock 与清单一致)。
  • 阅读范围:27 个变更文件全部核对,重点复核 ota.rs 状态机与持久化、device.rs 推送/确认、api/ota.rs 契约、loader.rs 的 v5 分发与 prune。

既有意见

之前 Mai 审查(head 67ddda2)的 6 条意见均非阻塞,逐条核实已在 c06bfcb 修复:取消任意非终态任务并清除观察记录(cancel() + active_update_can_be_cancelled_and_replaced_after_restart,UI 对所有非终态显示取消入口);README 中英描述已更新;锚点修正并补齐 loader-images/loader-updates/DELETE/image/ota-status 状态码表;前端 sanitizeImageVersion + Vitest 覆盖空格/中文/空标签/96 字节;新增两个 axum 假设备测试覆盖 epoch 变化重推、摘要拒绝重试、确认摘要不匹配、无 ota 时的启动推送;无 OTA 状态改为告警并继续启动推送。作者 3 条回复与代码一致,无需继续跟踪。

未解决项(均为 advisory,见内联评论)

  1. F-001 ostool-server/src/device.rs:219:push_boot() 只用 create 建立事务,未使用协议中定义的 DELETE /api/v1/boot/jobs/{id};同一设备启动代次内旧清单仍在时,新 boot_id 的清单会持续 409,启动推送无法送达。
  2. F-002 ostool-server/src/api/ota.rs:54:Content-Length: 0 与超限共用 413 EFI image exceeds 32 MiB,与 docs/api.md 的 400/413 契约不一致。
  3. F-003(body-only) 持久镜像库只增不减:无删除端点也无回收策略,长期运行会让 data_dir 无限增长(每个 ≤32 MiB),GET /api/v1/admin/loader-images 还会把全部镜像返回到管理页。

残余风险(不阻塞)

  • v5 广播与设备 HTTP 无认证:任何能发 UDP 广播并可指定端口的主机都能让服务端对 peer.ip():http_port 发 GET /api/v1/status,并在有排队任务时把镜像推给它。与文档声明的“可信隔离实验网”一致,上线前需网络隔离或后续认证。
  • 待试槽只允许在板卡空闲时确认:若在设备进入待试槽后、下一次广播前启动 Session,服务端会因 board_idle=false 持续返回 Wait,该 Session 的启动推送被暂缓,直到设备按自身 A/B 规则回滚。文档已说明该语义,但缺少自动化覆盖。
  • 端到端证据仍依赖 ostool-server/scripts/test-axloader-local.py(需 TGOS 工作树 + OVMF + KVM + mtools),CI 不执行;文档示例中的 --tgos /home/zhourui/.codex/worktrees/9a24/tgoskits-dev 是开发者本机绝对路径,建议改为占位路径以便复现。
  • 相似 PR:仓库当前仅本 PR 与 #196(FIT 生成)处于打开状态,无重复或重叠工作。

Powered by deepseek-flash

Comment thread ostool-server/src/device.rs
Comment thread ostool-server/src/api/ota.rs Outdated
@ZR233

ZR233 commented Sep 29, 2026

Copy link
Copy Markdown
Member Author

本轮审查中的 body-only F-003 已在 678b227 一并处理:

  • 新增 DELETE /api/v1/admin/loader-images/{sha256} 和管理页删除操作;
  • 非终态任务引用时返回 409,终态任务保留摘要、长度和版本审计字段;
  • 上传、指派、枚举与删除使用统一的进程内目录互斥和锁序;删除中断留下单侧文件时可重试清理;
  • 旧 v4 下载只接受非终态任务,终态镜像删除后返回 404;
  • 覆盖 204/400/404/409、跨重启物理文件回收、历史任务保留和前端禁用/删除行为。

文档中的本机绝对 TGOS 路径也已替换为 /path/to/tgoskits-dev。

@mai-team-app mai-team-app Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

审查结论:COMMENT(无阻塞问题)

本 PR 做了什么

  • v5 设备协议(DEVICE_PROTOCOL_VERSION = 5):装载器在同一网卡广播 MAC、架构、启动代次与设备 HTTP 端口,loader::serve_udp_discovery() 把 v5 广播交给 device::reconcile(),先反向 GET /api/v1/status 核对 MAC、代次、架构(LoaderRegistry::accept_announcement),再从板卡 TOML 按 MAC 查绑定;device::push_boot/push_file 从会话存储重新核对长度与 SHA-256,经设备 HTTP 提交启动事务并请求交接,不再向 v5 装载器发送文件下载 URL。
  • 独立于 Session 的持久 OTA 镜像库与任务(ostool-server/src/ota.rs、api/ota.rs):镜像上传/枚举/删除、按板卡指派/查询/取消、旧 v4 下载与阶段上报、ota SSE 主题、BoardEditor 内的 BoardOta 面板。
  • httpboot-protocol 0.3.0→0.4.0(PROTOCOL_VERSION 3→4,新增 PREVIOUS_PROTOCOL_VERSION = 3、DEVICE_PROTOCOL_VERSION = 5 与 v5 消息类型)、文档重写与本地隔离脚本 ostool-server/scripts/test-axloader-local.py。

影响面与隔离性

  • v2/v3 旧路径未被破坏:offer/accept_poll/accept_status 只把可接受版本集合扩成 2|3|4;LoaderPollRequest.ota 带 serde(default) + skip_serializing_if,旧 JSON 仍可解析;v2 仍只在无 initramfs/cmdline 时接收启动。prune() 新增的 v5 冲突分支只在 current_registration_id 不在 registrations 且记录仍在线时生效,v2-v4 冲突语义与基线一致;v5 重复 MAC 冲突会一直保持,直到记录中那个 IP 的实例离线超过 10 秒后由新地址接管,未发现状态泄漏。
  • 有意的契约变化:0.4.0 的 v4 poll 必须携带 ota,否则得到 ota_state_missing;LoaderPollResponse 新增 Update/ConfirmUpdate。仓库内只有 ostool-server 使用这些常量,ostool CLI 不构造 LoaderPollRequest,不受影响。
  • 关键耦合:v4 poll 与 v5 reconcile 的启动下发现在都先经过 OTA 决策,Decision::Wait 会让 poll 回 bound_idle、让 reconcile 直接结束本轮,OTA 状态机已成为既有启动路径的一部分(文档已说明“待试未确认时设备自身拒绝内核启动”的语义)。
  • 锁序核对:queue/decide/report/cancel 统一 observations→jobs,delete_image 为 jobs→image_io,queue_job 为 boards→runtimes(与既有 create_session_inner/sync_board_runtime_states 相同),未发现锁序反转、持锁 await 或 std 锁跨 await;put_image 的 PE 偏移有 checked_add(94) 边界,未发现越界读。

验证

  • CI(head 678b227):两条 check (stable, x86_64-unknown-linux-gnu) 均 completed/success、annotation 0,覆盖 cargo fmt --all -- --check、clippy、build、cargo test、webui 单测/build/Playwright e2e 与 cargo publish --workspace --dry-run --locked;没有由本 PR 引起的失败。
  • 本地:本审查环境没有 Rust 工具链(cargo/rustc 均不存在)与 pnpm,无法复现 PR 描述的 cargo fmt/clippy/test 与前端命令,验证结论以 CI 为准。已执行:review_pr_helper.py prepare-review(HEAD/工作树校验通过)与 git diff --unified=0 的行级可评论性核对(用于下方内联评论)。
  • 阅读范围:29 个变更文件全部核对,重点复核 ota.rs 状态机与持久化、device.rs 推送/确认/替换、api/ota.rs 契约、loader.rs 的 v5 分发与 prune、webui 的 BoardOta/client/types/events。

既有意见

上一轮 Mai 审查(head c06bfcb,job 34d006fc)的 3 条意见已在 678b227 逐条修复并复核:push_boot() 现在读取起始状态中的旧 boot_id,首次 create 返回 409 时携带同一 X-Boot-Epoch 删除旧事务、要求 204 后只重试一次,假设备用例覆盖“旧清单替换”“上传摘要拒绝后重试”“删除后二次仍 409 时停止”;Content-Length: 0 返回 400 empty EFI image、仅超限返回 413,路由契约测试同时断言 code/message;新增镜像删除接口与页面入口,覆盖“非终态引用返回 409、终态引用可删且保留摘要/长度/版本”。更早一轮(job 2f97351a)的取消任意非终态任务、锚点与端点补齐、文件名清洗、README 中英一致意见同样已落实。作者回复与代码一致,无需继续跟踪。

未解决项(均 advisory,非阻塞)

  1. F-001 ostool-server/src/ota.rs:438:重复上报同一阶段(staged/succeeded/failed)被 409 拒绝,且与相邻“终态同阶段可再报”的判断自相矛盾 —— 见内联评论。
  2. F-002 ostool-server/src/device.rs:94:设备拒绝 OTA 上传时任务会永久停在 downloading 且从不写 job.error;另 231-233 行要求旧事务删除必须返回 204,404 会让整轮推送放弃 —— 见内联评论。
  3. F-003 ostool-server/webui/src/pages/BoardOta.tsx:44:上传成功后未清空 file input,同一文件无法再次上传 —— 见内联评论。
  4. F-004(body-only) docs/axloader-network-control.md:重写删除了“内建 QEMU 虚拟板”整节,[virtual_qemu] 配置键(ovmf_code/tap_pool/network_namespace/bridge/memory_mib 等)与虚拟板验收流程在仓库文档中已不可查,而该功能与代码本 PR 未改动(src/virtual_qemu.rs、/api/v1/admin/virtual-devices 与 virtual-lab 子命令仍在,docs/api.md:489-491 仍宣传 virtual-lab up/status/down),README 也不再指向虚拟板设计。建议补回该配置参考(或移入 docs/api.md 的 virtual-lab 小节旁),并保持中英一致。

残余风险(不阻塞)

  • v5 广播、设备 HTTP 与 POST /api/v1/loaders/ota-status 均无认证:能发广播或能取得 registration_id 的主机可让服务端对 peer.ip():http_port 发起状态查询与镜像推送,也可自行上报阶段与结果。与文档声明的“可信隔离实验网”一致,上线前需要网络隔离或后续认证。
  • 0.4.0 的 v4 poll 必须携带 ota,对升级到 0.4.0 的其他消费方是隐性不兼容(缺字段即 ota_state_missing,且不带 retry_after_ms);TGOS 锁 0.3.0,暂不受影响。
  • v5 设备侧(TGOS/axloader)请求类型目前两仓各自定义,一致性只由需要 TGOS 工作树 + OVMF + KVM + mtools 的手工脚本 ostool-server/scripts/test-axloader-local.py 证明,CI 不执行;服务端侧已有 axum 假设备与路由契约测试覆盖。
  • 管理端 ota 主题在 admin_topic 失败时下发 {"error": ...},而前端按 LoaderOtaResources 使用(ota?.jobs.find),与其它主题一样缺少防御性处理(既有模式,非本 PR 引入)。
  • 相似 PR:仓库当前仅本 PR 与 #196(FIT 生成)处于打开状态,无重叠或重复工作。

Powered by deepseek-flash

Comment thread ostool-server/src/ota.rs
Comment thread ostool-server/src/device.rs Outdated
Comment thread ostool-server/webui/src/pages/BoardOta.tsx

@mai-team-app mai-team-app Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

审查范围与结论

本 PR(head 097d373f,base 04170c34)为 axloader 引入协议 v5:设备通过 UDP 2998 单向广播自身 HTTP 入口,ostool-server 反向调用设备接口完成启动交接(POST /api/v1/boot/jobs + PUT …/{id}/kernel|initramfs + POST …/{id}/start)与 OTA 下发(PUT /api/v1/ota/image、POST /api/v1/ota/confirm),并新增独立于 Session 的持久 OTA 镜像库与任务(ostool-server/src/ota.rs、api/ota.rs、管理页 BoardOta.tsx)。旧 v2/v3/v4 的发现、poll、状态上报与下载 URL 路径保留;httpboot-protocol 新增 DEVICE_PROTOCOL_VERSION = 5 与 PREVIOUS_PROTOCOL_VERSION = 3,LoaderPollRequest 增加可选 ota(v4 缺失时返回 ota_state_missing)。最后一个提交把 v5 清单构造抽成 v5_boot_manifest,固定把兼容入口 httpboot_entry 转换为 __x86_64_efi_pe_entry(cmdline/initramfs 仍按可选字段原样保留),并让 BootPayloadConfig 序列化时省略未配置字段。

结论:未发现阻塞性问题;但有 3 项上一轮已提出、本 head 仍未处理的非阻塞问题,因此以 COMMENT 提交(详见下方内联评论)。

契约与影响面

  • 新增管理端与设备端接口:GET|POST /api/v1/admin/loader-images、DELETE …/{sha256}、GET|POST /api/v1/admin/boards/{board_id}/loader-updates、DELETE …/{update_id}、GET /api/v1/loader-updates/{update_id}/image、POST /api/v1/loaders/ota-status。docs/api.md 已补齐成功/错误状态码契约(空镜像 400、超限 413、非终态引用 409、终态下载 404),与代码一致。
  • 旧装载器兼容:loader.rs/router.rs 对 v2/v3/v4 的 Offer/poll/status 判定改为 LEGACY | PREVIOUS | PROTOCOL_VERSION,v2/v3/v4 poll 响应仍返回原 entry_symbol 与可选字段(api/router.rs 测试同时覆盖 v3 与 v2)。PROTOCOL_VERSION 3→4 只影响本 PR 同时引入的 v4 OTA 语义,未发现对既有 v4 客户端的破坏(base 中不存在 v4 客户端)。
  • 共享状态与并发:OTA 任务持久化在 jobs.json(状态)与 devices.json(观测),镜像在 images/,锁序统一为 observations → jobs → image_io;delete_image 为 jobs → image_io,未发现锁序反转或死锁路径,生产路径无 unwrap/panic(ota.rs 两处 unwrap 均由前置 get 保证)。管理路由沿用既有 notify_admin_changes 层并额外 invalidate(["ota"])。
  • 前端新增 ota SSE 集合,BoardEditor 仅在板卡绑定 MAC 后挂载 BoardOta;sanitizeImageVersion 与服务端 is_ascii_graphic/96 字节校验一致。

CI 与本地验证

  • CI:head 097d373f 的两个 check (stable, x86_64-unknown-linux-gnu) check-run 均为 completed/success(06:39/06:40 UTC),覆盖 cargo fmt --check、clippy、build、cargo test 与 webui pnpm test/build。
  • 本审查容器内没有 Rust 工具链(cargo/rustc 不存在)也没有 pnpm,因此未能本地重跑 fmt/clippy/cargo test 与前端测试;上述结论基于 CI 结果与静态阅读。
  • 已执行:prepare-review 校验(head 与工作树一致、干净)、git rev-parse HEAD = 097d373f…,并用脚本核对本 PR 涉及的跨文件文档锚点均可解析。

上一轮审查评论

上一轮(针对 678b2278)的 6 项评论中:取消任意非终态任务、docs/api.md 失效锚点与端点表、前端版本标签清洗已在 c06bfcb 修复;push_boot 的 409 旧事务替换、空 EFI 上传的 400/413 区分已在 678b227 修复,本轮复核确认落码且与文档一致。剩余 3 项(ota.rs 阶段上报、device.rs 拒绝处理、BoardOta.tsx 文件输入)在 097d373f 上仍成立且无回复,本轮继续以内联评论提出。

未解决的非阻塞问题

  1. ostool-server/src/ota.rs:438 — 同阶段重复上报被判 invalid OTA phase transition(映射 409),而第 433-436 行的守卫显式允许终态同阶段重发,该分支不可达。
  2. ostool-server/src/device.rs:96 — 设备拒绝 OTA 上传时错误只进 log::warn!:任务停在 Downloading、job.error 为空、每次广播重复上传整份镜像;镜像库仅接受 AMD64 PE,而指派不校验设备架构,AArch64 板卡是必然失败组合。
  3. ostool-server/webui/src/pages/BoardOta.tsx:44 — 非受控 <input type="file"> 上传成功后未清空 value,再选同一文件不触发 change,上传按钮持续禁用。

残余风险(不阻塞,建议后续单独跟踪)

  • v5 发现与 HTTP 控制无认证、MAC 仅作绑定键:在设备离线超过 ONLINE_TTL 的窗口内,同网段伪造广播可令服务器把 Session 内核/initramfs 与 OTA 镜像上传到伪造端点;文档已声明本阶段仅用于可信隔离实验网,实体网络上线前需单独验收(PR 描述亦如此声明)。
  • GET /api/v1/loader-updates/{update_id}/image 为公开非管理端点,仅凭不可猜测的 update_id 即可下载镜像字节,未校验请求方 MAC。
  • put_image 在持有 std::sync::Mutex 时同步写至多 32 MiB 并 fsync,delete_image 在持有 jobs 异步锁时做阻塞文件操作,并发上传/删除期间会占用 tokio worker;建议改 spawn_blocking 或缩小锁范围。
  • ostool-server/scripts/test-axloader-local.py 需要本地 TGOS 产物与 OVMF/QEMU,本容器无法执行;该脚本与 device.rs 的 loopback 假设备测试仅能以 CI 与作者记录的手工验证为证据。

Powered by deepseek-flash

Comment thread ostool-server/src/ota.rs
Comment thread ostool-server/src/device.rs Outdated
Comment thread ostool-server/webui/src/pages/BoardOta.tsx

@mai-team-app mai-team-app Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

审查结论:COMMENT(无阻塞问题)

本 PR 做了什么

  • 引入 v5 设备协议(DEVICE_PROTOCOL_VERSION = 5):装载器在同一 UEFI 网卡广播 MAC、架构、启动代次与设备 HTTP 端口,loader::serve_udp_discovery() 把 v5 广播交给 device::reconcile(),先反向 GET /api/v1/status 核对 MAC、启动代次与架构(LoaderRegistry::accept_announcement),再按板卡 TOML 的 MAC 绑定决策;device::push_boot/push_file 从会话存储重新核对长度与 SHA-256 后把内核与可选 initramfs 推送到设备的启动事务接口并请求交接,不再向 v5 装载器下发文件下载 URL。
  • 新增独立于 Session 的持久 OTA 镜像库与任务(ostool-server/src/ota.rs、api/ota.rs):镜像上传/枚举/删除、按板卡指派/查询/取消、旧 v4 下载与阶段上报、ota SSE 主题、BoardEditor 内的 BoardOta 面板。httpboot-protocol 0.3.0→0.4.0(PROTOCOL_VERSION 3→4,新增 PREVIOUS_PROTOCOL_VERSION = 3、DEVICE_PROTOCOL_VERSION 与 v5 消息类型),LoaderPollRequest 增加可选 ota。
  • 文档重写,并新增本地隔离脚本 ostool-server/scripts/test-axloader-local.py(临时目录中的真实 FAT 磁盘、OVMF VARS、hostfwd 与广播帧转交)。

影响面

  • v2/v3 旧路径未被破坏:offer/accept_poll/accept_status 只是把可接受版本集合扩成 2|3|4;LoaderPollRequest.ota 带 serde(default) + skip_serializing_if,旧 JSON 仍可解析;LoaderPollResponse 是 tag = "state" 的内部标签枚举,新增 Update/ConfirmUpdate 不会影响旧装载器;prune() 的 v5 冲突分支只在 current_registration_id 不在 registrations 且记录仍在线时生效,v2-v4 冲突语义与基线一致。ostool CLI 不构造 LoaderPollRequest,协议常量变更不影响它。
  • 有意的契约变化(已文档化):0.4.0 的 v4 poll 必须携带 ota,否则得到 ota_state_missing;TGOS 目前锁 0.3.0(版本 3),不受影响。
  • 关键耦合:v4 poll 与 v5 reconcile 的启动下发都要先经过 OTA 决策,Decision::Wait 会直接跳过本轮启动推送(见 F-002)。
  • 并发与边界核对:queue/decide/report/cancel 统一按 observations→jobs 取锁,queue_job 为 boards→ota,delete_image 为 jobs→image_io,未发现锁序反转、持 std 锁跨 await 或生产路径 panic/unwrap;put_image 的 PE 解析用 checked_add(94) 限定边界,pe_offset+94 覆盖了所有硬编码偏移,未发现越界读。

验证

  • CI(当前 head b5b3f2e):两条 check (stable, x86_64-unknown-linux-gnu) 均 completed/success、annotations 0(07:04–07:11 UTC),覆盖 cargo fmt --all -- --check、clippy、build、cargo test、webui pnpm test/build、Playwright e2e 与 cargo publish --workspace --dry-run --locked;没有由本 PR 引起的失败。补充一点上下文:工作流的 clippy 步骤是 cargo clippy --target ... --all-features,没有 -D warnings,所以 lint 回归不会让 CI 失败,PR 描述里的 -D warnings 只来自本地运行。
  • 本地:本审查容器没有 Rust 工具链(cargo/rustc 不存在)也没有 pnpm,未能重跑 fmt/clippy/test 与前端命令,验证结论以 CI 为准。已执行:review_pr_helper.py prepare-review(HEAD 与工作树校验通过,HEAD = b5b3f2e)、git diff --unified=0 origin/main...HEAD(逐条核对下方内联评论的行级可评论性)以及全部引用锚点的复核。

既有意见

上一轮(job 30c24a05,head 097d373)的 3 条意见已在 b5b3f2e 逐条落实,本轮复核确认:

  1. report() 同阶段重复上报被判 409 → 现在 job.phase == phase 时按幂等请求处理,只在错误文本变化时持久更新(ota.rs:437-446),新增 duplicate_phase_reports_are_idempotent 覆盖 downloading/staged/failed/succeeded。
  2. 设备拒绝镜像只留 warn、任务长期停在 downloading → 新增 record_delivery_failure(ota.rs:481-511)持久记录设备错误与投递次数,连续 3 次进入 Failed 并停止重发,device.rs 的假设备用例断言错误文本与计数;删除旧启动事务时把 404 视为“已不存在”(device.rs:246),用例覆盖替换成功、摘要拒绝后重试、二次仍 409 时停止。
  3. 上传成功后未清空 file input → BoardOta.tsx 通过 ref 同时清空 DOM value 与状态,BoardOta.test.tsx 覆盖同一文件二次选择并上传。

更早两轮(job 92d29d28 / job 34d006fc、job 2f97351a)的其余意见(取消任意非终态任务并清理观察记录、docs/api.md 失效锚点与端点状态码表、EFI 文件名清洗、README 中英一致、Content-Length: 0 → 400 empty EFI image、镜像删除接口与终态审计字段)同样已在 c06bfcb/678b227 落码。作者回复与代码一致,无需继续跟踪。

本轮提出的非阻塞问题(均为 advisory)

  1. F-001(内联 ostool-server/src/api/ota.rs:119):指派架构门槛把 boot_arch 省略的合法 x86_64 HTTP Boot 板卡一并拒绝,消息也会误导。
  2. F-002(内联 ostool-server/src/ota.rs:383):待试确认以 board_idle 为前提,与“待试槽未确认前设备拒绝启动内核”叠加,会让随后启动的 Session 一直等不到启动推送。
  3. F-003(body-only,docs/axloader-network-control.md + docs/api.md):重写删除了“内建 QEMU 虚拟板”整节与 [virtual_qemu] 配置参考,而虚拟板代码与 virtual-lab 子命令仍在,文档不再可查。
  4. F-004(内联 ostool-server/src/ota.rs:207):镜像库的阻塞文件 I/O(≤32 MiB 写盘 + fsync、read_dir/read、remove_file + fsync)直接跑在 async handler 内。

残余风险(不阻塞,建议后续单独跟踪)

  • v5 广播、设备 HTTP 与 POST /api/v1/loaders/ota-status 均无认证,MAC 仅作绑定键:能发 UDP 广播的主机可让服务端对 peer.ip():http_port 发起状态查询与镜像/内核推送,也能自行上报阶段与结果。与文档声明的“可信隔离实验网”一致,实体网络上线前需要网络隔离或后续认证。
  • device::push_boot/push_file 用硬编码前缀 /boot/sessions/{session_id}/ 从公开 URL 反解会话内相对路径;http_boot.public_base_url 若带路径前缀(append_url_path 会保留既有路径段)会导致 strip_prefix 失败,v5 启动推送每轮只留一条 warn。默认配置(public_base_url 为空)不受影响。
  • 设备状态缺少 ota(无 OTA 持久区)时,每轮广播都会写一条 log::warn!,长期运行可能刷日志,建议降级为限频日志。
  • 端到端证据仍依赖 ostool-server/scripts/test-axloader-local.py(需 TGOS 工作树 + OVMF + KVM + mtools),CI 不执行;服务端侧已有 loopback 假设备与路由契约测试覆盖。
  • 相似 PR:本 PR 之外仓库只有 #196(FIT 生成)处于打开状态,无重叠或重复工作。

Powered by deepseek-flash

Comment thread ostool-server/src/api/ota.rs
Comment thread ostool-server/src/ota.rs Outdated
Comment thread ostool-server/src/ota.rs Outdated
@ZR233

ZR233 commented Sep 29, 2026

Copy link
Copy Markdown
Member Author

已在 3621fe8 处理本轮 body-only F-003:恢复 docs/axloader-network-control.md 的内建 QEMU 虚拟板章节,补回完整 [virtual_qemu] 字段、virtual-lab 前提、默认网络、绑定约束和六步验收流程;docs/api.md 增加配置示例与交叉链接,README 中英文入口同步恢复。

@mai-team-app mai-team-app Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

变更概述

PR #198 在保留 axloader v2/v3/v4(UDP Offer + /api/v1/loaders/poll + 状态上报 + 下载 URL)的前提下,新增 v5「设备自持 HTTP 入口」控制路径:

  • httpboot-protocol 0.3.0 → 0.4.0:PROTOCOL_VERSION = 4、新增 DEVICE_PROTOCOL_VERSION = 5 与 PREVIOUS_PROTOCOL_VERSION = 3,以及 LoaderAnnouncement/LoaderDeviceStatus/DeviceBootJob/LoaderOtaState 等类型。
  • ostool-server:新增 device::reconcile()(广播 → 真实 HTTP GET /api/v1/status 核对 MAC/代次/架构 → 按板卡 TOML 的 MAC 绑定 → 反向推送启动事务或 OTA)、ota::OtaStore(镜像库 + 持久升级任务 + 观察记录)、api/ota.rs(管理端镜像/任务接口、v4 loader-updates/{id}/image、loaders/ota-status)与 ota SSE 主题;管理页新增 BoardOta 面板与镜像版本清洗。
  • ostool:BootPayloadConfig 的 cmdline/initramfs 增加 skip_serializing_if,未配置字段不再序列化。

既有契约与耦合面

  • 兼容性:v2/v3 装载器仍被 offer/accept_poll/accept_status 接受(PREVIOUS_PROTOCOL_VERSION 已加入白名单);v4 poll 新增「必须携带 ota 状态」的要求(缺失返回 ota_state_missing),LoaderPollRequest.ota 带 serde default + skip_serializing_if,JSON 契约向后兼容;LoaderPollResponse 新增的 Update/ConfirmUpdate 只会发给 v4 装载器。仓库内 5 处 LoaderPollRequest 构造点(含测试)均已补齐 ota 字段。
  • Session/租约:v5 推送复用现有 Session、板卡租约与 loader 状态;串口/WebSocket 生命周期未改动。新增 OTA 持久状态独立于 Session(loader-ota/jobs.json、devices.json、images/),服务端重启后可恢复任务。
  • 锁序:OtaStore 统一为 image catalog → observations → jobs(queue/delete_image/decide/report/cancel 一致),未发现锁序反转;镜像写入、摘要校验、删除与目录 fsync 已移入 spawn_blocking,且删除前释放 jobs 锁。
  • SSE/前端:新增 ota 主题,admin_topic("ota") 只读内存缓存;events.ts/types/api.ts/docs/admin-ui.md 集合键已同步。

CI 与本地验证

  • head 3621fe8 上两个 check run(check (stable, x86_64-unknown-linux-gnu))均为 success(2026-09-29T07:53Z 完成)。该 workflow 覆盖 cargo fmt --all -- --check、clippy、build、cargo test、pnpm test、pnpm build、Playwright e2e 与 cargo publish --workspace --dry-run --locked,即本 PR 的 Rust 与 web UI 变更均已通过 CI 验证。
  • 本次 review 环境缺少 cargo/rustc 与 pnpm(cargo: not found、pnpm: not found,仅有 Node 24 且无法联网安装依赖),因此本地未能复现 fmt/clippy/test;未发现由本 PR 引起的 CI 失败。
  • PR 作者声明的隔离联调(ostool-server/scripts/test-axloader-local.py:真实 QEMU + 真实 ostool-server + 真实 FAT 磁盘)需要 KVM、OVMF 与 TGOS 产物,本环境无法复现,属未独立验证部分。

既往评审意见核对

此前 19 条 review comment 中的可执行意见已在 head 上逐条核对,均已在 b5b3f2e / 3621fe8 解决:任意非终态任务可取消并清除观察记录;boot_arch 省略按 x86_64 处理且显式非 x86_64 仍拒绝,管理页仅显示受支持面板;匹配的 server 待试槽在活动 Session 下也可确认;镜像 I/O 移入 spawn_blocking、元数据启动时缓存、统一锁序;同阶段回报幂等;Content-Length: 0 返回 400、超 32 MiB 返回 413;删除旧启动事务 404 视为已消失;v5 投递失败计数 3 次后转 failed;webui 同一文件可重复上传。这些意见技术上均合理,当前 head 已不再存在对应问题。

遗留问题与测试缺口

无阻塞问题。本次提出 3 条 advisory(见行内评论):活动 Session 内按广播重复整批重推启动事务并回退会话 loader 状态(device.rs:162);未做 MAC/绑定校验即按广播来源与 http_port 发起 HTTP 调用(device.rs:33);jobs.json/devices.json 解析失败会阻止服务端启动(ota.rs:101)。另有两点残余风险/测试缺口:v5 广播在真实二层网络的可达性与反向路由(PR 已声明为上线前单独验收),以及 reconcile 任务异常结束时 in_progress MAC 集合的释放(当前仅在任务正常返回时移除,最长受 300 s HTTP 超时约束)。

同 PR 检索:repo:drivercraft/ostool type:pr axloader OTA 与 repo:drivercraft/ostool type:pr loader-images 均只命中本 PR #198,未发现重叠或重复 PR。

结论:CI 通过、协议与持久状态契约向后兼容、既往评审项已闭环,仅剩上述非阻塞加固建议,可以合并。

Powered by deepseek-flash

peer.ip()
);
}
if runtime.lease_state != BoardLeaseState::Using {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

板卡处于 Using 且已发布启动命令时,每次收到 v5 广播都会重推整套启动事务:本函数只检查 runtime.lease_state 与 session.boot_command(),完全没有使用起始 GET /api/v1/status 已经拿到的 observed.boot(boot_id/phase/kernel_received)。设备在接收过程中会继续广播(docs/axloader-network-control.md §2.1 已说明),而 loader::serve_udp_discovery 只在单次 reconcile 进行中对同一 MAC 去重,因此上一次上传一结束就会开始下一次完整上传:重复 POST /api/v1/boot/jobs + PUT kernel + PUT initramfs + start,内核与归档各可达 256 MiB(MAX_HTTP_BOOT_INITRAMFS_BYTES),单次多余流量约 512 MiB。同时 push_boot() 在上传前调用 session.update_loader_status(..., LoaderStatusPhase::Accepted),而 SessionState::update_loader_status 只校验 boot_id 并直接覆盖 loader.status,同一 boot_id 的会话状态会从 ready_to_handoff 回退到 accepted,GET /api/v1/sessions/{id}/loader-status 的观察者与 ostool HTTP Boot runner 日志都会看到这次回退(runner 只在 failed 时结束,因此只是噪声而非失败)。建议在推送前判断设备是否已持有本次事务:observed.boot.boot_id == command.boot_id 且阶段已完成文件接收/交接(如 ready_to_handoff)时直接跳过,只在启动代次变化(复位)、设备无事务或上报失败阶段时重推,并在假设备测试中覆盖“同一代次连续两次广播只上传一次文件”。

"unsupported device protocol"
);
ensure!(announcement.http_port > 0, "missing device HTTP port");
let device_addr = SocketAddr::new(peer.ip(), announcement.http_port);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里用 peer.ip() 与广播自带的 http_port 拼出设备地址后,立刻对任意来源执行 GET {endpoint}/api/v1/status,而 MAC/板卡绑定校验(board_id_for_mac,第 47-62 行)发生在其后,协议本身也没有认证(docs/axloader-network-control.md §1.1 已写明限定可信隔离实验网)。因此同一二层网络中任意主机(UDP 源地址可伪造、http_port 完全可控)都能让 ostool-server 向任意 IP:端口发起请求并解析响应;若再伪造出已绑定板卡的 MAC,服务端还会把内核/initramfs/EFI 镜像 POST/PUT 到该地址,等于一个未认证的盲 SSRF 通道与镜像外发路径。建议在发起任何 HTTP 调用之前先按 announcement.mac_address 找到未禁用且 boot.kind == httpboot 的绑定板卡,无绑定则直接返回;也可考虑把设备端口固定为协议常量 2999,如确实需要非默认端口(本地脚本会把它改写成 hostfwd 端口)则用显式配置放行。若这是有意保留的行为,请在文档中明确写出该信任边界。

Comment thread ostool-server/src/ota.rs
pub fn open(data_dir: &Path) -> anyhow::Result<Self> {
let root = data_dir.join("loader-ota");
fs::create_dir_all(root.join("images"))?;
let jobs = match fs::read(root.join("jobs.json")) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jobs.json 或 devices.json 解析失败时 OtaStore::open 直接返回错误,而 build_app_state 里的 OtaStore::open(&config.data_dir)?(ostool-server/src/state.rs)会让整个服务端启动失败。这与同一文件里镜像元数据的容错处理不一致(read_images 只 log::warn! 并跳过不一致条目,PR 说明也写了“异常元数据隔离”),也与板卡配置的既有做法不一致(不兼容板卡 TOML 会被移入 quarantine/ 后继续启动,见 board_store/fs.rs)。结果是运维手工编辑或磁盘损坏导致的一个状态文件异常,会让串口、板卡管理和 SSE 管理台全部无法启动,只能人工删文件恢复。建议与镜像/板卡保持一致:解析失败时记录告警并把该文件(或整个 loader-ota 目录)重命名隔离,然后以空任务表继续启动;并补一个“启动时 jobs.json 已损坏仍能启动”的单元测试。

@ZR233
ZR233 merged commit b8065a5 into main Sep 29, 2026
2 checks passed
@ZR233
ZR233 deleted the codex/axloader-ota-v4 branch September 29, 2026 08:08
This was referenced Sep 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant