블로그

R8 컴파일러 버그를 신고했더니 10시간 만에 재현 커밋이 올라옴

키보드 앙 릴리스 빌드에서 만난 R8 VerifyError를 신고했고, 메인테이너가 덤프 없이 재현해 R8 main 브랜치에 테스트를 커밋함.

키보드 앙 릴리스 빌드에서 R8 컴파일러 버그를 만나 Google 이슈 트래커에 신고했습니다. 3시간이 안 돼 R8 메인테이너가 우선순위를 P2에서 P1로 올렸고, 10시간이 안 돼 컴파일러 입력 덤프 없이 문제를 재현해 R8 main 브랜치에 재현 테스트를 커밋했습니다.

시작은 R8 설정 정리

입력 쪽은 더 최적화할 부분이 보이지 않아, 개발 초기부터 넓은 keep 규칙(앱 패키지 전체, 모든 클래스의 @Composable 함수)으로 묶어 두었던 R8 난독화를 풀어 내는 작업을 시작했습니다. 그중 모든 클래스의 @Composable 함수를 지키던 규칙을 지우고 R8 full mode로 만든 릴리스 빌드를 설치하자, 앱이 실행되자마자 종료됐습니다.

java.lang.VerifyError: Verifier rejected class e03: void e03.e(te, co1, long, long, long, long,
int, boolean, int, int, java.util.Map, ur0, y03, xz, int, int, int) failed to verify:
void e03.e(...): [0x3CA] copy2 v9<-v6 type=Long (High Half)/Long (High Half)

mapping.txt로 찾아보니 e03은 androidx.compose.material3.TextKt였습니다. 화면에 글자를 그리는 Text가 처음 사용되는 순간 ART 검증기가 클래스를 거부한 것입니다. Android 10 기기에서 adb shell cmd package compile -m verify -f로 APK 전체를 검증해 보니 거부된 메서드는 이것 하나뿐이었습니다.

레지스터 한 칸이 덮이는 순서

R8 9.4.24가 만든 코드를 dexdump로 열어 보니 원인이 보였습니다. long 값은 레지스터 두 칸(v6:v7)을 한 쌍으로 사용합니다.

03c5: move-object v2, v10
03c6: move-wide v6, v4          // v6:v7 <- long A
03c7: move-wide/from16 v5, v74  // v5:v6 <- long B (A의 하위 절반 v6를 덮음)
03c9: move-wide v3, v8
03ca: move-wide v9, v6          // v6:v7을 A로 읽지만 v6는 이미 B의 상위 절반 -> 거부
03cb: move-wide/from16 v7, v76
03cd: invoke-virtual {v13}, ...

여러 값을 한꺼번에 옮기는 과정(parallel move)에서 레지스터 쌍이 겹칠 때 옮기는 순서가 잘못된 것으로 보였습니다. 0x3c7에서 이미 덮인 v6를 0x3ca에서 v6:v7 쌍으로 읽습니다. 이번에는 검증기가 막아 줬지만, 검증을 통과했다면 틀린 값이 들어갔을 순서입니다.

확인한 범위는 다음과 같습니다.

  • R8 9.4.24(AGP 9.4.1 포함), 9.3.16(AGP 9.3.2), 9.5.26(AGP 9.4.1에서 R8만 교체) 모두 같은 메서드, 같은 오류. 9.5.26에서는 위치만 0x3B5로 다름
  • 화웨이 메이트 20 X(Android 10), Oppo Find N5(Android 16) 두 기기 모두 실행 즉시 종료. Android 16에서는 오류가 copy-cat2로 표시되고, compile -m verify가 실패를 기록하지 않아 실행해야만 드러남
  • Compose @Composable 메서드를 지키는 keep 규칙이 없으면 키보드 앙에서는 항상 재현
  • Text(String)·Text(AnnotatedString)만 사용하는 최소 앱(같은 버전, keep 규칙 없음)으로는 재현되지 않음. 앱의 호출 지점을 보고 하는 전체 프로그램 최적화(인자 제거·특수화)에 따라 코드 모양이 정해지는 것으로 보임

빌드 환경은 AGP 9.4.1, Gradle 9.6.1, Kotlin·Compose 컴파일러 플러그인 2.2.10, Compose BOM 2026.02.01(material3 1.4.0), min-api 29, R8 full mode, proguard-android-optimize.txt입니다.

덤프 없이 보낸 신고

R8 팀은 재현을 위해 컴파일러 입력 덤프를 요청하는 경우가 많습니다. 덤프는 만들어 두었지만 키보드 앙 코드가 그대로 들어 있어 공개 이슈에 올릴 수 없었습니다. 그래서 오류 메시지, dexdump, 덮이는 레지스터 순서, 재현되는 버전과 기기, 최소 앱에서는 재현되지 않는다는 점, 우회 방법까지 정리해 올리고, 덤프를 비공개로 전달할 연락처를 알려 달라고 적었습니다.

신고부터 커밋까지

  • 10월 15일 15:21 이슈 트래커에 신고
  • 17:38 메인테이너 첫 답글. 덤프를 비공개로 보내 달라며 받을 주소를 알려 줌
  • 18:02 우선순위 P2 → P1
  • 10월 16일 00:40 덤프를 보내기 전에 메인테이너가 직접 재현 성공
  • 아침 R8 main 브랜치에 재현 테스트가 커밋된 것을 확인: 0640694 Reproduce VerifyError clobbering live wide register pair
R8 저장소 main 브랜치 커밋 목록. 맨 위에 Søren Gjesse의 0640694 Reproduce VerifyError clobbering live wide register pair 커밋이 main 표시와 함께 있음
R8 main 브랜치 커밋 기록. 노란 상자의 0640694가 이 이슈의 재현 테스트 커밋입니다.

신고하고 9시간 19분 만에, 덤프 없이 신고 글의 내용만으로 재현까지 갔다는 점이 가장 기뻤습니다. 아직 수정 커밋이 아니라 재현 테스트가 들어간 단계이며, 진행 상황은 이슈 573664753에서 확인할 수 있습니다.

수정 전까지는

androidx의 @Composable 함수만 최적화에서 빼는 규칙 하나로 우회하고 있습니다. 앱 코드에는 keep 규칙이 없습니다.

-keepclasseswithmembers class androidx.** { @androidx.compose.runtime.Composable <methods>; }
-keepclassmembers class androidx.** { @androidx.compose.runtime.Composable <methods>; }

참고

수정이 들어간 R8 버전이 나오면 이 규칙을 빼고, Android 10 기기에서 앱 전체 검증(cmd package compile -m verify)으로 거부되는 클래스가 없는지 다시 확인할 예정입니다. Android 16 기기에서는 이 검증이 실패를 기록하지 않았기 때문입니다.