# Firmware Boot Hardening — dpworld-network-apply-hardened.sh ## 근본 원인 (PMU 워치독 재부팅 루프, 2026-06-15) `.56` (telechips-tcc8030-main)이 하드웨어 재부팅 루프에 진입했다. 타임라인: 1. **트리거:** WiFi 국가 코드 변경이 DB에 대기(pending) 상태였다. 2. **부팅 경로:** `dpworld-network-seed.service`가 펌웨어 내장 `/usr/bin/dpworld-network-apply.sh --boot`를 실행했다. 3. **행(Hang):** 원본 펌웨어 스크립트가 `modprobe -r wlan`을 호출해 새 국가 코드로 WiFi 모듈을 라이브 재로드하려 했다. QCA6490 (cnss_pci / Qualcomm WLAN.HSP.1.1) 모듈에서 이 호출은 **무한 정지** 상태가 된다 — PCIe 링크 해제가 완료되지 않기 때문이다. 4. **리셋:** Telechips PMU 하드웨어 워치독이 20초 후 발동 → 하드 리셋 → 2단계부터 루프 반복. 5. **해결:** 하드닝 스크립트를 라이브 디바이스에 설치(~07:44). 이후 디바이스 안정 유지 중. ## 하드닝 스크립트 `deploy/dpworld-network-apply-hardened.sh`는 펌웨어 내장 `dpworld-network-apply.sh`를 POSIX sh로 독립 재작성한 것이다. 펌웨어 원본의 문서화된 6가지 문제(P1–P6, CC)를 수정하며, 부팅 하드닝 수정은 **P2**에 해당한다. ### P2: 부팅 시 modprobe -r 제거 | | 펌웨어 원본 | 하드닝 버전 | |---|---|---| | `--boot` + 국가 코드 변경 | `reload_wifi_modules()` 호출 → `modprobe -r wlan` → **HANG** | `modprobe.d` 기록 + `reboot-required` 마커 설정; **modprobe 호출 없음** | | `--country-now` (전문가 모드) | 동일한 `reload_wifi_modules()` | 호환성을 위해 인자 수용하나 **재부팅 지연 처리** — 의도를 로그에 기록하고 `reboot-required` 유지; **라이브 모듈 언로드 없음** (v1.11.6: 행(hang)이 드라이버 언로드 경로 자체에서 발생하므로 가드된 전문가 재로드도 미탑재) | | 국가 코드 검증 | `wpa_cli get country` (wpa 설정 렌더링 기반 — 거짓 양성 가능) | `live_country()`: sysfs `/sys/module/wlan/parameters/country_code` → `iw reg get` 폴백 | ### 세 가지 호출 모드 ``` dpworld-network-apply-hardened.sh # 온디맨드 (apply.service 또는 수동) dpworld-network-apply-hardened.sh --boot # 부팅 시드 (라이브 터치 없음, /run 만 채움) dpworld-network-apply-hardened.sh --country-now # 호환 별칭 — 재부팅 지연 처리 (라이브 모듈 재로드 없음) ``` ### reboot-required 마커 - **설정:** 라디오의 실효 국가 코드(sysfs/iw reg)가 원하는 국가 코드와 다를 때 `${STATE_DIR}/reboot-required` (`/opt/dpworld-network/reboot-required`)에 생성. - **해제:** `live_country()` == 원하는 국가 코드일 때 — 즉, 업데이트된 `modprobe.d`가 모듈 로드 시점에 적용된 재부팅 이후. - Web Configurator는 이 마커를 읽어 대시보드에 "Reboot required"를 표시한다. ### 빈 네트워크 부트스트랩 (v1.11.6) `--boot` 실행 시(및 온디맨드 시), `/home/root/network`가 비어 있으면 `/run/systemd/network` 동기화 **전에** `/etc/dpworld/network-defaults`에서 시드를 채운다 — 펌웨어의 팩토리/복구 부트스트랩을 복원하는 동작이다. 디렉터리에 파일이 하나라도 있으면 절대 덮어쓰지 않는다. ## v1.6 Country-Deferred 설계와의 관계 v1.6에서 Python `apply_engine`이 도입됐으며, 이미 국가 코드 변경을 `modprobe.d`로 지연 처리하고 동일 마커를 설정하도록 설계되어 있다. 하드닝 스크립트는 **펌웨어 레이어**에서 동일하게 동작하는 계층을 추가한다 — Python 엔진이 실행 중이지 않은 경우(예: 새 플래시 직후나 서비스 크래시 시)에도 펌웨어 자체 부팅 서비스가 modprobe hang을 유발할 수 없게 된다. 두 레이어 모두 동일한 `REBOOT_MARK` 경로와 `MODPROBE_CONF`에 기록하므로 멱등(idempotent)적이며 조합해서 사용할 수 있다. 하드닝 스크립트는 Python 엔진의 `country-now` 마커(`/run/dpworld-network/country-now`)를 확인하여 존재하면 `COUNTRY_NOW=1`로 승격한다 — 엔진 호환성을 유지하기 위함이다. v1.11.6부터는 `COUNTRY_NOW=1`이어도 하드닝 스크립트가 WiFi 모듈을 라이브 언로드하지 **않는다**; 마커를 소비하고, 지연 처리를 로그에 남기며, `reboot-required` 마커를 유지한다. ## 설치 / 배포 `scripts/deploy.ps1`은 배포 시마다 세 가지 아티팩트를 설치한다 (해시 비교 방식, 멱등, 영속적 `/lib/systemd/system`): | 로컬 경로 | 디바이스 경로 | |---|---| | `deploy/dpworld-network-apply-hardened.sh` | `/usr/bin/dpworld-network-apply-hardened.sh` (chmod +x) | | `deploy/dpworld-network-apply.service.d/20-hardened.conf` | `/lib/systemd/system/dpworld-network-apply.service.d/20-hardened.conf` | | `deploy/dpworld-network-seed.service.d/20-hardened.conf` | `/lib/systemd/system/dpworld-network-seed.service.d/20-hardened.conf` | 변경 후에는 `systemctl daemon-reload`가 호출된다. 세 경로 모두 영속적 읽기-쓰기 오버레이(`/lib`) 위에 있어 재부팅 및 유저스페이스 펌웨어 재플래시 이후에도 유지된다. 펌웨어 원본 `/usr/bin/dpworld-network-apply.sh`는 **수정하지 않는다**. ## 테스트 `tests/test_firmware_apply_hardened.py` (23개 테스트, 표준 라이브러리만 사용, 디바이스 불필요): - 아티팩트가 예상 경로에 존재하는지 확인. - 스크립트 내 **`modprobe -r` 구문이 전혀 없음** (가드된 전문가 경로 포함). - `safe_module_reload` / `reload_wifi_modules` 헬퍼가 잔존하지 않음. - 빈 네트워크 부트스트랩(`seed_defaults_if_empty`)이 `/run` 동기화 전에 `/etc/dpworld/network-defaults`에서 시드를 채우는지 확인. - `reboot-required` 마커가 올바르게 설정 및 해제되는지 확인. - 마커 판단이 `live_country()`를 사용하는지 확인 (`wpa_cli get country` 아님). - 두 드롭인(drop-in) 모두 `ExecStart`가 하드닝 스크립트를 가리키는지 확인. - `deploy.ps1`이 스크립트, 두 드롭인을 설치하고 `daemon-reload`를 호출하는지 확인. - 세 아티팩트 모두 LF 줄 끝 (CRLF 없음). ## 펌웨어 베이킹 (BSP) — 원본 교체 방법 위 "설치 / 배포" 방식은 Web Configurator가 사용하는 **라이브 디바이스 오버라이드** 방식이다: `dpworld-network-apply-hardened.sh`를 별도 파일로 설치하고, 두 개의 systemd 드롭인을 함께 설치하되, 펌웨어 원본 `/usr/bin/dpworld-network-apply.sh`는 손대지 않는다 (실행 중인 디바이스는 펌웨어에 베이킹된 원본을 덮어쓸 수 없으며, 재플래시하면 원본이 복원된다). **펌웨어 이미지(BSP 베이킹) 시에는 대신 다음과 같이 진행한다:** 1. `deploy/dpworld-network-apply-hardened.sh`의 내용을 `/usr/bin/dpworld-network-apply.sh`에 설치한다 — 즉, **원본 이름을 유지한 채 펌웨어 원본 스크립트를 교체**한다 (root:root, 0755, LF, POSIX /bin/sh). 하드닝 스크립트의 헤더에 이미 이 최종 이름이 명시되어 있다. 2. 두 오버라이드 드롭인(`dpworld-network-apply.service.d/20-hardened.conf`와 `dpworld-network-seed.service.d/20-hardened.conf`)은 **베이킹하지 않는다**. 이 드롭인들은 라이브 디바이스 오버라이드 전용이며 `…-hardened.sh`를 가리키는데, 베이킹 이미지에는 해당 파일이 존재하지 않는다. 펌웨어 자체의 `dpworld-network-seed.service` / `dpworld-network-apply.service`가 이미 `/usr/bin/dpworld-network-apply.sh`를 호출하므로 하드닝 동작이 자동으로 적용된다. 3. 하드닝 스크립트는 원본을 완전히 독립적으로 재작성한 것으로 (모든 호출 모드 포함: 인자 없음 / `--boot` / `--country-now`), 원본을 내부적으로 호출하지 않는다. 베이킹 전 펌웨어 원본과 diff를 수행해 펌웨어 고유 책임 사항이 누락되지 않았는지 확인할 것. **예상되는 동작 변경:** 하드닝 스크립트는 QCA6490 / cnss_pci 모듈에서 PMU 워치독 재부팅 루프를 유발하는 라이브 `modprobe -r wlan` (국가 코드 라이브 재로드)을 제거한다 — **이전의 `--country-now` 전문가 경로 포함** (v1.11.6). 국가 코드 변경은 항상 **재부팅 지연 처리**된다: 새 국가 코드가 `modprobe.d`에 기록되고 `reboot-required` 마커가 설정되며, 다음 재부팅 시에 적용된다. 이는 P2 수정이며 회귀가 아니다.