Contribution · PostgreSQL · RDBMS Core (contrib)

PostgreSQL — btree_gist NaN ordering (BUG #19501 / #19524)

contrib/btree_gist의 float4/float8 opclass가 raw C ==/</> 연산자를 그대로 써서 IEEE 754 NaN에 대해 false를 돌려주는 바람에, GiST 인덱스가 regular btree opclass와 같은 total order를 따르지 않는 버그. EXCLUDE 제약 우회, RLS 누수, index scan ↔ seq scan 결과 불일치 세 시나리오가 동시에 발생. NaN-aware 헬퍼(float4_* / float8_* from utils/float.h)로 교체하는 패치 준비 완료.

문제 — BUG #19501 / #19524

contrib/btree_gist/btree_float4.c·btree_float8.c의 5개 GiST 비교 콜백(gbt_float{4,8}{gt,ge,eq,le,lt})이 raw C 연산자를 사용. IEEE 754에서 NaN과의 모든 비교는 false를 돌려주므로, GiST가 "NaN ↔ NaN", "NaN > 1.0" 같은 케이스를 잘못 판정. 같은 float에 대해 regular btree opclass는 float4_cmp_internal()(NaN 모두 동등, NaN이 모든 non-NaN 위에 정렬)을 쓰니까 두 인덱스 종류가 같은 데이터에 대해 다른 결과를 돌려주는 상태.

SQL로 재현 가능한 세 시나리오:

  • Index/seq scan 불일치 — float GiST 인덱스에 WHERE v = 'NaN'::float8이 index scan에선 0 row, seq scan에선 NaN row를 정상 반환.
  • EXCLUDE 우회EXCLUDE USING gist (a WITH =)가 NaN 중복을 허용. gbt_float8eq(NaN, NaN)이 false로 평가되어 충돌 감지가 안 됨.
  • RLS 누수USING (a != 'NaN'::float8) 정책이 index scan 경로에서만 NaN row를 노출. predicate의 !=가 raw 연산자 의미로 인덱스 키에 적용되기 때문.

수정

5개의 boolean 콜백을 utils/float.h의 NaN-aware float{4,8}_{gt,ge,eq,le,lt} inline 헬퍼로 교체, picksplit 비교(gbt_float{4,8}key_cmp)도 float{4,8}_cmp_internal()로 교체. 이 헬퍼들은 regular btree opclass가 이미 쓰는 total order(NaN 모두 동등, NaN이 모든 non-NaN 위에 정렬)와 같으므로, 두 인덱스 종류의 동작이 일치하게 됨. on-disk 포맷 변경 없음 — 메모리 비교 경로만 수정.

회귀 테스트는 contrib/btree_gist/sql/float4.sql·float8.sql에 NaN 섹션을 추가: index scan ↔ seq scan 일치 확인, NaN이 finite 값 뒤로 정렬되는지 확인, EXCLUDE 제약이 NaN 중복을 정확히 거부하는지 확인. 패치 적용 후 make check 결과: core 245/245 + contrib btree_gist 32/32 + 전체 contrib 48 modules 통과.

제출 현황

관련 자료