24. 중첩 컨테이너 재귀 역직렬화 (깊이 제한 부재 / 노드 예산 부재)

난이도 상 해설 보기 →

결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커 (A)(B) 는 주목 위치 힌트다.

결함 코드 · C#
// ============================================================================
// [코드리뷰 문제] C# - 중첩 가능한 컨테이너 메시지의 재귀 역직렬화
// ----------------------------------------------------------------------------
// 시나리오 (프로토콜/직렬화 - 서버-클라/서버-서버):
//   - 게임 메시지는 "값(Value)"의 트리다. 값은 정수(INT)이거나, 다른 값들을 담는
//     컨테이너(LIST)다. LIST 안에 LIST 가 또 들어갈 수 있어 임의 깊이로 중첩된다.
//   - 와이어 포맷: [tag(1바이트)] 뒤에 INT 면 [int32], LIST 면 [count(uint32)] + count개의 값.
//   - 수신측은 재귀적으로 파싱해 Value 트리를 만든 뒤 게임 로직에 넘긴다.
//
// 상황:
//   - 신뢰할 수 없는 송신자(클라 또는 다른 존 서버)가 보낸 바이트열을 ParseValue 가 파싱한다.
//   - 정상 메시지는 보통 2~3단계 중첩이지만, 입력은 공격자가 자유롭게 구성할 수 있다.
//
// 과제:
//   이 파서가 적대적 입력에 대해 어떻게 동작하는지 분석하라. 특히 (A) 지점의 재귀 호출과
//   (B) 에서 읽는 count 가 입력에 따라 무엇을 좌우하는지, 그리고 서버 프로세스가
//   어떤 영향을 받는지 설명하라. 수정안과 더 나은 설계를 제시하라.
//   (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================

using System;
using System.Collections.Generic;

public enum Tag : byte { Int = 1, List = 2 }

public sealed class Value
{
    public Tag Tag;
    public int IntVal;
    public List<Value> Children = new();
}

public ref struct ByteReader
{
    private readonly ReadOnlySpan<byte> _buf;
    private int _pos;
    public ByteReader(ReadOnlySpan<byte> buf) { _buf = buf; _pos = 0; }

    public byte ReadU8()
    {
        if (_pos + 1 > _buf.Length) throw new FormatException("truncated");
        return _buf[_pos++];
    }
    public int ReadI32()
    {
        if (_pos + 4 > _buf.Length) throw new FormatException("truncated");
        int v = BitConverter.ToInt32(_buf.Slice(_pos, 4));
        _pos += 4;
        return v;
    }
    public uint ReadU32() => unchecked((uint)ReadI32());
}

public static class MessageParser
{
    // 재귀 파서. 신뢰 불가 입력에서 호출된다.
    public static Value ParseValue(ref ByteReader r)
    {
        var v = new Value { Tag = (Tag)r.ReadU8() };

        if (v.Tag == Tag.Int)
        {
            v.IntVal = r.ReadI32();
            return v;
        }
        // Tag.List
        uint count = r.ReadU32();                       // (B) 자식 개수
        v.Children = new List<Value>((int)count);       // (C) 미리 확보
        for (uint i = 0; i < count; i++)
            v.Children.Add(ParseValue(ref r));          // (A) 자식마다 재귀
        return v;
    }

    public static Value ParsePacket(ReadOnlySpan<byte> data)
    {
        var r = new ByteReader(data);
        return ParseValue(ref r);
    }
}
내 리뷰 · C#
내 답안 · 자동 저장

작성 후 위 해설 보기에서 모범 해설과 대조하세요.