들어가며

Fail0verflow 에서 Tegra X1 부트롬의 unpatchable 0-day 를 발표하며, PoC 로 닌텐도 스위치용 리눅스 포트를 함께 발표했습니다. 이 포트는 상당히 많은 것들이 동작했지만, 몇몇 삐걱이는 부분들이 존재했고, 이 중 심각했던 것이 “battery desync”1 버그였습니다.

해당 버그의 증상은, 리눅스를 부팅한 스위치에서 게임을 할 때, 대략 46% 부근에서 갑자기 시스템이 꺼지고, 게임을 하지 않을 때도 26% 부근에서 시스템이 꺼지는 증상이 나타나는 것입니다.

이 증상은 배터리의 전력이 모자랄 때, 배터리가 충분한 전류를 공급하지 못해 전압 드롭이 나타날 때 흔히 일어나는 증상과 굉장히 유사합니다. 이 지식과, 닌텐도 스위치용 Linux device tree, 그리고 해당 버그를 해결하는 방법은, 시스템 전체 전원을 완전히 끊었다가 올리는 것 밖에 없다는 정보를 기반으로, 다음과 같은 추론을 할 수 있습니다

  1. 닌텐도 스위치의 전원 계통에는 다음 칩들이 존재한다
  2. 시스템 전체 전원 사이클은 일반적인 방법으로는 끌 수 없는 해당 칩들을 껐다 켤 수 있다.
  3. 해당 칩들을 껐다 켜는 행위는 해당 칩의 레지스터 값을 초기화 시킨다.
  4. 이는 리눅스가 변경하는 해당 칩들의 레지스터 값이 Horizon OS 가 원하는 값과 달라서 일어나는 증상일 것이다
  5. 해당 증상이 일어난 상태의 레지스터 상태와 해당 증상이 일어나지 않는 상태의 레지스터 값을 비교하면 해당 증상을 고칠 실마리를 얻을 수 있을 것이다



NX-MemXPlorer – 닌텐도 스위치 로우 레벨 탐험 도구

이에, 이 추론을 바탕으로, 문제 해결에 필요할 데이터를 모으기 위해, 필요한 도구를 만들었습니다.

손쉬운 출발을 위해, 스위치 시스템의 드라이버들이 구현되어 있는 Memloader 를 기반으로, 옛날에 텀 프로젝트를 하면서 구현했던 대화형 초 날림 디버그 셸 코드를 이용해 디버깅 도구를 구현하였습니다. 오랜 역사와 전통을 자랑하는 peek 과 poke 를 컨셉으로, 주변기기의 레지스터와 테그라 칩 자체의 메모리를 읽고 쓸 수 있는 명령들을 만들고, 편의를 위해 몇가지 디바이스의 레지스터 덤프 기능을 가진 도구입니다.

명령 중, bq24193_dump 와 max17050_dump 명령은, 해당 칩의 읽기 가능한 모든 레지스터를 출력하는 명령입니다

이를 이용해 우선 BQ24190 의 레지스터부터 비교해 보도록 하겠습니다

BQ24193 덤프와 분석

MemXPlorer의 bq24193_dump 명령을 이용해 Horizon OS 부팅 직후와 리눅스 부팅 후를 비교해봅니다.

레지스터 덤프를 뜬 뒤, 리눅스 이후와 HOS 이후를 분석해보면 별 차이가 보이지 않습니다

$ diff batt-horizon.log batt-linux.log 
2,3c2,3
< 0x00 = 0x02
< 0x01 = 0x10
---
> 0x00 = 0x32
> 0x01 = 0x15
5c5
< 0x03 = 0x00
---
> 0x03 = 0x31
7c7
< 0x05 = 0x82
---
> 0x05 = 0x8a

MAX17050 덤프와 분석

마찬가지로, MemXPlorer 의 max17050_dump 명령을 이용해, HorizonOS 이후와 리눅스 부팅 이후의 레지스터 상태를 비교해 봅니다.

$ diff fuel-horizon-after-linux-after-horizon.log fuel-horizon.log 
6c6
< 0x03 = 0x6563
---
> 0x03 = 0xff00
8,18c8,18
< 0x05 = 0x2571
< 0x06 = 0x6400
< 0x07 = 0x66a3
< 0x08 = 0x2084
< 0x09 = 0xd1b8
< 0x0a = 0x0017
< 0x0b = 0x00dc
< 0x0d = 0x6480
< 0x0e = 0x6480
< 0x0f = 0x25a1
< 0x10 = 0x2571
---
> 0x05 = 0x258d
> 0x06 = 0x63f3
> 0x07 = 0x6707
> 0x08 = 0x211e
> 0x09 = 0xd194
> 0x0a = 0x000b
> 0x0b = 0xffba
> 0x0d = 0x64be
> 0x0e = 0x646d
> 0x0f = 0x25bc
> 0x10 = 0x2592
22c22
< 0x16 = 0x2063
---
> 0x16 = 0x2118
25c25
< 0x19 = 0xd0f6
---
> 0x19 = 0xd101
31c31
< 0x1f = 0x2573
---
> 0x1f = 0x258a
37c37
< 0x27 = 0x75fa
---
> 0x27 = 0x74b2
53c53
< 0x3e = 0xe220
---
> 0x3e = 0xc139
58c58
< 0x4d = 0x122f
---
> 0x4d = 0x124b
107,108c107,108
< 0xfb = 0xd16e
< 0xff = 0x6498
---
> 0xfb = 0xd19f
> 0xff = 0x64eb

이 차이 중 유의미해 보이는 부분은 0x03 배터리 퍼센트 경고 정도입니다. 하지만, 이를 Horizon OS 가 원하는데로 0xff00 으로 복구하는 것은 해당 이슈를 해결하는데 도움이 되지 않았습니다.

이제 남은 것은 MAX77620 입니다. 하지만 이제부터는 약간의 문제가 생깁니다.

MAX77620 레지스터 분석

MAX77620 은 Nvidia Jetson TX1 에 탑재되었던 GPIO 겸 RTC 겸 PMIC 입니다. 문제가 있다면, 이 칩에 대한 정보가 Maxim Integrated 공식 홈페이지에도 존재하지 않고, 오직 Jetson TX1 을 통해 이 칩이 존재한다만 알려져 있다는 것이죠.

해당 칩의 리눅스 드라이버 컨트리뷰터 또한 Nvidia 이고, Nvidia 개발자 포럼의 포스트에 따르면, 해당 칩에 대한 정보는 NDA 하에 제공되고 있다고 합니다.

이는 해당 칩을 디버깅할 때 참고 가능한 자료가 오직 리눅스 드라이버 코드 뿐이라는 문제를 유발합니다. 게다가 분석 대상 칩이 PMIC 인 것은 문제를 더 까다롭게 만듭니다. 잘못된 액세스 명령 하나가 닌텐도 스위치를 바로 튀겨버릴 수 있는 상황이니까요.

이 상황에서 무엇을 어떻게 액세스해야 안전한지를 알아내는 것은, 이미 구현된 드라이버가 어떻게 장치를 액세스하는것을 보는 것입니다.

이를 위해 리눅스 I2C 시스템에 다음 패치를 이용해서 디버그 루틴을 삽입합니다

diff --git a/drivers/i2c/i2c-core-base.c b/drivers/i2c/i2c-core-base.c
index 1ba40bb2b966..626ca9a8b11e 100644
--- a/drivers/i2c/i2c-core-base.c
+++ b/drivers/i2c/i2c-core-base.c
@@ -1971,6 +1971,17 @@ int i2c_transfer_buffer_flags(const struct i2c_client *client, char *buf,
                .len = count,
                .buf = buf,
        };
+       int i;
+       char foo[512];
+
+       if (strcmp(client->name, "max77620") == 0 || strcmp(client->name, "max77621") == 0) {
+               snprintf(foo, 512, "I2C XFER: device = %s, flags = 0x%02x, data = [", client->name, msg.flags);
+               for (i = 0; i < msg.len; i++) {
+                       snprintf(foo+strlen(foo), 512, "0x%02x, ", msg.buf[i]);
+               }
+               printk("%s]\n", foo);
+               dump_stack();
+       }
 
        ret = i2c_transfer(client->adapter, &msg, 1);
 

그 뒤, 커널 메시지 로그를 분석해 어떤 레지스터가 어떤 값으로 쓰여지는지를 분석합니다.

그 결과로, 다음과 같은 레지스터의 최종 상태를 얻었습니다

[    1.544424] I2C XFER: device = max77620, flags = 0x00, data = [0x56, 0x22, ] // Differ on pure HOS - 0x23
[    1.939605] I2C XFER: device = max77620, flags = 0x00, data = [0x1d, 0x70, ] // Differ on pure HOS - 0x40
[    2.138520] I2C XFER: device = max77620, flags = 0x00, data = [0x1e, 0x70, ] // Differ on pure HOS - 0x40
[    2.337222] I2C XFER: device = max77620, flags = 0x00, data = [0x1f, 0x70, ] // Differ on pure HOS - 0x40
[    2.536109] I2C XFER: device = max77620, flags = 0x00, data = [0x20, 0x70, ] // Differ on pure HOS - 0x40
[    2.741083] I2C XFER: device = max77620, flags = 0x00, data = [0x25, 0xca, ] // Differ on pure HOS - 0x0a
[    2.945738] I2C XFER: device = max77620, flags = 0x00, data = [0x29, 0xee, ] // Differ on pure HOS - 0x2e
[    3.348048] I2C XFER: device = max77620, flags = 0x00, data = [0x2b, 0xc4, ] // Differ on pure HOS - 0x10
[    3.741160] I2C XFER: device = max77620, flags = 0x00, data = [0x2d, 0xd4, ] // Differ on pure HOS - 0x2e
[    4.134812] I2C XFER: device = max77620, flags = 0x00, data = [0x2f, 0xea, ] // Differ on pure HOS - 0x28
[    4.323549] I2C XFER: device = max77620, flags = 0x00, data = [0x31, 0xc5, ] // Differ on pure HOS - 0x05
[    4.522174] I2C XFER: device = max77620, flags = 0x00, data = [0x33, 0xc5, ] // Differ on pure HOS - 0x05
[    4.721309] I2C XFER: device = max77620, flags = 0x00, data = [0x00, 0xff, ] // Differ on pure HOS - 0x92 * DIFFER FROM HOS AFTER LINUX - 0xFF
[    5.310380] I2C XFER: device = max77620, flags = 0x00, data = [0x0d, 0xe7, ] // Differ on pure HOS - 0x75
[    5.924899] I2C XFER: device = max77620, flags = 0x00, data = [0x0e, 0x08, ] // Differ on pure HOS - 0x00
[    6.276275] I2C XFER: device = max77620, flags = 0x00, data = [0x27, 0xd4, ] // ?? Why 0xf2 in memxplorer?
[    7.397757] I2C XFER: device = max77620, flags = 0x00, data = [0x3c, 0x09, ] // Differ on pure HOS - 0x02

여기에서 가장 눈에 띄는 부분은, 레지스터 주소 0x00 입니다. 이 레지스터를 0x92 로 덮어씌우고 코로그마크2018젤다를 돌려서 배터리를 테스트 해 봅니다.

이번은 성공입니다. 이제 원인을 분석해봅시다.

해당 레지스터의 이름은 CNFGGLBL1 이고, 이 레지스터의 비트들의 이름도 꽤 의미심장해보입니다. LBRSTEN이라고요? Low Battery ReSeT ENable 인 걸까요?

저전압 관련 레지스터로 추정되는 부분들이고, Horizon OS 는 해당 비트들을 건드리지 않고 있습니다.

네. 결국 “Battery desync” 버그라는 이름은, 배터리 캘리브레이션과는 완전히 관계 없는 훌룡한 훈제 청어였군요 (짝짝짝)

원인 분석

해당 값을 0xFF 로 설정하는 부분의 스택 추적을 살펴봅시다

[    4.721309] I2C XFER: device = max77620, flags = 0x00, data = [0x00, 0xff, ]
[    4.728382] CPU: 2 PID: 1 Comm: swapper/0 Not tainted 4.17.0-00059-g87bd3c37c1cd-dirty #16
[    4.736641] Hardware name: Nintendo Switch (DT)
[    4.741165] Call trace:
[    4.743614]  dump_backtrace+0x0/0x1b0
[    4.747274]  show_stack+0x14/0x20
[    4.750589]  dump_stack+0x9c/0xbc
[    4.753903]  i2c_transfer_buffer_flags+0x120/0x178
[    4.758691]  regmap_i2c_write+0x1c/0x50
[    4.762526]  _regmap_raw_write_impl+0x5f4/0x750
[    4.767053]  _regmap_bus_raw_write+0x60/0x78
[    4.771321]  _regmap_write+0x58/0xa8
[    4.774893]  _regmap_update_bits+0xf0/0x108
[    4.779073]  regmap_update_bits_base+0x60/0x90
[    4.783513]  regmap_irq_update_bits.isra.1+0x44/0x50
[    4.788472]  regmap_add_irq_chip+0x4c4/0x8a8
[    4.792739]  devm_regmap_add_irq_chip+0x8c/0x100
[    4.797354]  max77620_gpio_probe+0x10c/0x1a8
[    4.801620]  platform_drv_probe+0x58/0xb8
[    4.805628]  driver_probe_device+0x298/0x468
[    4.809895]  __device_attach_driver+0x88/0x140
[    4.814335]  bus_for_each_drv+0x78/0xc8
[    4.818168]  __device_attach+0xd4/0x150
[    4.822001]  device_initial_probe+0x10/0x18
[    4.826180]  bus_probe_device+0x90/0x98
[    4.830012]  device_add+0x3ec/0x5f8
[    4.833497]  platform_device_add+0x110/0x278
[    4.837766]  mfd_add_device+0x2a8/0x2f8
[    4.841598]  mfd_add_devices+0xac/0x148
[    4.845431]  devm_mfd_add_devices+0x78/0xd8
[    4.849610]  max77620_probe+0x49c/0x6d8
[    4.853444]  i2c_device_probe+0x264/0x2c8
[    4.857450]  driver_probe_device+0x298/0x468
[    4.861717]  __driver_attach+0x114/0x118
[    4.865637]  bus_for_each_dev+0x70/0xc0
[    4.869469]  driver_attach+0x20/0x28
[    4.873042]  bus_add_driver+0x248/0x278
[    4.876872]  driver_register+0x60/0xf8
[    4.880617]  i2c_register_driver+0x44/0xa0
[    4.884713]  max77620_driver_init+0x18/0x20
[    4.888891]  do_one_initcall+0x70/0x144
[    4.892724]  kernel_init_freeable+0x180/0x21c
[    4.897078]  kernel_init+0x10/0x104
[    4.900565]  ret_from_fork+0x10/0x18

이상함이 느껴집니다. GPIO 드라이버가 해당 코드를 건드리고 있습니다. 그리고 해당 코드를 살펴본 결과…

인터럽트 마스킹 코드가 0x00 레지스터에 0xFF 마스크를 기록하고 있었습니다.

제출된 패치를 찾아본 결과, 원래는 GPIO 드라이버가 저전력 배터리 설정까지 건드렸던것으로 보입니다. (아니 도대체 왜죠?)

덤으로, MAX77620 칩은 인터럽트 마스킹을 지원하지 않는 칩이지만, Linux regmap 시스템은 인터럽트 마스킹을 지원하지 않는 장치를 지원하지 않는 상태였습니다. 이에 인터럽트 마스킹 코드는 초기화되지 않은 기본값 0x00 레지스터에 인터럽트 마스킹 플래그인 0xFF 를 써 버린 것이죠.

엔비디아 또한 이 문제를 알고, 해당 문제를 문제가 있는 방법(인터럽트 마스크 레지스터가 0x00 인 경우) 으로 고치려고 했었습니다. 이 패치 또한 메인라이닝 되지 못하였고, 이 결과는 후에 돌아와서 닌텐도 스위치 리눅스의 배터리 버그로 우리들의 뒤통수를 후드려 까게 됩니다.

결국, 해당 문제는 현재로서는 깔끔한 해답이 없는 채로, 덕 테이프 패치로만 해결할 수 있는 상황으로 판정되었습니다.

CTCaer Hekate IPL 에 레지스터 Fix 가 포함되었습니다. 이미 해당 증상을 겪고 계신 분은 CTCaer Hekate 를 이용해 desync 증상을 고치면 되겠습니다.

결론


Nvidia, fuck you.

  1. 스포일러: 사실 이 “battery desync” 라는 이름은 강력한 훈제 청어입니다