14. 부동소수점 좌표 직렬화와 NaN/Inf·결정론 방어 (C#)
난이도 중 해설 보기 →
결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커
(A)(B) 는 주목 위치 힌트다.
결함 코드 · C#
// ============================================================================
// [코드리뷰 문제] C# - 부동소수점 좌표 직렬화와 NaN/Inf·결정론 방어 (서버-서버 동기화)
// ----------------------------------------------------------------------------
// 시나리오 (프로토콜/직렬화):
// - 존 서버 A 가 엔티티 스냅샷(위치 x,y,z 등 float)을 직렬화해 존 서버 B(또는
// 관전/리플레이 서버)로 전송하고, B 는 역직렬화해 자기 시뮬레이션에 반영한다.
// - 송신 측은 클라이언트가 보고한 값에서 비롯된 좌표를 그대로 중계할 수도 있다.
// - float 비트 패턴에는 NaN/+Inf/-Inf, -0.0, 비정규(subnormal) 같은 특수값이 있다.
// - 서로 다른 런타임/플랫폼(엔디안 포함)을 쓰는 서버들이 같은 바이트를 주고받는다.
//
// 요구사항:
// - 수신한 float 좌표는 신뢰할 수 없다. NaN/Inf/범위초과 좌표가 시뮬레이션에
// 들어가 물리/판정/AoI 계산을 오염시키면 안 된다(NaN 은 비교가 모두 false 라
// 경계검사·정렬·충돌이 조용히 망가진다).
// - 직렬화 포맷은 송수신 서버 플랫폼이 달라도 동일하게 해석돼야 한다(엔디안 등).
// - 좌표는 맵 경계 등 유효 범위 안의 유한값이어야 한다.
//
// 과제:
// 이 코드의 잠재적 문제를 모두 찾고, 왜/어떻게 악용·오작동하는지 설명하고,
// 수정안과 더 나은 설계를 제시하라.
// (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================
using System;
using System.Collections.Generic;
public struct Vec3 { public float X, Y, Z; }
public struct EntitySnapshot
{
public long Id;
public Vec3 Pos;
public Vec3 Vel;
}
public class SnapshotCodec
{
// 직렬화: 스냅샷을 바이트 리스트에 append
public void Write(List<byte> outBuf, in EntitySnapshot s)
{
outBuf.AddRange(BitConverter.GetBytes(s.Id));
// (A) float 들을 BitConverter 로 그대로 직렬화
outBuf.AddRange(BitConverter.GetBytes(s.Pos.X));
outBuf.AddRange(BitConverter.GetBytes(s.Pos.Y));
outBuf.AddRange(BitConverter.GetBytes(s.Pos.Z));
outBuf.AddRange(BitConverter.GetBytes(s.Vel.X));
outBuf.AddRange(BitConverter.GetBytes(s.Vel.Y));
outBuf.AddRange(BitConverter.GetBytes(s.Vel.Z));
}
// 역직렬화: 버퍼에서 스냅샷 1개 읽기. 성공 시 true.
public bool Read(byte[] buf, int offset, out EntitySnapshot s)
{
s = default;
if (buf.Length - offset < 8 + 4 * 6)
return false;
int o = offset;
s.Id = BitConverter.ToInt64(buf, o); o += 8;
// (B) 바이트를 그대로 float 로 해석
s.Pos.X = BitConverter.ToSingle(buf, o); o += 4;
s.Pos.Y = BitConverter.ToSingle(buf, o); o += 4;
s.Pos.Z = BitConverter.ToSingle(buf, o); o += 4;
s.Vel.X = BitConverter.ToSingle(buf, o); o += 4;
s.Vel.Y = BitConverter.ToSingle(buf, o); o += 4;
s.Vel.Z = BitConverter.ToSingle(buf, o); o += 4;
// (C) 읽은 좌표를 그대로 사용 가능 상태로 반환
return true;
}
// 수신한 스냅샷을 시뮬레이션에 적용
public void Apply(ref EntitySnapshot dst, in EntitySnapshot recv)
{
dst.Pos = recv.Pos;
dst.Vel = recv.Vel;
// 이후 물리/AoI/충돌 계산이 dst.Pos 를 사용
}
} 결함 코드 · C++
// ============================================================================
// [코드리뷰 문제] C++ - 부동소수점 좌표 직렬화와 NaN/Inf·결정론 방어 (서버-서버 동기화)
// ----------------------------------------------------------------------------
// 시나리오 (프로토콜/직렬화):
// - 존 서버 A 가 엔티티 스냅샷(위치 x,y,z 등 float)을 직렬화해 존 서버 B(또는
// 관전/리플레이 서버)로 전송하고, B 는 이를 역직렬화해 자기 시뮬레이션에 반영한다.
// - 송신 측은 클라이언트가 보고한 값에서 비롯된 좌표를 그대로 중계할 수도 있다.
// - float 비트 패턴에는 NaN/+Inf/-Inf, -0.0, 비정규(subnormal) 같은 특수값이 있다.
// - 서로 다른 컴파일러/플랫폼/최적화 옵션을 쓰는 서버들이 같은 바이트를 주고받는다.
//
// 요구사항:
// - 수신한 float 좌표는 신뢰할 수 없다. NaN/Inf/범위초과 좌표가 시뮬레이션에
// 들어가 물리/판정/AoI 계산을 오염시키면 안 된다(NaN 은 비교가 모두 false 라
// 경계검사·정렬·충돌이 조용히 망가진다).
// - 직렬화 포맷은 송수신 서버의 플랫폼이 달라도 동일하게 해석돼야 한다(엔디안 등).
// - 좌표는 맵 경계 등 유효 범위 안의 유한값이어야 한다.
//
// 과제:
// 이 코드의 잠재적 문제를 모두 찾고, 왜/어떻게 악용·오작동하는지 설명하고,
// 수정안과 더 나은 설계를 제시하라.
// (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================
#include <cstdint>
#include <cstring>
#include <vector>
struct Vec3 { float x, y, z; };
struct EntitySnapshot {
int64_t entityId;
Vec3 pos;
Vec3 vel;
};
class SnapshotCodec {
public:
// 직렬화: 스냅샷을 바이트 버퍼에 append
void Write(std::vector<uint8_t>& out, const EntitySnapshot& s) {
AppendRaw(out, &s.entityId, sizeof(s.entityId));
// (A) float 들을 메모리 그대로 복사
AppendRaw(out, &s.pos, sizeof(s.pos));
AppendRaw(out, &s.vel, sizeof(s.vel));
}
// 역직렬화: 버퍼에서 스냅샷 1개 읽기. 성공 시 true.
bool Read(const uint8_t* buf, size_t len, EntitySnapshot& out) {
if (len < sizeof(int64_t) + sizeof(Vec3) * 2)
return false;
size_t off = 0;
std::memcpy(&out.entityId, buf + off, sizeof(out.entityId)); off += sizeof(out.entityId);
// (B) 바이트를 그대로 float 로 해석
std::memcpy(&out.pos, buf + off, sizeof(out.pos)); off += sizeof(out.pos);
std::memcpy(&out.vel, buf + off, sizeof(out.vel)); off += sizeof(out.vel);
// (C) 읽은 좌표를 그대로 사용 가능 상태로 반환
return true;
}
// 수신한 스냅샷을 시뮬레이션에 적용
void Apply(EntitySnapshot& dst, const EntitySnapshot& recv) {
dst.pos = recv.pos;
dst.vel = recv.vel;
// 이후 물리/AoI/충돌 계산이 dst.pos 를 사용
}
private:
void AppendRaw(std::vector<uint8_t>& out, const void* p, size_t n) {
const uint8_t* b = reinterpret_cast<const uint8_t*>(p);
out.insert(out.end(), b, b + n);
}
}; 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.