다음과 같은 콜스택으로 크래시가 보고되어 원인 파악해보았는데, 수정이 필요할 것으로
생각되어 문의드립니다.
원인
InstanceDataSceneProxy.h의 BeginWriteAccess에 다음 check문이 있습니다.
FWriteView BeginWriteAccess(FAccessTag AccessTag)
{
check(AccessTag.Kind == FAccessTag::EKind::Writer && AccessTag.WriterTag != 0u);
…
}
FAccessTag는 WriterTag == 0을 invalid로 예약하고 있는데, 엔진 전반에서 writer tag를 FAccessTag(PointerHash(this)) 형태로 생성하는 패턴이 널리 사용됩니다.
예시 (FastGeoInstancedStaticMeshComponent.cpp:97-98):
FInstanceSceneDataBuffers::FAccessTag AccessTag(PointerHash(this));
auto View = InstanceSceneDataBuffers.BeginWriteAccess(AccessTag);
이 패턴은 ISMInstanceDataSceneProxy.cpp, InstanceDataManager.cpp, InstancedSkinnedMeshComponent.cpp 등에서도 동일하게 사용되며, 확인된 것만 총 14곳 이상입니다.
PointerHash가 0을 반환하는 조건
uint32 PointerHash(const void* Key)
{
const UPTRINT PtrInt = reinterpret_cast<UPTRINT>(Key) >> 4;
return UE::Private::MurmurFinalize32((uint32)PtrInt);
}
여기에 두 가지 문제가 겹칩니다.
1) 64비트 포인터의 uint32 truncation
(uint32)PtrInt은 포인터의 상위 32비트를 버립니다. 따라서 this의 하위 36비트(>> 4 후 하위 32비트)가 모두 0인 주소 — 예를 들어 0x00000002’00000000 같은 주소에 객체가 할당되면 (uint32)(ptr >> 4) == 0이 됩니다.
2) MurmurFinalize32(0)은 확정적으로 0을 반환
즉, 확률적 문제가 아니라 특정 주소 조건에서 반드시 0을 반환하며, 이 경우 WriterTag != 0 계약을 위반하여 크래시로 이어집니다.
수정 제안
드물게 발생하지만 발생 시 반드시 크래시로 이어지며, 동일 패턴이 엔진 전반 14곳 이상에서 사용되고 있으므로 보완이 필요할 것 같습니다.
가능한 수정 방향:
- (A) FAccessTag 생성자에서 전역 보장 — WriterTag = InWriterTag ? InWriterTag : 1u 로 non-zero를 강제
- (B) 각 호출처에서 non-zero 정규화 — Tag = PointerHash(this); if (Tag == 0) Tag = 1;
호출처가 14곳 이상이므로 **(A)**가 누락 없이 안전할 것으로 보이는데, 어떤 방향이 적절할지 의견 부탁드립니다.
[Attachment Removed]