[php-src] Issue #23979: zend_get_typ e_by_const 의 ZEND_UNREACHABLE 위반 (배 열 오프셋 에러 경로)
| From: | rlaaudrb1104 | Date: | Tue, 29 Sep 2026 08:28:08 +0000 |
| Subject: | [php-src] Issue #23979: zend_get_typ e_by_const 의 ZEND_UNREACHABLE 위반 (배 열 오프셋 에러 경로) | ||
| Groups: | php.bugs | ||
| Request: | Send a blank email to php-bugs+get-252847@lists.php.net to get a copy of this message | ||
Issue: https://github.com/php/php-src/issues/23979
Author: rlaaudrb1104
### Description
## 개요
| | |
|---|---|
| 대상 |
php @ 38956a3 |
| 결함 유형 | ZEND_UNREACHABLE(=assert(0)) 위반 → SIGABRT (**디버그 빌드
전용**) |
| 크래시 지점 | zend_get_type_by_const@Zend/zend_API.c:126 — default:
ZEND_UNREACHABLE(); (← zend_zval_value_name ←
zend_fetch_dimension_address_read@zend_execute.c:3206) |
| 재현 | 재현됨 (php --enable-debug + ASan, 동일 커밋) |
| 최신 릴리스 | 릴리스는 NDEBUG 라 UNREACHABLE 이 컴파일아웃됨 (아래 영향
참조) |
| PoC | pocs/04_3e4b59e314ffb841.php (256 bytes) |
## 전체 출력 (디버그 빌드 재현)
--enable-debug + ASan 로 빌드한 php(CLI)로 PoC 를 실행하면 다음과 같이
종료한다.
```
Deprecated: Non-canonical cast (integer) is deprecated ...
Warning: Octal escape sequence overflow \400 is greater than \377 ...
Deprecated: The backtick (`) operator is deprecated ...
Warning: String offset cast occurred ...
php: Zend/zend_API.c:126: const char *zend_get_type_by_const(int): Assertion `0' failed.
==ABORTING (SIGABRT)
```
## 원인
$expr[$offset] 형태의 배열 오프셋 읽기에서 컨테이너가
배열/문자열/객체가 아니면, 읽기 경로는 zend_execute.c:3206 의 에러
리포팅 분기로 들어가 zend_zval_value_name(container) 를 호출한다. 이
함수는 다시 zend_get_type_by_const(Z_TYPE_P(container)) 를 호출하는데, PoC
가 만든 컨테이너의 타입 태그가 switch 에 열거되지 않은 값이라 default:
ZEND_UNREACHABLE()(zend_API.c:126)에 도달한다.
**호출 지점 (Zend/zend_execute.c:3206)** — 컨테이너가 배열이 아닐
때의 경고 경로:
```c
3202 zend_error(E_WARNING, "Cannot use %s as array",
zend_zval_type_name(container));
3203 }
3204 if (!is_list && type != BP_VAR_IS) {
3205 zend_error(E_WARNING, "Trying to access array offset on %s",
>> 3206 zend_zval_value_name(container));
3207 }
3208 ZVAL_NULL(result);
3209 }
3210 }
```
**zend_zval_value_name (Zend/zend_API.c:147)** — 타입 이름을 얻기
위해 zend_get_type_by_const 로 넘긴다:
```c
143 } else if (Z_TYPE_P(arg) == IS_TRUE) {
144 return "true";
145 }
146
>> 147 return zend_get_type_by_const(Z_TYPE_P(arg));
148 }
149
150 ZEND_API const char *zend_zval_type_name(const zval *arg)
151 {
```
**zend_get_type_by_const (Zend/zend_API.c:126)** — switch 에 없는
타입은 default: ZEND_UNREACHABLE() 로 떨어진다:
```c
116 case IS_ITERABLE:
117 return "iterable";
118 case IS_ARRAY:
119 return "array";
120 case IS_VOID:
121 return "void";
122 case IS_MIXED:
123 return "mixed";
124 case _IS_NUMBER:
125 return "int|float";
>> 126 default: ZEND_UNREACHABLE();
127 }
128 }
129 /* }}} */
130
131 ZEND_API const char *zend_zval_value_name(const zval *arg)
132 {
133 ZVAL_DEREF(arg);
134
135 if (Z_ISUNDEF_P(arg)) {
136 return "null";
```
- 깨진 불변식: zend_get_type_by_const/zend_zval_value_name 은 입력
zval 의 타입 태그가 switch 가 열거한 값 중 하나라고 전제한다. 이 전제는
호출자(zend_fetch_dimension_address_read)가 항상 정상 타입의 container 만
넘긴다는 상류 계약에 의존한다.
- PoC 는 그 계약을 깨는 container — 정상 타입 태그를 갖지 않는 임시 zval —
을 3206 에러 경로로 흘려보낸다. 따라서 결함은 이름 조회 함수가 아니라,
비정상 타입 container 를 이 경로로 흘려보낸 상류(피연산자 생성·임시 스택
관리)에 있다. 크래시 지점은 증상이고 결함은 상류다.
## 트리거
PoC(pocs/04_3e4b59e314ffb841.php, 256 bytes) 원문:
```php
<?php switch ( sprintf ( true , 'b' ) [ 2.2 ] = "\143" ) { } assert ( $ {
function ( ) { } } [ function ( ) : void { try { $ { '1!' } [ ( integer ) fn & ( )
=>ire ( ( object ) fn & ( ) => re + "ss\n" ) ] ; } finally { r ?-> db ; } } ]
) ; \400\40\ \u ;
```
- 문법적으로는 통과하지만 의미적으로 극도로 비정상적인 중첩
표현식들의 집합이다. 실행 중 다음 경고가 먼저 출력된다: non-canonical cast,
octal overflow \400 > \377, backtick deprecated, String offset cast
occurred.
- 크래시를 일으키는 핵심 구조는 <임시식>[<오프셋>]
형태의 배열 오프셋 읽기다. VM 핸들러가
ZEND_FETCH_DIM_R_SPEC_TMPVAR_TMP_HANDLER 인 점에서, 컨테이너가 변수가
아니라 임시 계산 결과(TMPVAR)임을 알 수 있다. PoC 안의 후보:
- sprintf(true,'b')[2.2] — 함수 반환값(임시 문자열)에 대한
float 오프셋 읽기 → 로그의 String offset cast occurred 경고와 부합.
- ${ function(){} }[ ... ] / ${'1!'}[ ... ] —
변수-변수(${expr})로 만든 컨테이너에 대한 오프셋 접근.
${closure} 처럼 정상 변수명으로 환원되지 않는 표현이 컨테이너가
된다.
- 오프셋식 자체가 ( integer ) fn & ( ) => ..., ( object ) fn
& ( ) => re + "ss\n" 처럼 캐스트·화살표
함수·참조(&)·미정의 심볼이 뒤엉킨 비정상식이다.
- 이 중첩식들의 평가가 정상 타입 태그를 갖지 않는 임시 zval 을
컨테이너로 만들어 zend_fetch_dimension_address_read 의 에러 경로(3206)로
흘려보내는 것이 트리거다. 어느 부분식이 최종 오염 컨테이너를
만드는지는 gdb 에서 3206 의 container 를 브레이크해 Z_TYPE_P
를 확인하면 확정된다.
## 영향
- 디버그 빌드(--enable-debug): ZEND_UNREACHABLE 이
abort() 이므로 DoS(크래시)다.
- 릴리스 빌드(NDEBUG): ZEND_UNREACHABLE 이 __builtin_unreachable()
로 바뀐다. 옵티마이저가 이 분기를 도달 불가로 가정하므로, 실제로
도달하면 정의되지 않은 동작(잘못된 분기나 메모리 접근)이 될 수 있다.
릴리스에서의 실제 영향은 별도의 릴리스 ASan 빌드로 확인이 필요하다.
- 정리하면, 확정된 것은 디버그 전용 도달 불가 위반(타입 매핑 누락 또는
정규화되지 않은 타입 유입)이다. 릴리스 메모리 취약점 여부는
미확정이지만, ZEND_UNREACHABLE 이 실제로 도달 가능하다는 사실
자체가 상류에 보고할 만한 엔진 불변식 버그다.
## 재현 방법
```bash
git clone https://github.com/php/php-src.git
php-src && cd php-src
git checkout 38956a3 && ./buildconf --force
CC=clang CFLAGS="-g -O1 -fsanitize=address -fno-omit-frame-pointer"
LDFLAGS="-fsanitize=address" \
./configure --disable-all --enable-cli --enable-debug --enable-tokenizer --enable-mbstring
make -j$(nproc)
USE_ZEND_ALLOC=0 ASAN_OPTIONS='symbolize=1:handle_segv=1' sapi/cli/php
pocs/04_3e4b59e314ffb841.php
```
**--enable-debug 는 필수다.** 없으면 UNREACHABLE 이 컴파일아웃되어
재현되지 않는다.
### PHP Version
```plain
PHP 8.6.0-dev (master, commit 38956a3f3eeb9d878d578efa4163b74fa20ebbfb)
```
### Operating System
_No response_