You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

134 lines
8.5 KiB

# 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 수정이며 회귀가 아니다.