[3] IOSurface from renderer send right consumed without validation
The privileged process took the sandboxed one's word for a buffer's shape.
High — 권한을 가진 host process가 sandbox에 갇힌 child process의 말만 믿고 공유 buffer의 geometry를 그대로 받아들인 뒤 이를 순회합니다. 이 primitive는 읽기 전용이라는 점에서 그 자체로 완결된 escape보다는 낮은 수준에 머물지만, UI process crash를 넘어서는 확장 여부는 결과 이미지가 attacker에게 관찰 가능한지에 달려 있습니다.
WebKit은 작업을 여러 process로 분리합니다. UI process는 embedding application과 WebKit API를 host하며 renderer의 sandbox에 갇히지 않는 반면, WebContent는 페이지를 렌더링하는 sandbox 안의 child process입니다. Pixel 데이터는 두 process 사이를 IOSurface로 이동합니다. IOSurface는 커널이 관리하는 공유 bitmap으로, width, height, bytesPerRow, pixelFormat, allocSize 같은 자기 서술적 metadata를 함께 가지고 있으며, process 경계를 넘어서는 참조는 MachSendRight — 전달 가능한 Mach port right로, 수신 측에서 실제 surface로 resolve됩니다 — 를 통해 이루어집니다. 수신 측이 기대하는 바는, IPC로 도착한 surface가 WebKit 자체의 IOSurface::create()가 생성한 것이어야 한다는 점입니다. 즉 압축되지 않은 단일 plane이며, metadata 필드끼리 서로 일관되어야 합니다.
관전 포인트: renderer에서 code execution을 확보한 attacker는 snapshot 요청에 대해 악의적인 surface로 응답할 수 있고, 이를 통해 권한을 가진 UI process가 mapping된 allocation 범위를 넘어 읽도록 유도할 수 있습니다. 최소한 브라우저 세션 전체가 crash하며, 결과 이미지가 관찰 가능한 경우에는 인접 메모리 노출로 이어질 가능성도 있습니다.
Commit message
In
WebPageProxy::takeSnapshot(), validate IOSurface from MachSendRight
WebPageProxy::takeSnapshot()now uses the newIOSurface::createFromUntrustedSendRight(), which has stronger checks of the MachSendRight-provided IOSurface, expecting a valid IOSurface as produced uncompressed and uni-planar fromIOSurface::create().
Source/WebCore/platform/graphics/cocoa/IOSurface.mm
Source/WebKit/UIProcess/WebPageProxy.cpp
Tools/TestWebKitAPI/Tests/WebCore/cocoa/IOSurfaceTests.mm
Patch Details
이번 변경은 두 갈래로 나뉩니다. WebCore의 Cocoa IOSurface wrapper 안에 새로운 검증 import 경로가 추가되었고, UI process의 단일 호출 지점이 이 경로를 사용하도록 전환되었으며, 새 계약을 고정하는 GTest도 함께 추가되었습니다.
WebCore 쪽에서는 파일 내부 template인 validateAndCreateFromUntrustedSurface<bytesPerElement>(IOSurfaceRef)가 IOSurfaceGetWidth, IOSurfaceGetHeight, IOSurfaceGetBytesPerRow를 읽어 0 값을 거부하고, IOSurface::maximumSize()를 초과하는 width/height를 거부합니다. 이어서 새로 #import <wtf/CheckedArithmetic.h>로 들여온 CheckedSize를 사용해 width * bytesPerElement <= bytesPerRow와 bytesPerRow * height <= IOSurfaceGetAllocSize(surface)를 요구하며, overflow나 불일치가 있으면 IOSurface::createFromSurface로 넘어가기 전에 nullptr을 반환합니다. 공개 factory인 IOSurface::createFromUntrustedUncompressedWebKitSendRight(const MachSendRight&&)는 ProcessCapabilities::canUseAcceleratedBuffers()를 assert한 뒤 IOSurfaceLookupFromMachPort를 수행하고, IOSurfaceGetPixelFormat으로 분기합니다. kCVPixelFormatType_32BGRA, kCVPixelFormatType_32RGBA, kCVPixelFormatType_30RGBLEPackedWideGamut는 4-byte-per-element validator로, kCVPixelFormatType_64RGBAHalf는 8-byte-per-element validator로 향하며, 그 외 모든 pixel format은 return { }로 빠집니다.
WebKit 쪽에서는 takeSnapshot() reply handler의 MachSendRight 분기가 기존의 IOSurface::createFromSendRight() — 단순히 IOSurfaceLookupFromMachPort와 createFromSurface만 수행하고 어떤 property 검사도 없던 함수 — 대신 새 검증 factory를 사용하도록 바뀌었습니다. GTest는 세 가지 케이스로 구성됩니다. SRGB BGRA surface와 RGBA16F surface는 round-trip에서 살아남아야 하고, YUV422 surface는 nullptr로 거부되어야 합니다.
하위 권한 process에서 넘어온 공유 메모리 handle에 딸린 자기 서술적 metadata를 그대로 신뢰하고, 이를 사용하기 전에 buffer의 geometry 제약 조건을 다시 검증하지 않은 패턴.
Background
WebKit process model.
UI process는 embedding application과 WebKit API를 host하며, WebContent는 페이지를 렌더링하는 sandbox 안의 child process입니다. WKWebView 계열의 snapshot API는 WebPageProxy::takeSnapshot()이 처리하는데, 이 함수는 renderer에게 특정 영역을 그려달라고 요청한 뒤 결과 pixel을 ShareableBitmap으로 받거나, 이번 patch가 다루는 분기에서는 MachSendRight로 참조되는 IOSurface로 받습니다. 이후 IOSurface::sinkIntoImage()가 이 surface를 호출자를 위한 CGImageRef로 변환합니다.
IOSurface.
Cocoa/커널이 관리하는 공유 pixel buffer 객체입니다. IOSurfaceGetWidth, IOSurfaceGetHeight, IOSurfaceGetBytesPerRow, IOSurfaceGetPixelFormat, IOSurfaceGetAllocSize처럼 자기 서술적인 metadata를 함께 가지고 있어 소비자는 이 값들을 읽어 mapping된 byte를 해석합니다. IOSurfaceCreate는 호출자가 이 property들을 서로 독립적으로 지정할 수 있게 허용합니다.
Stride와 alloc size.
bytesPerRow(stride)는 한 pixel row의 시작에서 다음 row 시작까지의 byte 거리입니다. 정상적인 surface라면 최소 width * bytesPerElement이며, alignment padding으로 인해 더 클 수도 있습니다. allocSize는 surface의 backing allocation 전체 크기입니다.
MachSendRight.
전달 가능한 Mach port right입니다. IOSurfaceLookupFromMachPort()는 이를 수신 측 process에서 실제 IOSurfaceRef로 변환합니다. 이 과정이 곧 surface가 process 경계를 넘어 공유되는 방식입니다.
Pixel format.
kCVPixelFormatType_* 형태의 4문자 code로, pixel당 byte 수와 channel 배치를 나타냅니다. YUV 4:2:2를 비롯한 일부 format은 여러 plane으로 구성되거나 chroma subsampling을 사용하기 때문에, base + y * bytesPerRow 같은 단일 linear 모델로는 메모리 구조를 전혀 설명할 수 없습니다.
CheckedSize.
WTF의 overflow 검사가 내장된 size_t wrapper입니다. hasOverflowed()는 산술 연산 과정에서 값이 wrap되었는지를 알려줍니다.
Analysis
이 버그의 본질은 process 신뢰 경계에서 attacker가 제공한 resource handle에 대한 검증이 누락되었다는 점입니다. 그 결과로 out-of-bounds read가 발생하며, non-linear pixel format에 대해서는 layout/format confusion 측면도 함께 존재합니다.
WebContent (sandboxed) UI process (privileged)
────────────────────── ───────────────────────
IOSurfaceCreate( takeSnapshot() reply handler
width = 0x10000, ──MachSendRight──► createFromSendRight()
bytesPerRow = 64, └─ IOSurfaceLookupFromMachPort
allocSize = small, └─ createFromSurface (no checks)
format = 32BGRA) └─ sinkIntoImage()
walks width * 4 bytes/row
for height rows ──► OOB read
▲
└── the boundary the bug crosses
Fix 이전에는 UI process가 들어온 send right를 IOSurface::createFromSendRight()로 resolve했습니다. 이 함수는 IOSurfaceLookupFromMachPort()와 createFromSurface()만 수행할 뿐, resolve된 surface의 어떤 property도 검사하지 않았습니다. 여기서 빠져 있던 invariant는, IPC로 도착한 surface가 반드시 IOSurface::create()가 만든 것이라는 보장이 없다는 점입니다. 송신 측은 IOSurfaceCreate를 직접 호출해 임의의 kIOSurfacePixelFormat, 임의의 kIOSurfaceWidth/kIOSurfaceHeight를 지정할 수 있고, 무엇보다 kIOSurfaceBytesPerRow와 kIOSurfaceAllocSize를 이들과 무관하게 독립적으로 설정할 수 있습니다. 커널이 이런 불일치한 생성을 허용한다는 점은 이번 fix 전체의 전제에 해당합니다. diff나 제공된 source 어디에도 이 사실이 직접 명시되어 있지는 않지만, patch가 설치하는 조건들의 형태 자체가 이 전제가 성립할 때만 의미를 가집니다.
createFromSurface() 이후의 소비자들 — 이 호출 지점에서는 sinkIntoImage() → createImage() — 은 surface를 압축되지 않은 단일 plane, pixel당 N byte 이미지로 취급하고, 읽어야 할 범위를 surface 자신의 metadata로부터 유도합니다. 이번 fix는 서로 구분되는 세 가지 metadata 불일치를 막습니다.
- 선언된 format과 선언된 stride 사이의 어긋남.
bytesPerElement는IOSurfaceGetPixelFormat자체에서 선택되므로,width * bytesPerElement <= bytesPerRow검사는 특이한 소형 element format을 겨냥한 것이 아닙니다. 그런 format은 switch문의default:로 빠집니다. 실제로 이 검사가 막는 것은,kCVPixelFormatType_32BGRA처럼 allowlist에 포함된 format(즉 pixel당 4byte)을 선언하면서도 큰width와width * 4보다 작은bytesPerRow를 함께 선언하는 경우입니다. 이런 surface를 row당 4byte pixelwidth개씩 순회하는 소비자는 각 declared row의 끝을 넘어서게 되고, 누적되면 allocation 끝까지 넘어서게 됩니다. - Row와 alloc 사이의 불일치.
bytesPerRow * height가IOSurfaceGetAllocSize()를 초과하거나 overflow되면,heightrow만큼 순회할 때 mapping 끝을 넘어 실행될 수 있습니다. 이 때문에CheckedSize로 곱한 값을 alloc size와 비교합니다. - Format/layout confusion. Pixel format allowlist는 처리 가능한 4가지 압축되지 않은 단일 plane format 이외의 모든 것을 거부합니다. 여기에는 multi-planar format과 chroma-subsampled format이 포함되는데, 이런 layout은 linear 모델로는 backing memory를 전혀 설명할 수 없습니다.
nullptr을 assert하는YUV422regression test는, 이런 surface가 fix 이전에는 그대로createFromSurface()까지 흘러 들어갔음을 보여주는 직접적인 증거입니다.
maximumSize() clamp와 0 dimension 거부는 같은 metadata 공간의 극단적인 degenerate 값들을 함께 처리합니다.
이 취약점이 성립하려면 snapshot reply를 생성하는 process에서 사전에 code execution을 확보해야 합니다. 어떤 send right를 메시지에 담을지는 오직 reply를 보내는 쪽만 결정하므로, 일반적인 JavaScript에서는 도달할 수 없는 경로입니다. WebPageProxy.cpp 발췌 부분은 takeSnapshot 본문보다 훨씬 앞에서 잘려 있지만, 송신자가 WebContent라는 점은 process model과 새 factory 이름 자체 — 입력을 명시적으로 untrusted라고 표기한 이름 — 에서 유추할 수 있습니다. 이 전제가 성립한다는 가정 하에, 실제 trigger는 다음과 같습니다. embedder나 WebKit 내부가 snapshot을 요청하여 WebPageProxy::takeSnapshot()에 도달하면, 정상적인 상황이라면 IOSurface::create()로 만든 surface의 send right로 응답해야 하지만, 이미 손상된 송신 측은 대신 IOSurfaceCreate를 직접 호출해 악의적인 property dictionary를 만들고 그 port로 응답합니다.
Crash를 넘어서는 확장은 조건부이며, 다음 조건이 모두 충족되어야 합니다. 먼저 attacker가 UI process의 VM layout을 surface mapping 인접 영역까지 충분히 통제하고 있어서 over-read가 의미 있는 데이터에 도달해야 합니다. Surface는 별개의 커널 backed mapping이므로 그 뒤에 무엇이 위치하는지는 제공된 context만으로는 확인되지 않습니다. 다음으로 over-read된 byte가 unmapped page에서 fault를 일으키지 않고 생성된 CGImageRef까지 살아남아야 합니다. 마지막으로 결과 이미지가 attacker에게 관찰 가능한 경로가 있어야 합니다. 이 마지막 조건과 관련해, snapshot 이미지는 web content가 아니라 embedding application의 completion handler로 전달되므로, 손상된 renderer 입장에서는 이를 읽기 위한 별도의 channel이 필요합니다. 반대로 attacker가 악의적인 페이지를 구동하는 악성 application 쪽이라면 이미지를 직접 관찰할 수 있고, 이 경우 over-read는 UI process 메모리 노출로 이어질 수 있습니다. 이런 조건들이 충족되지 않는다면, 안정적으로 관찰되는 결과는 out-of-bounds read fault로 인한 권한을 가진 UI process의 crash입니다.
이 primitive는 attacker가 크기를 정한 mapping의 base를 기준으로 attacker가 영향을 미치는 out-of-bounds read이며, 읽는 범위 역시 송신자가 결정합니다. 읽기 전용이라는 점도 특징입니다. Validator나 takeSnapshot reply 경로의 UI process 쪽 어디에도 surface에 쓰기를 수행하는 코드는 없습니다. 취약한 소비자는 process 분리 구조에서 권한을 가진 쪽에 위치하므로, 성공적인 exploitation은 renderer sandbox의 반대편에서 동작하는 셈이며 추가적인 sandbox escape를 요구하지 않습니다. 다만 읽기 전용이라는 성격 때문에 이 자체로 완결된 escape라기보다는 info leak, ASLR defeat, host process에 대한 DoS 등 escape chain의 한 고리로 기능합니다.
이 버그의 발견 과정은 renderer와 UI process 사이 경계를 대상으로 한 패턴 점검에 가까워 보입니다. 권한을 가진 process가 sandbox 안의 renderer가 생성한 resource handle을 소비하는 지점들을 체계적으로 나열하고, 각 지점에서 handle이 스스로 선언한 property가 검증되는지를 확인했을 가능성이 높습니다. 이 fix가 해당 호출 지점 하나에만 inline check를 넣는 대신 범용 helper인 createFromUntrustedUncompressedWebKitSendRight로 구현된 점은, IOSurface import 경로 전반에 대한 내부 audit이 있었을 가능성을 시사합니다. GPU process의 IOSurface 소비 경로에 대한 이전 hardening 작업에 이은 variant analysis일 가능성도 있습니다. Regression test들이 crash를 재현하기보다는 새 계약을 고정하는 방식으로 작성된 점은 fuzzing보다는 audit 기반 발견을 뒷받침합니다.
이 취약점은 renderer와 UI process 사이의 신뢰 경계를 약화시킵니다. Security model은 renderer를 신뢰할 수 없는 쪽으로 간주하고, renderer가 UI process에 넘기는 모든 resource를 attacker가 통제하는 데이터로 취급해야 한다고 가정합니다. Fix 이전의 takeSnapshot()은 이 원칙 대신, send right가 가리키는 surface가 IOSurface::create()로 생성된 것 — 압축되지 않고 단일 plane이며 metadata가 자체적으로 일관된 것 — 이라고 암묵적으로 가정하고 있었습니다. 이미 renderer에서 code execution을 확보한 attacker는 악의적인 surface로 이를 치환함으로써, 더 높은 권한을 가진 UI process가 mapping된 surface allocation 범위를 넘어 읽도록 유도할 수 있습니다. 최소한으로는 단일 tab이 아니라 브라우저 세션 전체를 무너뜨리는 원격 트리거 가능한 UI process crash로 이어지고, 최선의 경우에는 결과 이미지에 인접 메모리의 byte가 반영되는 통제된 읽기로 이어질 수 있습니다. 이는 그 자체로 완결된 escape라기보다는 sandbox escape chain의 한 단계에 해당하는 권한 경계 침해입니다.
Insight
함수 이름 자체가 security model을 그대로 담고 있습니다. createFromUntrustedUncompressedWebKitSendRight라는 이름은 세 가지를 말합니다. 입력이 untrusted라는 것, 기대하는 형태가 압축되지 않은 것이라는 것, 그리고 기대하는 생성 주체가 WebKit 자신의 IOSurface::create()라는 것입니다. Trust level을 주석이 아니라 API 계약 자체에 담아낸다는 점에서 본받을 만한 패턴입니다. 구조적으로 주목할 점은, createFromSendRight()가 아무 표시 없이 코드베이스에 그대로 남아 있다는 사실입니다. 이 함수를 여전히 사용하는 모든 호출 지점은 이제 암묵적으로 "내 send right는 신뢰할 수 있는 생산자에게서 왔다"고 주장하는 셈이지만, 이 주장을 정당화하도록 강제하는 장치는 API 어디에도 없습니다. 또한 이 fix가 다루지 않는 부분도 눈여겨볼 만합니다. Validation 이후에도 pixel content 자체는 여전히 송신 측 process가 쓸 수 있는 상태로 남습니다. 즉 이번 fix는 content에 대한 신뢰 문제가 아니라 metadata 검증 문제를 다룬 것입니다. Geometry field들은 surface 생성 시점에 고정되어 이후 변경할 수 없다는 점 — 이는 제공된 context에서 직접 확인되는 사실이라기보다 IOSurface의 일반적인 동작 방식입니다 — 이 바로 이 지점에서 check-then-use 패턴이 안전하게 성립하는 이유이며, pixel data라면 같은 패턴이 성립하지 않았을 것입니다.
Audit directions
-
자신의 형태 정보를 스스로 담고 있는 resource handle이 privilege 경계를 넘는 경우입니다. 이때 수신 측은 독립적으로 확립된 bounds가 아니라, 발신 측이 넘긴 metadata로부터 read extent를 도출하게 됩니다. 수신 측은 자신이 순회할 모든 extent를 다시 계산해서, 그것이 매핑된 영역 안에 들어맞는지 직접 증명해야 합니다. 발신 측이 선언한 크기는 사실이 아니라 하나의 입력값으로 취급해야 합니다. Narrow:
Source/WebKit/UIProcess와Source/WebKit/GPUProcess에서 남아 있는IOSurface::createFromSendRight(호출 지점과IOSurfaceLookupFromMachPort를 검색해야 합니다. Send right가 더 낮은 권한을 가진 process의 reply나 message에서 비롯된 경우가 후보에 해당합니다. IPC message handler나 completion handler를 통해 도착한MachSendRight가 width/height/bytesPerRow/allocSize 검사 없이 곧바로createFromSurface/sinkIntoImage/createNativeImage에 도달하는 패턴이 판별 기준입니다. Wider: 같은 부류의 문제는 동일한 경계를 넘는 다른 shared-buffer 타입 전반에서도 반복됩니다.SharedMemory::Handle,ShareableBitmap::Handle,WebCore::SharedVideoFrame, 그리고 CVPixelBuffer를 다루는 경로가 여기 해당하는데, 수신 측이 message에서 선언된 size나 stride를 읽어 순회 크기를 정하는 방식이 문제입니다. Decode된 size나 stride 필드가 실제 매핑 길이와의 비교 없이 loop bound나span생성으로 곧장 흘러가는지가 판별 기준입니다. Widest: 이는 "handle과 공격자가 선언한 descriptor"라는 더 넓은 범주에 속하며, 권한을 가진 process가 더 낮은 권한의 process가 소유한 buffer를 매핑하는 모든 곳에서 나타날 수 있습니다. Chromium의base::UnsafeSharedMemoryRegion과 Mojo로 전달되는gfx::Size, 클라이언트가 stride/modifier를 지정하는 Linux dma-buf import, Vulkan external memory import, offset을 그대로 받아들이는 Wayland compositor의wl_shm_pool처리가 모두 같은 구조입니다. Descriptor와 실제 allocation이 서로 어긋날 가능성이 있다면 항상 어긋난다고 가정해야 하며, clamping이 아니라 불일치 시 거부하는 방식으로 처리해야 합니다. -
Consumer의 메모리 모델이 실제로 지원하는 포맷/variant에 대한 allowlist가 없는 경우입니다. 이 경우 특이한 variant마다 아무 검증 없이 linear fast path를 그대로 타게 됩니다.
base + row * stride + column * elementSize형태로 주소를 계산하는 모든 코드는, 그 전에 buffer가 single-plane이고 비압축이며 elementSize를 알고 있는 포맷인지부터 증명해야 합니다. Narrow:Source/WebCore/platform/graphics/cocoa/IOSurface.mm에 있는 다른IOSurfaceconsumer들과 그 호출자를 점검해야 합니다.createImage,createNativeImage,createPlatformContext,createBitmapPlatformContext, 그리고IOSurfaceGetBaseAddress와IOSurfaceGetAllocSize로부터 span을 구성하는 locking helper들 중IOSurfaceGetPixelFormat을 한 번도 확인하지 않은 surface로 도달 가능한 경로가 있는지 확인해야 합니다. Bytes-per-pixel 상수(4, 8)나PixelFormatenum 값을 surface 자체의 포맷 태그에서 도출하지 않고 맥락상 가정하는 부분이 판별 기준입니다. Wider: decoder나 importer가 포맷 태그로 분기하면서 관대한default:분기를 두는 곳이면 어디든 같은 형태가 나타납니다. 미디어 파이프라인의CVPixelBufferRef처리, WebGPU/GraphicsContextGL의 texture import, ImageBuffer backend 선택이 여기 해당하며, 코드 검색 시 format/fourcc에 대한switch문의default가 거부 대신 일반 경로로 흘러가는 패턴이 판별 기준입니다. Widest: 신뢰할 수 없는 출처에서 온 포맷 태그는 메모리 해석 방식 자체를 결정하므로, layout 관련 가정을 세우기 전에 반드시 closed set 기준으로 검증되어야 합니다. 이는 모든 브라우저의 이미지/비디오 디코더, Vulkan/Metal wrapper의 GPU texture import, type byte로 분기하는 모든 deserializer에 공통으로 적용됩니다. 인식되지 않은 포맷은 fallback이 아니라 반드시 거부로 처리해야 합니다. -
하나는 안전하고 하나는 안전하지 않은 병렬 factory function이 존재하는 경우입니다. 이 둘은 오직 naming discipline만으로 구분되기 때문에, 새로운 호출 지점이 컴파일러나 리뷰어의 아무런 경고 없이 잘못된 쪽을 고를 수 있습니다. 입력의 신뢰 수준은 caller가 잊어버릴 수 있는 convention이 아니라, 타입이나 피할 수 없는 API 구분을 통해 전달되어야 합니다. Narrow:
IOSurface::createFromSendRight(새 factory와 함께Source/WebCore/platform/graphics/cocoa/IOSurface.h에 선언됨)의 남아 있는 모든 호출자를 나열하고, 각각에 대해MachSendRight가 어디서 왔는지 추적해야 합니다.*Proxy.cpp나*MessageReceiver맥락에 있는 호출자는 구조상 신뢰도가 낮은 process로부터 수신하는 쪽에 해당하므로 후보가 됩니다. Wider: 같은 비대칭 쌍 구조는 WebKit의 IPC 레이어 곳곳에서 검증된 variant와 검증되지 않은 variant가 함께 존재하는 decode 경로마다 반복됩니다.MESSAGE_CHECK계열과 decode된 필드를 그대로 사용하는 방식의 대비, caller가 지정한 size를 쓰는SharedMemory::map과 handle 자체의 size를 쓰는 방식의 대비, 그리고 message-handling 경로에서 검증 없이 이루어지는 span 생성이 모두 여기 해당합니다. 시그니처가 거의 동일한 두 함수 중 하나만 validation을 수행하는 패턴이 판별 기준입니다. Widest: 이는 "신뢰된 parser와 신뢰되지 않은 parser가 같은 namespace를 공유하는" 더 넓은 범주에 속하며, Chromium의mojotraits와 raw struct access의 대비, Rust 코드베이스에서from_slice와from_slice_unchecked를 함께 노출하는 경우,parse와parse_trusted를 짝지어 제공하는 라이브러리 전반에서 나타납니다. 안전한 진입점과 안전하지 않은 진입점이 똑같이 도달 가능하고 똑같이 타이핑하기 쉬운 상태라면, 결국 안전하지 않은 쪽이 선택될 수밖에 없습니다. 안전하지 않은 쪽에는 명시적인 witness 타입이나 감사된 annotation을 요구하는 방식이 바람직합니다.