← 문제로

45. 배치 메시지 스트림의 미지(unknown) 타입 처리 (C#)

난이도 상
내 리뷰 · C#
해설 · C#

해설 — 배치 메시지 스트림의 미지(unknown) 타입 처리 (C#)

난이도: 상

요약

디코더가 각 메시지의 선언된 len 을 전진 크기로 사용하지 않고 고정 상수 KnownPayloadSize 만큼만 오프셋을 전진시킨다. 그 결과 (A) 미지 타입이나 크기가 다른 메시지를 만나면 오프셋이 실제 메시지 경계와 어긋나 이후 배치 전체가 desync 되고, (B) len 을 남은 버퍼와 비교하지 않아 Slice(off, len) 에서 범위를 벗어나면 ArgumentOutOfRangeException(또는 손상된 payload 해석)이 난다.

문제점

  • (A) 전진 크기 오류 → 프레임 desync: off += KnownPayloadSize; 는 항상 8 을 더한다. 실제 메시지 길이는 len 이므로 len != 8 인 메시지(특히 서버가 모르는 새 타입, 또는 가변 길이 채팅)를 지나면 다음 메시지의 시작 위치가 어긋난다. 한 번 어긋나면 이후 모든 헤더가 payload 중간을 가리켜 배치 전체가 오해석된다. "미지 타입은 무시"라는 default 는 맞지만, len 만큼 건너뛰지 않은 것이 핵심 결함이다.

    • 재현: 신버전 클라가 배치 앞에 새 타입(len=16) 하나를 넣어 보냄 → 뒤 메시지 붕괴.
    • 근본 원인: 메시지 경계를 선언 길이(len)가 아니라 처리 타입의 가정 크기로 계산.
  • (B) 길이 경계 미검증: buf.Slice(off, len) 전에 off + len <= buf.Length 를 확인하지 않는다. 악성/손상 입력이 len 을 크게 주면 예외로 연결이 끊기거나(가용성), 검증을 우회한 경로에선 인접 데이터를 payload 로 오해석한다.

    • 근본 원인: 신뢰 불가 길이를 경계 검사 없이 사용.

수정안

헤더와 payload 모두 경계 검사하고, 항상 len 만큼 전진한다.

public void Decode(ReadOnlySpan<byte> buf)
{
    int off = 0;
    while (off + HeaderSize <= buf.Length)
    {
        ushort type = (ushort)(buf[off] | (buf[off + 1] << 8));
        ushort len  = (ushort)(buf[off + 2] | (buf[off + 3] << 8));
        off += HeaderSize;

        if (len > buf.Length - off)              // (B) 경계 검증
            throw new ProtocolException("payload length exceeds buffer");

        var payload = buf.Slice(off, len);
        switch (type)
        {
            case 1: HandleMove(payload); break;
            case 2: HandleChat(payload); break;
            default: /* 미지 타입: len 만큼 건너뛴다 */ break;
        }
        off += len;                              // (A) 선언 길이만큼 전진
    }
}

남은 바이트가 헤더보다 작으면(부분 프레임) 루프를 끝내고 다음 수신과 이어붙인다.

더 나은 설계

  • 자기 기술적(self-describing) 프레이밍: 모든 메시지가 길이 프리픽스를 갖게 해 파서가 타입을 몰라도 안전히 스킵(전방 호환)하게 만든다 — protobuf 의 wire-type/length 규칙이 대표적.
  • 엄격 vs 관용 모드: 미지 타입을 스킵(관용)할지 거부(엄격)할지 정책화하고, 메트릭으로 미지 타입 비율을 관측해 배포 회귀를 조기 감지.
  • 최대 배치/메시지 크기 상한과 메시지 개수 상한으로 자원 남용 방지.

면접 포인트

  • 전방 호환(forward compatibility)의 핵심은 "모르는 것을 길이 기준으로 안전히 건너뛰기".
  • 프레임 경계는 처리 코드의 가정이 아니라 wire 상의 선언 길이로 결정해야 한다.
  • 신뢰 불가 길이는 항상 남은 버퍼와 비교 후 사용.