fail-closed 값을 Long.MAX_VALUE 로 두면 합산에서 넘쳐 통과한다
확인할 수 없는 크기를 Long.MAX_VALUE 로 표현했더니 합산에서 -2 로 넘쳐 한도 검사를 통과했습니다. 유한 상한과 포화 덧셈으로 고친 과정을 적었습니다.
업로드 용량 검사를 손보다가, 서버가 크기를 확인할 수 없는 첨부를 어떻게 다룰지 정해야 했습니다. 확인이 안 되면 통과시키는 게 아니라 막아야 합니다(fail-closed).
그런데 검사 코드를 감싼 쪽이 예외를 삼키고 통과시키는 구조였습니다. 그래서 예외를 던지는 대신 크기 값 자체를 무한대로 돌려주기로 했습니다.
정수에는 무한대가 없습니다
자바에는 정수 무한대가 없으니 Long.MAX_VALUE 를 쓰고 싶어집니다. 어떤 한도보다 크니 항상 막히겠죠?
long total = 0;
for (Item item : items) {
total += sizeOf(item); // 확인 불가 항목은 Long.MAX_VALUE
}
if (total > limit) reject();확인 불가 항목이 하나일 때는 맞습니다. 그런데 둘이 되면 Long.MAX_VALUE + Long.MAX_VALUE 가 예외 없이 감겨 돌아서 -2 가 됩니다.
막으려고 넣은 값이 우회 수단이 된 셈입니다. 요청에 확인 불가 항목을 두 개 이상 섞기만 하면 됩니다.
합을 다른 서비스에 넘겨 거기서 usage + incoming 을 계산하게 해도 똑같이 넘칩니다. 그쪽이 JavaScript 라면 2^53 을 넘는 순간 정밀도부터 깨집니다.
유한한 상한과 포화 덧셈
고치는 방법은 두 가지를 같이 쓰는 겁니다.
먼저 무한대 대신 유한한 상한을 씁니다. 어떤 정상 한도보다 확실히 크면서, 여러 번 더해도 넘치지 않는 값이면 됩니다. 1 PiB(2^50)면 충분하고, JS 안전 정수(2^53) 안이라 다른 쪽으로 넘겨도 정밀도가 유지됩니다.
그리고 합은 그 상한에서 멈추게 합니다. 이른바 포화 덧셈입니다.
static final long UNVERIFIED = 1L << 50; // 1 PiB. 어떤 한도보다 크고, 더해도 넘치지 않는다
static long plus(long total, long bytes) {
return Math.min(UNVERIFIED, total + bytes);
}
long total = 0;
for (Item item : items) {
total = plus(total, sizeOf(item)); // 확인 불가 항목은 UNVERIFIED 를 돌려준다
}
if (total > limit) reject();total 과 bytes 가 둘 다 상한 이하이면 total + bytes 는 2^51 을 넘지 않아서 중간 합도 long 범위 안입니다. 확인 불가 항목이 몇 개든 합은 2^50 에서 멈추고, 한도 비교는 항상 참이 됩니다. 정상 크기도 이 상한 아래라고 보는 전제입니다.
Math.addExact 는 넘치면 예외를 던지지만, 예외를 삼키는 구조에서는 쓸 수 없습니다.
기존 경로는 따로 봅니다
기존 동작을 바꾸면 안 되는 경로(기능 스위치가 꺼진 상태 등)에는 포화를 적용하지 않고, 새 판정 모드에서만 쓸 수도 있습니다. 다만 그 경로에 원래 있던 넘침 우회는 별도로 고쳐야 합니다.
"확인할 수 없음"을 값으로 표현할 때는 그 값이 지나갈 모든 산술(합, 차, 비교, 직렬화)을 한 번씩 따라가 봐야 합니다. 무한대 흉내는 산술 한 번에 부호가 뒤집힙니다.