- Published on
Clang 22의 MSVC ABI 변경 — vector deleting destructor와 아직 22.1.x에 남아 있는 delete[] 힙 손상
- Authors

- Name
- Youngju Kim
- @fjvbn20031
- 들어가며 — 릴리스 다음 날 올라온 이슈
- MSVC ABI에서 소멸자는 하나가 아니다
- Clang이 11년 동안 못 넣은 것
- Clang 22가 바꾼 두 가지
- 릴리스 이후 — 두 개의 버그, 하나의 백포트
- 왜 링크는 되는데 런타임에 깨지나
- 그래서 지금 무엇을 확인해야 하나
- 마치며
- 참고 자료
들어가며 — 릴리스 다음 날 올라온 이슈
LLVM/Clang 22.1.0이 2026년 2월 24일에 릴리스됐습니다. 그리고 그 다음 날인 2월 25일, 이런 이슈가 올라옵니다 — #183255 "[Clang-Cl] Wrong destructing code generated in Clang 22".
재현 코드는 이보다 더 작을 수 없을 만큼 작습니다. 헤더에 가상 소멸자를 가진 클래스를 선언하고, 소멸자 정의는 별도 .cpp에 두고, 다른 .cpp에서 배열로 할당했다가 지웁니다.
// inc.h
class Class1 {};
class Class2 : public Class1 {
public:
Class2();
virtual ~Class2();
};
// impl.cpp — 생성자/소멸자 정의는 여기에만 있다
Class2::Class2() {}
Class2::~Class2() {}
// main.cpp
int main() {
Class2* p1 = new Class2;
delete p1; // 이건 괜찮다
Class2* p2 = new Class2[1];
delete [] p2; // Debug: RtlValidateHeap assertion / Release: 크래시
return 0;
}
보고자의 말 그대로 옮기면, 커스텀 컴파일러 플래그는 하나도 쓰지 않았고 Clang 21과 20에서는 일어나지 않습니다. 그리고 후속 코멘트에서 범위를 좁혔습니다 — MSVC 타깃(x86과 x86_64 둘 다)에서만 발생하고 Linux도 MinGW도 영향받지 않는다는 것입니다.
무슨 일이 있었나 하면, Clang 22가 MSVC ABI를 타깃할 때의 소멸자 코드 생성을 두 군데 바꿨습니다. 둘 다 "고치는" 변경이었고, 둘 다 ABI 변경이었습니다. 이 글은 그 변경이 무엇이고, 왜 11년이 걸렸고, 왜 링크는 멀쩡히 되는데 런타임에 힙이 깨지는지, 그리고 지금 무엇을 확인해야 하는지를 정리합니다.
MSVC ABI에서 소멸자는 하나가 아니다
먼저 배경입니다. Itanium ABI(리눅스·macOS·MinGW)에 익숙하다면 소멸자가 complete/base/deleting 세 변종으로 나뉜다는 건 아실 겁니다. MSVC ABI도 여러 변종을 두는데, 이름과 규약이 다릅니다. Clang의 MicrosoftMangle.cpp에 맹글링이 주석과 함께 그대로 적혀 있습니다.
// <operator-name> ::= ?_D # vbase destructor
case Dtor_Complete: Out << "?_D"; return;
// <operator-name> ::= ?_G # scalar deleting destructor
case Dtor_Deleting: Out << "?_G"; return;
// <operator-name> ::= ?_E # vector deleting destructor
case Dtor_VectorDeleting:
Out << "?_E";
return;
정리하면 ??_D는 vbase destructor, ??_G는 scalar deleting destructor, ??_E는 vector deleting destructor입니다. 그리고 중요한 건 deleting 소멸자 두 종류가 AST에 없는 숨은 파라미터를 하나 더 받는다는 점입니다. 같은 파일의 주석이 이걸 설명합니다 — 이 추가 인자는 객체의 저장 공간을 해제할지 여부와, 단일 객체를 파괴하는지 객체 배열을 파괴하는지를 가리키며, AST에는 반영되지 않는다고요. 64비트에서 deleting 소멸자의 맹글링이 PEAXI@Z로 끝나는 이유가 이것입니다 — 저 I 자리가 AST에 없는 그 정수 인자입니다. 같은 자리에서 vbase destructor는 XXZ로 끝납니다. clang 내부에서 이 파라미터는 should_call_delete라는 이름에 Context.IntTy 타입으로 만들어집니다.
그 플래그의 비트 의미는 MicrosoftCXXABI.cpp의 호출부에 그대로 드러나 있습니다.
llvm::Value *ImplicitParam =
CGF.Builder.getInt32((IsDeleting ? 1 : 0) | (IsGlobalDelete ? 4 : 0) |
(IsArrayDelete ? 2 : 0));
- 비트 0 (값 1) — 저장 공간을 해제하라
- 비트 1 (값 2) — 배열이다
- 비트 2 (값 4) — 전역
operator delete를 부르라(즉::delete였다)
그래서 ::delete는 5(=1|4), ::delete[]는 7(=1|2|4)이 됩니다.
왜 이렇게 만들었냐면, MSVC에는 확장이 하나 있기 때문입니다. 2014년에 열린 이슈 #19772의 설명이 정확합니다 — MSVC는 배열의 정적 타입과 동적 타입이 일치하지 않아도 다형 객체 배열을 delete[]할 수 있게 해 주는 확장을 지원합니다. 즉 A*로 받은 것이 실제로는 B[]여도 delete[] a가 동작합니다. 표준 C++에서는 UB지만 MSVC는 되게 해 뒀고, 그걸 위해 vftable의 소멸자 슬롯에 "배열도 처리할 줄 아는" ??_E를 넣습니다.
Clang이 11년 동안 못 넣은 것
문제는 Clang이 그 슬롯에 ??_G(스칼라)를 넣었다는 것입니다. 이슈 #19772는 2014년 4월 11일에 열렸습니다. 닫힌 건 2025년 11월 13일 — 11년 7개월이 걸렸습니다.
그 사이 Clang은 MSVC와 vftable 내용이 어긋난 채로 살았습니다. 최종적으로 이 기능을 넣은 PR #170337의 설명이 상황을 요약합니다 — MSVC의 가상 테이블은 가상 소멸자를 가진 클래스에 대해 항상 vector deleting destructor 포인터를 담기 때문에, 이 확장을 구현하지 않으면 clang은 MSVC가 만든 코드와 호환되지 않는 코드를 내게 된다는 것입니다. clang은 항상 scalar deleting destructor 포인터를 vtable에 넣으니까요. 그리고 덤으로, 다형 객체 배열의 삭제가 MSVC와 똑같이 동작하게 된다고 덧붙입니다 — 메모리 누수 없이, 올바른 소멸자가 호출되도록.
왜 11년이 걸렸는지는 커밋 히스토리가 말해 줍니다. 이 기능은 네 번 랜딩되고 세 번 되돌려졌습니다.
| PR / 커밋 | 날짜 | 무슨 일 |
|---|---|---|
| #126240 | 2025-03-04 | 최초 랜딩 |
| (되돌림 커밋) | 2025-03-12 | 되돌림 — Chromium의 sanitizer coverage 링크 에러 |
| #133451 | 2025-03-31 | 리랜드 |
| #135611 | 2025-04-14 | 되돌림 — operator delete[] 탐색이 보안 위험 |
| #165598 | 2025-11-13 | 리랜드 (이슈 #19772 닫힘) |
| #169116 | 2025-11-22 | 되돌림 — Chromium 빌드 깨짐 |
| #170337 | 2025-12-12 | 리랜드 (최종) |
| #172513 | 2025-12-17 | delete[]에 넘기는 크기 수정 |
되돌린 이유가 각각 읽어 볼 만합니다.
첫 번째 되돌림(2025-03-12)의 이유는 Chromium이었습니다. 리랜드 PR인 #133451의 설명에 그대로 적혀 있습니다 — sanitizer coverage를 켠 Chromium에서 링크 타임 에러가 나서 되돌려졌고, 그건 별도 PR #131929로 고쳐졌다고요. 같은 설명이 덧붙이길, 이 리랜드의 두 번째 커밋은 최초 PR의 코멘트에서 보고된 Chromium 런타임 실패에 대한 수정도 담고 있다고 합니다.
두 번째 되돌림(#135611, 2025-04-14)의 이유는 보안이었습니다. operator delete[]를 찾는 것이 여전히 문제이고, 그것 없이는 이 확장이 보안 위험(security hazard)이므로, operator delete[] 문제가 해결될 때까지 되돌린다고 적혀 있습니다.
세 번째 되돌림(#169116, 2025-11-22)의 이유도 다시 Chromium이었습니다. 실제로 머지된 되돌림은 #169116이고, 그 본문은 문제의 커밋과 그것에 의존하는 두 커밋을 함께 되돌린다며 #165598의 토론 코멘트를 가리킵니다. 같은 날 열렸다가 머지되지 않은 되돌림 PR #169063이 이유를 더 직설적으로 적어 뒀습니다 — /Zc:DllexportInlines-를 쓰는 Chromium 빌드를 깨뜨렸기 때문입니다.
그러니까 Chromium이 이 기능을 두 번 막아섰습니다. 3월엔 sanitizer coverage를 켠 링크 에러로, 11월엔 /Zc:DllexportInlines-로. 큰 실사용 코드베이스가 사실상 ABI 변경의 통합 테스트 역할을 한 셈입니다.
그리고 최종 리랜드 닷새 뒤에 나온 #172513은, EmitDeleteCall에 파라미터가 빠져서 delete[]에 배열 전체가 아니라 원소 하나의 크기만 넘어가고 있었다고 적고 있습니다. 이건 리랜드 PR의 코멘트에서 발견됐습니다.
11년 7개월이 걸린 이유는 "아무도 안 했기 때문"이 아닙니다. 이 변경이 건드리는 표면이 넓기 때문입니다.
Clang 22가 바꾼 두 가지
Clang 22의 릴리스 노트 "Potentially Breaking Changes"에는 이 이야기가 두 항목으로 나뉘어 있습니다.
첫째, scalar deleting destructor 정렬 (PR #139566, 2025-09-17 머지). 릴리스 노트를 옮기면, clang은 이전에 ::delete를 Itanium ABI에서처럼 complete object destructor를 부른 다음 적절한 전역 delete 연산자를 부르는 식으로 구현했는데, 이제는 scalar deleting destructor가 객체를 파괴하고 저장 공간까지 해제한다는 것입니다.
PR 작성자가 설명하는 발견 경위가 재밌습니다. vector deleting destructor 작업을 하다가, MSVC가 클래스가 자체 operator delete를 정의했는지에 따라 다른 코드를 낸다는 걸 알아챘다는 겁니다. MSVC는 ::delete일 때 플래그의 3번째 비트를 세워서 넘깁니다(스칼라면 5, 벡터면 7). 반면 이전 clang은 플래그로 0을 넘기고 호출 지점에서 직접 전역 delete를 불렀습니다. 문제는 MSVC로 컴파일된 바이너리와 링크할 때입니다 — MSVC가 만든 호출 지점은 "소멸자가 전역 delete를 부를 것"이라고 가정하고 자기는 부르지 않는데, clang이 생성한 소멸자 본문은 절대 그걸 부르지 않습니다.
릴리스 노트가 드는 예가 명확합니다. 가상 소멸자와 멤버 operator delete를 선언한 클래스 X가 있고, 소멸자는 라이브러리 A에, ::delete 호출은 라이브러리 B에 있다고 합시다. A를 clang 21로, B를 clang 22로 빌드하면, B의 ::delete 호출이 A에 있는 scalar deleting destructor로 디스패치되고 그것은 기대되는 전역 delete 대신 멤버 operator delete를 잘못 호출합니다. 릴리스 노트의 표현으로는, MSVC ABI용으로 빌드된 프로그램의 일부가 clang 21 이하로, 일부가 clang 22(또는 MSVC)로 컴파일되면 메모리 손상이 일어날 수 있는 ABI 변경입니다.
둘째, vector deleting destructor 지원. 릴리스 노트를 옮기면, 가상 소멸자를 가진 클래스의 vtable이 이제 scalar deleting destructor 대신 vector deleting destructor 포인터를 담게 되고, 그것은 이름도 링키지도 다른 별개의 심볼이라는 것입니다. 그래서 같은 클래스를 쓰는 두 바이너리가 서로 다른 clang 버전으로 컴파일되면 런타임 실패가 날 수 있다고 적혀 있습니다.
탈출구는 하나입니다. 둘 다 -fclang-abi-compat=21로 끌 수 있습니다. 이게 왜 하나의 스위치로 둘 다 끄는지는 TargetInfo.cpp를 보면 명확합니다 — 두 훅이 완전히 같은 조건을 봅니다.
bool TargetInfo::callGlobalDeleteInDeletingDtor(
const LangOptions &LangOpts) const {
if (getCXXABI() == TargetCXXABI::Microsoft &&
LangOpts.getClangABICompat() > LangOptions::ClangABI::Ver21)
return true;
return false;
}
bool TargetInfo::emitVectorDeletingDtors(const LangOptions &LangOpts) const {
if (getCXXABI() == TargetCXXABI::Microsoft &&
LangOpts.getClangABICompat() > LangOptions::ClangABI::Ver21)
return true;
return false;
}
getCXXABI() == TargetCXXABI::Microsoft 조건에 주목하세요. 이 변경들은 MSVC ABI 타깃에서만 켜집니다. Itanium ABI를 쓰는 리눅스·macOS·MinGW는 코드 경로 자체에 들어오지 않습니다. 이슈 보고자가 실험으로 좁힌 범위와 정확히 일치합니다.
릴리스 이후 — 두 개의 버그, 하나의 백포트
22.1.0이 나가고 이틀 사이에 이슈가 두 개 올라왔습니다. 둘은 별개의 버그입니다.
#183621 (2026-02-26 보고) — Clang 22 incorrect code for delete[] with MS ABI. 원인은 디버추얼라이제이션이었습니다. 수정 PR #183741의 설명이 간결합니다 — vector deleting destructor는 배열 원소를 도는 루프를 수행하고 delete[]를 부르기 때문에, 그 호출을 단순히 디버추얼라이즈하면 메모리 누수가 있는 잘못된 코드가 나온다는 것입니다. 수정은 vector deleting destructor로의 가상 호출을 내기 전에 디버추얼라이즈 가능한지 확인하고, 가능하면 가상 호출 대신 배열 원소를 도는 정상 루프를 내는 것이었습니다.
이건 2026년 3월 5일에 머지됐고, 백포트 PR #184806으로 release/22.x에 들어가 22.1.2(2026-03-24)에 실렸습니다. llvmorg-22.1.1과 llvmorg-22.1.2 사이를 비교하면 해당 커밋이 그대로 보입니다.
#183255 (2026-02-25 보고) — 이 글 맨 앞의 힙 손상입니다. 메인테이너의 진단은 이렇습니다. 소멸자가 export되지 않았기 때문에 clang은 impl.cpp에 대해 scalar deleting destructor 정의를 생성합니다 — 그 TU에 new[] 호출이 없으니까요. 그런데 main.cpp의 delete[]는 vector deleting destructor 정의를 기대하고, 그건 거기에 없습니다. 이어지는 관찰이 핵심입니다 — MSVC는 main.cpp의 오브젝트 파일에 vector deleting destructor 정의를 생성해서 이 상황을 넘기고, 그것도 new[]가 호출된 경우에만 그렇게 하는데, clang은 main.cpp에 대해 소멸자 정의를 아예 생성하지 않는다는 것입니다.
임시 우회책도 같이 제시됐습니다 — 소멸자 정의를 헤더로 옮기거나, dllexport로 export하면 됩니다.
수정은 PR #185653 "Define vector deleting dtor body for declared-only dtor if needed"로 2026년 3월 17일에 머지됐습니다. 문제의 조건이 PR 설명에 정확히 적혀 있습니다 — 현재 vector deleting destructor 본문 생성은 그 타입에 대해 new[]가 호출되고, 그리고 소멸자나 클래스 전체가 dllexport 속성으로 표시된 경우에 트리거됩니다. 문제는 new[]가 호출되고, 소멸자가 export되지 않았고, 해당 TU에는 선언만 있고 정의는 다른 곳에 있을 때 생성되지 않는다는 것입니다. 그래서 vector deleting destructor 본문이 없어지고 delete[]에서 런타임 실패가 납니다.
그리고 이게 이 글에서 가장 실무적인 부분입니다 — 이 수정은 22.x에 백포트되지 않았습니다.
이슈에 남은 메인테이너의 코멘트가 이유를 말합니다 — 방금 이슈를 고칠 PR을 머지했는데, 22.x에 체리픽하기에 충분히 작은 변경인지 확신이 서지 않아서, 일단 main에서 좀 묵혀 두겠다는 것입니다.
직접 확인해 보면 그 말 그대로입니다. llvmorg-22.1.0부터 release/22.x 브랜치 끝(2026-06-15)까지 180개 커밋 중 소멸자 관련 커밋은 디버추얼라이제이션 수정 하나뿐입니다. #185653의 커밋은 release/23.x의 조상이지만 release/22.x와는 갈라져 있습니다. 2026년 7월 16일 현재 최신 릴리스는 22.1.8(2026-06-16)이고, release/23.x는 바로 어제(2026-07-15) 잘렸으며 아직 rc 태그도 없습니다.
즉 22.1.0부터 22.1.8까지 전부 이 패턴에서 힙이 깨집니다. 고쳐진 건 LLVM 23(대략 9월 예정)입니다.
왜 링크는 되는데 런타임에 깨지나
여기가 이 버그의 진짜 흥미로운 지점입니다. ??_E가 없다면 링커가 unresolved symbol로 잡아 줘야 정상 아닌가요? 그런데 링크는 조용히 성공하고 런타임에 힙이 깨집니다. 왜일까요.
답은 MicrosoftCXXABI.cpp에 있습니다.
if (GD.getDtorType() == Dtor_VectorDeleting &&
!getContext().classNeedsVectorDeletingDestructor(dtor->getParent())) {
// Create GlobalDecl object with the correct type for the scalar
// deleting destructor.
GlobalDecl ScalarDtorGD(dtor, Dtor_Deleting);
// Emit an alias from the vector deleting destructor to the scalar deleting
// destructor.
CGM.EmitDefinitionAsAlias(GD, ScalarDtorGD);
return;
}
clang이 "이 클래스는 진짜 vector deleting destructor가 필요 없다"고 판단하면 — 즉 그 TU에서 new[]를 못 봤고 dllexport도 없으면 — ??_E를 ??_G로 가는 별칭(alias)으로 내보냅니다. 최적화로는 말이 됩니다. 배열을 안 쓰는데 루프가 든 본문을 두 벌 낼 이유가 없으니까요.
그런데 이게 정확히 링크가 성공하는 이유입니다. ??_E 심볼은 존재합니다. 다만 그 본문이 스칼라입니다.
그리고 그 본문에 무엇이 없는지는 CGClass.cpp가 보여 줍니다.
if (DtorType == Dtor_Deleting || DtorType == Dtor_VectorDeleting) {
if (CXXStructorImplicitParamValue && DtorType == Dtor_VectorDeleting)
EmitConditionalArrayDtorCall(Dtor, *this, CXXStructorImplicitParamValue);
배열 분기를 내는 EmitConditionalArrayDtorCall은 Dtor_VectorDeleting일 때만 호출됩니다. 그 함수가 하는 일은 이렇습니다.
llvm::Value *CheckTheBitForArrayDestroy = CGF.Builder.CreateAnd(
ShouldDeleteCondition, llvm::ConstantInt::get(CondTy, 2));
llvm::Value *ShouldDestroyArray =
CGF.Builder.CreateIsNull(CheckTheBitForArrayDestroy);
CGF.Builder.CreateCondBr(ShouldDestroyArray, ScalarBB, VectorBB);
플래그를 2와 AND해서 dtor.scalar냐 dtor.vector냐로 분기합니다. 앞에서 본 배열 비트입니다.
이제 조각이 맞춰집니다.
impl.cpp가 vftable을 내면서 소멸자 슬롯에??_E를 넣는다. 그런데 그 TU에는new[]가 없으므로??_E는??_G(스칼라 본문)로 가는 별칭이다.main.cpp의delete[]가 그 슬롯을 통해 가상 호출을 하면서 플래그에 배열 비트(2)를 세워 넘긴다.- 도착한 본문은 스칼라 본문이다 — 배열 비트를 검사하는 분기 자체가 코드에 없다.
- 그래서 배열 쿠키를 고려하지 않은 채 스칼라로 해제한다. 힙이 깨진다.
링커가 잡아 줄 방법이 없습니다. 심볼 이름은 맞고, 시그니처도 맞고, 다만 의미가 다릅니다. 타입 시스템 바깥에 있는 규약 — "이 심볼의 본문은 배열 비트를 처리한다" — 이 깨진 것이고, 그건 링커가 검사할 수 있는 종류의 것이 아닙니다. Debug 빌드에서 RtlValidateHeap이 먼저 비명을 지르는 게 차라리 다행인 이유입니다. Release에서는 그냥 크래시합니다.
#185653의 수정은 이 판단을 미룹니다. new[]를 보면 소멸자가 선언만 돼 있어도 vector deleting destructor 본문 생성을 강제합니다. PR의 표현으로는, vector deleting destructor가 weak linkage를 갖기 때문에 그렇게 해도 안전하다는 것입니다 — 여러 TU가 각자 본문을 내도 링커가 하나로 접습니다. MSVC가 원래 하던 것과 같은 동작입니다.
그래서 지금 무엇을 확인해야 하나
정직하게 범위부터 좁히겠습니다.
영향 없음. 리눅스·macOS·MinGW 등 Itanium ABI 타깃은 해당 없습니다. 코드 경로가 TargetCXXABI::Microsoft로 가드돼 있습니다. clang-cl이나 -target *-windows-msvc를 쓰지 않는다면 이 글은 교양입니다.
확인 필요. MSVC ABI를 타깃하는 clang(clang-cl 포함)으로 22.x를 쓰거나 쓰려는 경우:
- clang 21 이하로 빌드된 오브젝트/라이브러리와 섞고 있는가. 가상 소멸자를 가진 클래스가 그 경계를 넘나든다면, 릴리스 노트가 말하는 메모리 손상 시나리오에 정확히 해당합니다. 전부 다시 빌드하거나, 다시 빌드할 수 없다면
-fclang-abi-compat=21로 22 쪽을 옛 동작에 묶어야 합니다. - 가상 소멸자를 가진 클래스를
new[]/delete[]하는데, 그 소멸자 정의가 다른 TU에 있고dllexport가 아닌가. 이게 #183255 패턴이고, 22.1.8까지 살아 있습니다. 우회책은 소멸자 정의를 헤더로 옮기거나dllexport하는 것입니다. - 22.1.0이나 22.1.1을 쓰고 있다면 최소 22.1.2로 올리세요. 디버추얼라이제이션 버그(#183621) 수정이 거기 들어 있습니다.
-fclang-abi-compat=21에 대한 정직한 말. 이건 되돌리기 버튼이지 해결책이 아닙니다. 이 플래그를 켜면 clang 21 이하와는 호환되지만, MSVC와는 다시 어긋납니다 — 애초에 이 변경이 고치려던 그 어긋남으로 돌아갑니다. MSVC로 빌드된 라이브러리와 링크할 계획이 있다면 그건 해가 아닙니다. 전부 clang으로 빌드하는 폐쇄된 세계에서 전환을 미루는 용도로만 쓸 만합니다.
이 변경 자체는 옳습니다. 이 글이 버그 이야기로 채워져 있어서 오해를 살까 봐 덧붙이면, Clang 22의 방향은 맞습니다. 이전 clang은 MSVC가 만든 vftable과 내용이 어긋난 코드를 냈고, 다형 객체 배열의 delete[]는 조용히 새거나 틀린 소멸자를 불렀습니다. 11년 묵은 비호환을 고치는 데 ABI 변경이 필요했고, ABI 변경에는 원래 이런 종류의 대가가 따릅니다. 이 이야기의 교훈은 "clang 22를 쓰지 마라"가 아니라, ABI 경계를 넘는 변경은 컴파일러 버전을 섞는 순간 링커가 도와줄 수 없는 자리에서 실패한다는 쪽입니다.
마치며
정리하면 이렇습니다. Clang 22.1.0(2026-02-24)은 MSVC ABI 타깃의 소멸자 코드 생성을 두 군데 바꿨습니다 — ::delete를 소멸자 본문 안에서 처리하도록 MSVC와 맞췄고, 2014년에 열린 이슈를 11년 7개월 만에 닫으며 vector deleting destructor를 넣었습니다. vftable 소멸자 슬롯의 심볼이 ??_G에서 ??_E로 바뀌는 ABI 변경이고, 탈출구는 -fclang-abi-compat=21 하나입니다.
그리고 릴리스 다음 날부터 이틀 사이에 버그가 두 개 올라왔습니다. 디버추얼라이제이션 버그는 22.1.2에 백포트됐습니다. 하지만 ??_E가 ??_G의 별칭으로 나가면서 배열 비트를 처리하는 분기 없이 힙을 깨뜨리는 쪽은, 2026년 7월 16일 현재 22.1.8까지도 고쳐지지 않았습니다 — LLVM 23을 기다리거나, 소멸자를 헤더로 옮기거나 dllexport해야 합니다.
이 기능이 네 번 랜딩되고 세 번 되돌려진 이유 — Chromium을 두 번(sanitizer coverage 링크 에러, 그리고 /Zc:DllexportInlines-) 깨서 되돌리고, operator delete[] 탐색이 보안 위험이라 되돌리고, 최종 리랜드 닷새 뒤에 배열 크기를 잘못 넘기던 걸 고치고 — 를 보면, 11년이 걸린 게 게으름이 아니었다는 게 분명해집니다. 컴파일러의 ABI 표면은 릴리스 노트 한 줄로 요약되지만, 그 한 줄이 커버하는 실패 모드는 릴리스가 나가고 사용자가 자기 코드베이스를 들이받아 봐야 드러납니다. 이번엔 하루 걸렸습니다.
참고 자료
- Clang 22 릴리스 노트 — Potentially Breaking Changes (release/22.x 원본)
- Issue #19772 — Implement vector deleting destructors (2014-04-11 개설)
- Issue #183255 — [Clang-Cl] Wrong destructing code generated in Clang 22
- Issue #183621 — [clang] Clang 22 incorrect code for
delete[]with MS ABI - PR #139566 — [win][clang] Align scalar deleting destructors with MSABI
- PR #170337 — Reland [MS][clang] Add support for vector deleting destructors
- PR #133451 — 리랜드: 첫 되돌림이 Chromium sanitizer coverage 링크 에러였다는 설명
- PR #135611 — 되돌림: operator delete[] 탐색이 보안 위험
- PR #169116 — 실제로 머지된 세 번째 되돌림
- PR #169063 — 머지되지 않은 되돌림 PR: Chromium /Zc:DllexportInlines- 빌드 깨짐 기록
- PR #172513 — delete[]에 넘기는 크기 수정
- PR #183741 — 디버추얼라이제이션 수정 (22.1.2에 백포트)
- PR #185653 — 선언만 있는 소멸자에 대한 vector deleting dtor 본문 생성 (22.x 미백포트)
- MicrosoftCXXABI.cpp (release/22.x) — 플래그 비트와 별칭 생성
- CGClass.cpp (release/22.x) — EmitConditionalArrayDtorCall
- TargetInfo.cpp (release/22.x) — abi-compat 게이트
- LLVM 릴리스 목록 (22.1.0 = 2026-02-24, 22.1.8 = 2026-06-16)