[6] ANGLE IndexRange: stored inclusive count replaced by a derived one
Severity는 Low이며, 특히 실제로 우회 가능한 취약점은 아닙니다. Pre-image 상태에서도 inclusive cardinality를 uint64_t에 저장하고 있어, 최대 span이 정확하게 보고되고 bounds check가 생략되는 경우도 없습니다. 이번 패치가 제거하는 것은 구조적인 위험 요소입니다. 즉, endpoint와 나란히 별도로 저장되는 count 값이 있었고, "empty" 상태가 동일한 산술 연산으로도 만들어질 수 있는 값으로 인코딩되어 있었습니다.
N-bit 정수 타입에 대한 inclusive range는 자신의 cardinality를 표현하기 위해 N+1 bit가 필요합니다. 예를 들어 [0, 0xFFFFFFFF]는 0x100000000개의 원소를 담습니다. 이 산술적 사실은 WebGL의 indexed-draw bounds validation과 직결되는 부분으로, web content가 호출한 glDrawElements가 활성화된 attribute에 바인딩된 storage 범위 밖의 vertex를 절대 참조하지 못하도록 보장해야 합니다. ANGLE의 gl::IndexRange가 바로 이 span을 담는 value type입니다. Untrusted element-array buffer를 스캔해 생성되고, 캐시되며, 각 attribute의 element limit과 비교됩니다. 이때 isEmpty() predicate가 호출자에게 "여기에는 검증할 대상이 없다"는 신호를 전달하는 역할을 합니다.
관전 포인트: 이번 변경은 방어적 성격에 그칩니다. 최대 index span은 패치 이전에도 정확하게 보고되고 있었지만, empty 인코딩을 cardinality 산술로는 더 이상 합성할 수 없게 되었습니다. 추가된 테스트는 세 가지 index width 전체에 걸쳐 {0, 0xFFFFFFFF}에 대해 GL_INVALID_OPERATION을 고정적으로 검증합니다.
IndexRange는 uint32_t start, uint32_t count로 index range를 표기했는데, 이 방식으로는 [0, 0xFFFFFFFF] 범위를 제대로 표현할 수 없습니다. 이를 start, end 표기로 전환하고, start > end인 경우를 empty range로 취급하도록 변경합니다.
Source/ThirdParty/ANGLE/src/common/mathutil.h
Source/ThirdParty/ANGLE/src/tests/gl_tests/WebGLCompatibilityTest.cpp
Source/ThirdParty/ANGLE/src/common/utilities_unittest.cpp
Patch Details
이번 변경은 gl::IndexRange를 (start, end, stored-count) 구조에서 순수한 (start, end) 구조로 재구성하고, 이어서 unit test와 conformance test 양쪽의 커버리지를 확장합니다.
먼저 타입 자체를 살펴보면, 생성자는 더 이상 mCount를 계산해서 저장하지 않고 mStart/mEnd만 ASSERT(mStart <= mEnd)와 함께 보관합니다. isEmpty()는 mCount == 0 대신 mStart > mEnd로 판정하도록 바뀌었고, 기본 멤버 초기값도 mStart{0}, mEnd{0}, mCount{0}에서 mStart{1}, mEnd{0}으로 변경됩니다. 그 결과 기본 생성된 range는 생성자가 요구하는 순서 규칙 바깥에 위치하는, 정형화된 empty range가 됩니다. vertexCount()는 static_cast<size_t>(mEnd) - mStart + 1u로 파생 계산되는 size_t 값으로 다시 표현되며, 포함 범위 [0, 0xFFFFFFFF]의 원소 수가 0x100000000개라 32비트에 담기지 않는다는 점과, empty range에 대해서는 unsigned underflow가 의도적으로 0을 만들어낸다는 점을 명시하는 주석이 추가되었습니다. 기존의 수작업 operator==는 vertexCount()와, non-empty range의 경우 start()만 비교하던 방식이었는데, 이는 삭제되고 멤버 단위로 비교하는 defaulted friend bool operator==로 대체되었습니다.
테스트 측면에서는, utilities_unittest.cpp에 0xffffffff를 포함한 UnsignedInt 인덱스에 대한 ComputeIndexRange의 경계값 테스트와, 32비트 경계에서의 isEmpty()/vertexCount() 직접 검증이 추가되었습니다. Xcode 프로젝트에도 해당 파일이 unit test target에 추가되었습니다. WebGLCompatibilityTest.cpp에서는 테스트 대상 프로그램이 buffer로 뒷받침되는 vertex attribute를 갖도록 확장한 뒤, 인덱스 데이터 {0, 0xFFFFFFFF}로 draw하여 GL_UNSIGNED_INT/GL_UNSIGNED_SHORT/GL_UNSIGNED_BYTE 각 element type에서 GL_INVALID_OPERATION이 발생하는지, 그리고 {0, 1}에서는 GL_NO_ERROR가 나오는지를 검증합니다.
range의 카디널리티를 endpoint와 별도로 저장된 state로 중복시키면서, 동시에 'empty'를 그 카디널리티 계산 자체가 만들어낼 수 있는 값으로 인코딩한 패턴.
Background
이 코드가 있는 위치. ANGLE은 WebKit에서 WebGL을 뒷받침하는 OpenGL ES / EGL translation layer로, GPU process에서 동작합니다. 따라서 이곳의 validation layer는 web에서 제어하는 draw parameter와 platform graphics backend 사이의 경계에 위치합니다.
Indexed drawing.
glDrawElements(mode, count, type, offset)는 GL_ELEMENT_ARRAY_BUFFER에 bind된 buffer에서 offset부터 시작해 type(GL_UNSIGNED_BYTE/SHORT/INT) 형식의 인덱스를 count개 읽어들이고, 각 인덱스마다 활성화된 모든 vertex attribute array에서 대응하는 element를 가져옵니다.
gl::IndexRange.
Indexed draw call이 참조하는 vertex 인덱스의 포함 범위 [start, end]를 나타내는 ANGLE의 value type이며, 해당 범위가 포괄하는 vertex 개수를 나타내는 vertexCount()도 함께 가집니다. common/utilities.cpp의 ComputeTypedIndexRange는 주어진 정수 폭의 인덱스를 count개 스캔하며 minIndex/maxIndex를 추적하고, gl::IndexRange(minIndex, maxIndex)를 반환합니다. Vertex가 하나도 없는 경우에는 기본 생성된 range를 반환합니다.
Primitive restart.
하나의 예약된 인덱스 값(std::numeric_limits<IndexType>::max())이 "이 vertex를 가져와라"가 아니라 "새 primitive를 시작하라"는 의미로 쓰이는 모드입니다. 이 모드가 켜져 있으면 ComputeTypedIndexRange는 min/max를 계산할 때 해당 값을 건너뛰는데, 이 때문에 같은 index buffer라도 플래그의 on/off에 따라 서로 다른 range가 나오게 됩니다.
Index range caching.
gl::IndexRangeCache(std::map<IndexRangeKey, IndexRange>)와 gl::IndexRangeInlineCache는 element type, offset, count, primitive-restart 플래그를 key로 계산된 range를 캐싱하여, 비용이 큰 스캔이 index buffer 영역당 한 번만 실행되도록 합니다. Lookup은 IndexRangeKey::operator== / operator<로 key를 비교하고, 저장된 IndexRange payload를 복사해서 꺼내옵니다.
Vertex attribute element limits.
VertexArray::updateCachedElementLimit()가 관리하는 VertexAttribute::getCachedElementLimit()는 활성화된 attribute가 자신이 bind된 buffer의 크기, stride, offset을 고려했을 때 몇 개의 element를 공급할 수 있는지를 기록합니다. Desktop GL과 달리 WebGL은 이 한계를 넘어서는 인덱스를 참조하는 draw를 undefined behaviour로 두지 않고, GL_INVALID_OPERATION으로 거부하도록 요구합니다.
ANGLE의 ASSERT.
common/debug.h의 이 매크로는 assertion이 활성화된 빌드에서만 동작하고, 그렇지 않으면 컴파일 시 제거됩니다. 따라서 ASSERT로 표현된 precondition은 debug 빌드에서 계약을 문서화하는 역할을 할 뿐, release 빌드에서 런타임에 강제하지는 않습니다.
Inclusive versus half-open ranges.
uint32_t 위의 포함 범위(inclusive range)에서 카디널리티는 end - start + 1이며, 이 값의 최댓값([0, 0xFFFFFFFF]의 경우 0x100000000)은 33비트를 필요로 합니다. size_t는 LP64 타겟에서는 64비트, ILP32 타겟에서는 32비트입니다.
Analysis
먼저 무엇이 잘못되지 않았는지부터 짚어보겠습니다. 이 diff가 대체하는 이전 코드에서 mCount는 uint64_t로, static_cast<uint64_t>(end - start) + 1로 계산됩니다. start <= end인 한 이 연산은 정확합니다. 생성자 자체의 precondition 하에서는 uint32_t 뺄셈이 wrap될 수 없고, widening cast가 + 1 이전에 일어나기 때문입니다. 따라서 IndexRange(0, 0xFFFFFFFF).vertexCount()는 0x100000000이 되고, isEmpty()도 정확히 false를 반환합니다. 이 패치가 대체하는 코드에는 32비트 wrap-to-empty 문제가 존재하지 않습니다.
이전 코드의 문제는 구조적인 것입니다. (a) 카디널리티가 [mStart, mEnd]와 항상 일치해야 하는 중복 state로 존재한다는 점, (b) emptiness가 도메인 바깥의 endpoint 쌍이 아니라 그 산술 연산 자체가 만들어낼 수 있는 값인 mCount == 0으로 인코딩된다는 점, (c) 기본 생성된 range(mStart{0}, mEnd{0})가 endpoint 멤버만 놓고 보면 유효한 단일 vertex range [0, 0]과 구조적으로 동일하다는 점입니다. 이 둘을 구분해주는 것은 오직 mCount가 갖는 추가 비트뿐이었으므로, mStart/mEnd만 읽는 consumer나 향후 count의 폭을 좁히는 변경이 생기면 두 값이 그대로 합쳐질 수 있었습니다.
이 fix가 차단하는 것은 바로 이 두 결함이 열어두던 위험입니다. ComputeTypedIndexRange()는 element-array buffer를 스캔해 gl::IndexRange(minIndex, maxIndex)를 반환합니다. Primitive restart가 꺼진 상태에서 UnsignedInt 내용이 {0, 0xFFFFFFFF}라면 이는 가능한 최대 포함 범위이고, 그 카디널리티는 33비트를 필요로 합니다. 이 카디널리티를 32비트 값에 그대로 담아내는 어떤 표현 방식이든 결과는 0이 될 것이고, isEmpty() { return mCount == 0; }와 같은 predicate 아래에서는 가능한 가장 넓은 인덱스 범위가 vertex가 전혀 없는 상태로 잘못 해석될 수 있었습니다. 이번 패치는 이를 구조적으로 제거합니다. vertexCount()가 이제 파생값이므로 어떤 consumer도 endpoint와 어긋나는 count를 관찰할 수 없고, emptiness는 mStart > mEnd로 옮겨졌으며, 기본 초기값도 생성자 계약이 허용하는 순서 규칙 바깥에 위치하게 되었습니다. 이 계약은 ASSERT(mStart <= mEnd)로 표현되는데, assertion이 꺼진 빌드에서는 컴파일 시 제거됩니다. 따라서 이 분리는 컨벤션과 debug tripwire의 조합일 뿐, construction 시점에 보장되는 것은 아닙니다. 같은 유보 사항이 start()/end()의 ASSERT(!isEmpty()) 가드에도 그대로 적용됩니다.
operator== 교체 역시 같은 맥락의 중복 제거 작업입니다. 기존 comparator는 타입을 (vertexCount(), start())로 투영해서 비교했고, endpoint 멤버가 무엇이든 상관없이 count가 0인 range는 모두 같다고 취급했습니다. 즉 state 자체가 아니라 파생된 view를 비교하고 있었던 셈입니다. 다만 이 operator는 index-range 캐시의 hit 여부를 결정하는 요소가 아니라는 점에 유의할 필요가 있습니다. libANGLE/IndexRangeCache.h를 보면, IndexRangeInlineCache::get()은 IndexRangeKey::operator==를 비교하고 IndexRangeCache의 std::map은 IndexRangeKey::operator<로 정렬합니다. IndexRange는 그저 복사되어 나오는 payload일 뿐입니다. 따라서 IndexRange::operator==가 관여하는 것은 추가된 unit test의 assertion과 같은 값 비교이지, lookup의 정확성이 아닙니다.
새로 추가된 conformance test case는 이 표현 방식이 지원해야 하는 end-to-end 동작을 고정시켜 검증합니다. (1) a_Position attribute를 가진 program에 glEnableVertexAttribArray(posLocation), glVertexAttribPointer(posLocation, 4, GL_FLOAT, GL_FALSE, 0, nullptr)를 적용하고, GL_ARRAY_BUFFER에 float 12개, 즉 vec4 vertex 3개를 담아 attribute가 3개의 element를 공급할 수 있도록 구성합니다. (2) primitive restart가 꺼진 상태에서 {0, 0xFFFFFFFF}를 GL_UNSIGNED_INT 인덱스 데이터로 업로드합니다. (3) glDrawElements(GL_LINES, 2, GL_UNSIGNED_INT, 0)를 호출하며 GL_INVALID_OPERATION이 발생해야 함을 검증합니다. GL_UNSIGNED_SHORT를 offset 2에서, GL_UNSIGNED_BYTE를 offset 3에서 재해석하는 변형 테스트도 같은 바이트를 각각 {0, 0xFFFF}, {0, 0xFF}로 재해석하며, 이 값들 역시 3개짜리 attribute의 한계를 넘으므로 거부되어야 합니다.
이 패치가 방어하는 시나리오는 조건부이며 이번 diff에서 직접 드러나지는 않습니다. 만약 이 타입의 어떤 구현이든 포함 카디널리티를 32비트 폭에 그대로 담아냈다면 — 예를 들어 ILP32 타겟에서의 파생 size_t 표현이 그러했을 것처럼 — 가능한 최대 범위는 count가 0으로 평가되었을 것이고, isEmpty()나 vertexCount() == 0에 의존하는 consumer는 가능한 가장 넓은 인덱스 범위에 대해 "검증할 것이 없다"는 경로를 탈 수도 있었습니다. 다만 이것이 실제로 성립하려면 그런 폭이 좁혀진 구성이 shipping 빌드에 존재해야 하고, getCachedElementLimit()와의 element-limit 비교가 이 predicate로 guard되어야 합니다. 제공된 validationES.cpp 발췌는 잘려 있고 IndexRange consumer를 포함하지 않으므로, 두 조건 모두 이번 자료로는 확인되지 않습니다. 만약 이런 오판이 실제로 도달 가능했다면, 공격자가 얻을 수 있는 이론적 이득은 attribute stride로 스케일된, 페이지가 선택한 offset에서의 상대적인 out-of-bounds vertex fetch였을 것입니다. 그 내용이 shader input으로 흘러들어가고, rendering과 readPixels를 통해 회수 가능했을 수 있습니다. 즉 GPU process에서의 information-disclosure나 crash로 이어질 가능성이 있는 시나리오이지, write는 아닙니다. gl::IndexRange가 관여하는 것은 read-side 크기 비교이기 때문입니다.
이 value type이 위치한 지점은 WebGL의 필수적인 indexed-draw bounds validation입니다. Platform backend와 driver는 web content에 대한 trust boundary가 아니기 때문입니다. 이 코드의 security model이 전제하는 것은, 계산된 index range가 신뢰할 수 없는 index buffer를 32비트 인덱스 공간의 극단값까지 포함해 정확히 나타낸다는 것과, "empty"가 "카디널리티가 wrap된 상태"가 아니라 "vertex가 없는 상태"를 뜻해야 한다는 것입니다. 이번 commit은 실제로 존재하던 bypass를 닫았다기보다는 hardening 성격의 변경으로 보는 편이 적절합니다.
이전 코드에는 두 가지 design smell이 겹쳐 있었는데, 둘 다 이번 사례에서는 실제로 발현되지 않았지만 기억해둘 가치가 있습니다. 첫째, 포함 범위는 endpoint 타입이 제공하는 것보다 1비트 더 많은 카디널리티를 필요로 합니다. 따라서 자신의 길이를 endpoint와 같은 폭에 그대로 담아내는 inclusive-range 타입은 경계값 하나 차이로 wrap될 위험을 안고 있습니다. 실제로 이 패치의 주석에서도 저자가 바로 이 부분을 고민한 흔적이 드러납니다. 둘째, 더 위험한 것은 sentinel aliasing입니다. mCount == 0이 실제 카디널리티와 "여기 아무것도 없음" 마커라는 두 역할을 동시에 수행했기 때문에, 가장 out-of-bounds한 range에 대해 wrap된 count가 가장 안전한 상태로 잘못 해석될 수 있었습니다. Validation 코드가 if (range.isEmpty()) skip_the_check; 같은 shortcut을 취하는 곳이라면 어디든, 산술 연산이 만들어낼 수 있는 sentinel은 그 shortcut을 잠재적인 bypass 후보로 바꿔놓습니다. 이 패치가 다른 축에서 새로 도입한 유의 사항 하나는, vertexCount()가 이제 uint64_t가 아니라 size_t를 반환한다는 점입니다. ILP32 빌드에서는 최대 range의 카디널리티가 0으로 평가되는 반면 isEmpty()는 false를 반환하게 됩니다. 즉 derived-count 설계는 LP64 타겟에서만 기존의 저장된 uint64_t와 동등하며, 이 값을 사용하는 모든 ANGLE 빌드 구성에 대해 이 점을 확인할 필요가 있습니다.
Audit directions
-
자신의 카디널리티를 endpoint 폭 이하로 계산하는 inclusive range. 여기서 지켜야 할 invariant는 N비트 타입 위의 포함 범위 [lo, hi]의 카디널리티는 N+1비트를 필요로 하므로, 반드시 확실히 더 넓은 타입으로 파생 계산해야 하며 endpoint 폭으로 저장하거나 계산해서는 안 된다는 것입니다. 좁게는,
Source/ThirdParty/ANGLE/src/common과src/libANGLE에서 start/end 쌍과 count/size/length 멤버를 함께 갖는 struct, 그리고 생성자나 accessor 안의+ 1패턴을 검색해볼 필요가 있습니다.gl::IndexRange의 consumer들(ComputeTypedIndexRange,IndexRangeCache::invalidateRange,VertexArray::updateCachedElementLimit)부터 시작해서, 특히 새로운vertexCount()표현이 평가되는 모든 지원 빌드 구성에서size_t가 64비트인지를 확인해야 합니다. 넓게는, half-open 컨벤션과 inclusive 컨벤션이 하나의 타입 안에서 만나는 곳이라면 어디든 같은 유형의 문제가 나타날 수 있습니다. Buffer sub-range invalidation, texture level/layer span,libANGLE/angletypes.h의 pixel-rect clipping 등이 그 예입니다. 코드 리뷰에서는end - start + 1이나last - first + 1형태의 식이end와 같은 폭의 변수, 또는size_t/unsigned같은 platform-width 타입에 대입되는 형태를 눈여겨봐야 합니다. 가장 넓게 보면, 이는 고정폭 정수 위의 closed interval을 다루는 모든 코드베이스에 적용됩니다. 커널의 VMA/extent 구조체, 데이터베이스의 key-range planner, Rust의RangeInclusive::len, LLVM의ConstantRange등이 해당합니다. 어떤 타입이 endpoint와 length를 함께 노출한다면, endpoint가 그 타입의 극단값일 때 length가 어떤 값이 되는지, 그리고 그 값이 모든 타겟에서 length의 타입에 담길 수 있는지를 반드시 따져봐야 합니다. -
유효한 값 공간 안에서 뽑아낸 sentinel 값이라, arithmetic error가 이를 합성해낼 수 있는 경우입니다. Invariant는 "no data"를 나타내는 encoding은 어떤 well-formed constructor로도 도달할 수 없어야 하며, 이 보장이 debug assertion에만 의존하는 경우 그 공백 자체가 finding이 된다는 것입니다. 좁게 보면, emptiness/validity predicate가 숫자 필드를 0 또는
std::numeric_limits<T>::max()와 비교하는 ANGLE의 value type을 점검해야 합니다 — 이번 패치 이전의gl::IndexRange::isEmpty(),libANGLE/VertexAttribute.h의VertexAttribute::kIntegerOverflow,gl::ParseArrayIndex가 반환하는GL_INVALID_INDEX,ComputeTypedIndexRange의 primitive-restart index가 여기 해당합니다. 각각에 대해, overflow나 truncation 경로가 sentinel 값에 도달할 수 있는지, 그리고 sentinel과 payload를 가르는 guard가 실제 branch인지 아니면 release build에서 사라지는ASSERT인지를 확인해야 합니다. 넓게 보면, untrusted input에서 도달 가능한 모든 "magic value가 absence를 의미하는" encoding에서 같은 class가 나타납니다 — not-found를 뜻하는size_t(-1), unset handle을 뜻하는-1, valid offset이면서 동시에 null marker이기도 한0이 그 예입니다. 단서는 literal이나numeric_limitsextreme 값과의 비교인데, 이 값이 동시에 legal payload 값이기도 하다는 점입니다. absence를 별도의 state로 두는 설계 — optional, tagged union, 혹은 이번 패치가 택한 out-of-domain endpoint pair — 가 더 바람직합니다. 가장 넓게 보면, in-band-sentinel class는 parser, allocator, validator가 absence를 데이터 도메인 내부에 encoding하는 모든 곳에 존재합니다 — C의strtoul이 반환하는ULONG_MAX,mmap의MAP_FAILED, HTTP length field, protobuf의 default-zero field 등입니다. Attacker가 arithmetic을 sentinel 값으로 유도할 수 있다면, "여기서는 할 일이 없다"는 guard로 보호되는 code path를 스스로 선택하는 셈이 됩니다. -
Raw data가 아니라 derived predicate를 기준으로 삼는 validation shortcut입니다. Invariant는 security check 안의 early-out은 attacker가 조작할 수 없는 값에 의해서만 정당화되어야 한다는 것입니다. 좁게 보면, 먼저 ANGLE의 indexed-draw 경로에 이런 early-out이 애초에 존재하는지부터 확인해야 합니다.
libANGLE/validationES.cpp와libANGLE/VertexArray.cpp에서IndexRange::vertexCount(),end(),isEmpty()를 사용하는 지점을 추적하고, 이 값이getCachedElementLimit()과 비교되기 전에GLuint,GLint,uint32_t로 narrowing되는지를 점검해야 합니다. 넓게 보면, fast path를 가진 모든 validator에서 같은 형태가 반복됩니다 —ComputeVertexBindingElementCount의 instance-count 및 divisor 계산, transform-feedback capacity 점검, texture upload size 점검이 그 예입니다. 단서는if (computedSize == 0) return true;형태의 guard가 실제 bounds 비교 앞에 놓여 있는 패턴입니다. 가장 넓게 보면, 이는 attacker-influenced-predicate가 check 자체를 gate하는 일반적인 class로, zero-length fast path를 가진 모든 bounds checker에 적용됩니다 — Chromium의 Mojo message validator, 크기가 0인 dimension에서 작업을 건너뛰는 image decoder,len == 0에서 short-circuit하는 kernel copy routine이 그 예입니다. Computed size가 0이라는 이유로 check가 생략될 때마다, 어떤 입력이 그 size를 0으로 만드는지, 그리고 그 입력 중 어떤 것이 실제로는 거대한 object를 empty한 것처럼 위장할 수 있는지를 물어야 합니다. -
Type의 state 자체가 아니라 그 projection을 비교하는 수작업 equality operator입니다. Invariant는 memoisation이나 validation에 사용되는 value type의 equality는 동작에 영향을 미치는 field들에 대해 injective해야 한다는 것입니다. 좁게 보면, ANGLE의 cache 인접 value type들이 일부 member를 무시하는 bespoke
operator==를 갖고 있는지 확인해야 합니다. 이번 패치는 zero-count range를 전부 하나의 class로 묶어버리던gl::IndexRange의 operator를 교체했습니다.libANGLE/IndexRangeCache.h의IndexRangeKey::operator==/operator<— 이것이 실제로IndexRangeInlineCache::get()의 hit 여부와std::map의 ordering을 결정합니다 — 그리고VertexAttribCurrentValueData의 comparator들을 함께 검증해야 합니다. 넓게 보면, 같은 class는 validation 결과의 memoisation에 쓰이는 모든 custom hash/equality 쌍을 포괄합니다 —MemoryProgramCache의 program/shader cache key,BlobCache의 blob cache key가 그 예이며, field 하나를 누락한 comparator가 서로 다른 형태의 query에 대해 같은 entry가 대신 답하게 만들 수 있습니다. 단서는 struct가 선언한 것보다 더 적은 member를 건드리는operator==, 혹은 hash function과 equality function이 어떤 field가 중요한지에 대해 서로 다르게 판단하는 경우입니다. 가장 넓게 보면, memoisation-key-coarser-than-the-computation class는 캐싱되는 모든 security decision에 적용됩니다 — Vary header를 누락한 HTTP cache key, resource가 아니라 principal만으로 keyed된 permission cache, partial shape로 keyed된 JIT inline cache가 그 예입니다. Cache되거나 비교되는 모든 validation 결과에 대해, 어떤 두 개의 서로 다른 입력이 equal로 비교되는지, 그리고 그중 하나는 안전하고 다른 하나는 그렇지 않은지를 물어야 합니다.