21. 패킹된 비트플래그/필드 언패킹
난이도 중해설 — 패킹된 비트플래그/필드 언패킹
난이도: 중
답변 프레임워크: 요약 → 문제 분류 → 원인 → 수정안 → 더 나은 설계
요약
디코더가 비트 폭·부호·예약 비트·신뢰 경계를 모두 잘못 다룬다.
(A) 두 바이트를 short(부호 있는 16비트) 로 결합한다. 최상위 비트(bit15)가 켜져 있으면
packed 가 음수가 되고, 이어지는 packed >> 3 은 산술 시프트(부호 확장) 가 되어
상위 필드 추출이 전부 어긋난다. (B) slotIndex 는 4비트(0..15)인데 마스크가 0x1F(5비트)
라 0..31 까지 나온다 → 장비 배열(크기 16) 범위를 넘는다. (C) ServerTrusted 를
클라가 보낸 패킷 비트에서 읽는다 — 클라가 bit8 을 세우면 서버 검증을 건너뛰게 만들 수
있다(서버 권위 위반). 게다가 reserved 비트(8..15)를 검증하지 않는다. (D) Apply 는
ServerTrusted 면 범위 검사를 건너뛰고 _equip[SlotIndex] 에 접근 → IndexOutOfRange/
엉뚱한 슬롯. 정답 한 줄: 부호 없는 타입으로 결합하고, 정확한 폭의 마스크로 추출하고,
reserved 비트를 검증하고, 권한성 표식은 절대 와이어에서 읽지 마라.
변별: problem4(패킷 검증: 길이/시퀀스/페이로드), problem7(엔디안/정렬)과 달리, 본 문제는 비트 단위 언패킹의 부호 확장·마스크 폭·예약 비트·신뢰 경계 가 핵심이다.
문제점
(A) short 로 결합 → 부호 확장 (직렬화/정수 의미) ★간판
- 분류 태그: signed integer / sign extension.
- 증상: bit15=1 이면
packed < 0. C# 에서 시프트는 피연산자를int로 승격하는데 음수short는 음수int로 부호 확장되고,>>는 산술 시프트라 상위 비트가 1로 채워져SlotIndex/reserved추출이 깨진다. - 재현 조건: 첫 바이트의 최상위 비트(=bit15)가 켜진 입력.
- 근본 원인: 비트 패킹은 부호 없는 타입(
ushort/uint)으로 다뤄야 한다.
(B) 과대 마스크 0x1F — 필드 폭 초과 (검증/경계) ★간판
- 분류 태그: incorrect bitmask / out-of-range field.
- 증상: 4비트 필드를 5비트로 추출해
SlotIndex가 16~31 이 될 수 있다. - 근본 원인: 마스크 폭이 필드 폭과 불일치. (bits 3..6 →
(packed >> 3) & 0xF)
(C) 신뢰 표식을 와이어에서 읽음 + reserved 미검증 (서버 권위/검증) ★간판
- 분류 태그: client-controlled trust / missing reserved-bit validation.
- 증상: 클라가 bit8 을 세우면
ServerTrusted=true가 되어Apply의 범위 검사를 건너뛴다. reserved(8..15)를 검사하지 않으니 "정의되지 않은 비트"가 그대로 동작에 반영. - 근본 원인: 권한·신뢰는 서버 내부 상태다. 클라 입력의 어떤 비트도 검증을 면제하면 안 됨.
(D) 검증 우회 후 배열 접근 — OOB (메모리/안정성)
- 분류 태그: out-of-bounds access.
- 증상:
Apply의 범위 검사는ServerTrusted면 건너뛴다. 클라가 신뢰 비트(C)를 세우면SlotIndex가 (B)의 과대 마스크로 16~31 까지 나온 채_equip[SlotIndex](크기 16)에 직접 접근 → 예외/크래시(OOB). - 근본 원인: 신뢰 비트로 검증을 건너뛴 것 + 과대 마스크의 조합.
수정안 (정확한 코드)
public ActionPacket Decode(byte[] buf, int offset)
{
if (buf == null || offset < 0 || offset + 2 > buf.Length)
throw new ArgumentException("buffer too short");
// 부호 없는 타입으로 결합
ushort packed = (ushort)((buf[offset] << 8) | buf[offset + 1]);
// reserved(8..15) 는 0이어야 한다
if ((packed & 0xFF00) != 0)
throw new InvalidOperationException("reserved bits set");
var p = new ActionPacket
{
ActionType = packed & 0x7,
SlotIndex = (packed >> 3) & 0xF, // 정확히 4비트
Urgent = ((packed >> 7) & 1) != 0,
ServerTrusted = false // 와이어에서 읽지 않는다
};
// 의미 범위 검증(필요 시 actionType 화이트리스트도)
if (p.SlotIndex >= 16) throw new InvalidOperationException("slot out of range");
return p;
}
public void Apply(ActionPacket p)
{
// 신뢰 여부와 무관하게 항상 범위 검사
if ((uint)p.SlotIndex >= (uint)_equip.Length) return;
var name = _equip[p.SlotIndex];
_ = name;
}
핵심: 부호 없는 결합 + 정확한 폭 마스크 + reserved 검증 + 신뢰 비트 제거 + 항상 범위 검사.
더 나은 설계 (+ 트레이드오프)
- 결합·추출을 헬퍼로 캡슐화:
BinaryPrimitives.ReadUInt16BigEndian으로 엔디안/부호를 한 곳에서 처리하고, 필드별(shift,width)를 상수/제너레이터로 정의해 마스크 실수를 차단. - reserved 정책 명문화: "must be zero" 를 강제(거부)할지 "must ignore"(무시) 할지 프로토콜에 못 박는다. 게임 클라/서버 버전이 다를 수 있으면 보통 무시가 안전하지만, 보안 경계 필드는 거부가 낫다(트레이드오프: 하위호환 vs 엄격성).
- 권한/신뢰는 세션 상태로: "이 요청이 신뢰됐는가"는 세션 인증·서버 내부 플래그로만 결정. 와이어에는 권한성 비트를 아예 두지 않는다.
- 퍼징/속성 테스트: 16비트 전수(0..65535) 또는 랜덤 입력으로 디코더가 절대 OOB/예외 전파/음수 인덱스를 만들지 않음을 검증.
면접 포인트
- C# 시프트의 함정 두 가지 — ① 피연산자가
int미만이면int로 승격(부호 확장 주의), ② 시프트 카운트는int면& 31,long이면& 63로 마스킹된다(x >> 32 == x). - 비트 필드는 항상 부호 없는 타입으로. 마스크 폭은 필드 폭과 정확히 일치시킨다.
- "검증을 건너뛰게 하는 비트"를 클라가 제어하게 두지 마라 — 신뢰·권한은 서버 권위. reserved 비트 정책(거부/무시)도 프로토콜에 명시.
해설 — 패킹된 비트플래그/필드 언패킹 (C++)
난이도: 중
답변 프레임워크: 요약 → 문제 분류 → 원인 → 수정안 → 더 나은 설계
요약
C++ 의 비트 조작은 부호·승격·시프트 규칙 때문에 함정이 더 많다.
(A) 입력을 const char* 로 읽는데 char 는 플랫폼에 따라 부호 있음이다. 바이트가
0x80 이상이면 buf[offset] 가 음수 int 로 부호 확장되고, 음수 << 8 은 정의되지 않은
동작(UB)(음수 좌시프트)이다. 이어 int16_t packed 로 캐스트하면 음수가 되고
(B) packed >> 3 은 음수의 우시프트(부호 채움)라 상위 필드 추출이 깨진다. 또 마스크가
0x1F(5비트)라 4비트 필드 slotIndex 가 0..31 이 된다. (C) serverTrusted 를 와이어 비트에서
읽고 reserved(8..15)를 검증하지 않는다. (D) 신뢰 비트로 범위 검사를 건너뛰면
equip_[slotIndex](16칸)에 OOB 접근. 정답 한 줄: 입력은 unsigned char/uint16_t 로
다루고, 좌·우시프트 전에 부호를 없애고, 정확한 폭 마스크 + reserved 검증 + 신뢰 비트 제거.
변별: C# 트윈이 "부호 확장·마스크·신뢰"라면, C++ 트윈은 거기에
char부호성과 음수 좌시프트 UB 라는 언어 고유 함정이 더해진다.
문제점
(A) const char* + 음수 좌시프트 UB (직렬화/정수 의미·UB) ★간판
- 분류 태그: signed-char sign extension / left-shift of negative (UB).
- 증상:
char가 부호 있음인 구현에서 0x80~0xFF 바이트는 음수로 부호 확장된다.(음수 << 8)은 표준상 UB. 결과는 컴파일러/최적화에 따라 달라진다. - 재현 조건: 첫 바이트 ≥ 0x80.
- 근본 원인: 바이트는
unsigned char/uint8_t로 읽어야 한다.
(B) int16_t + 과대 마스크 — 부호 우시프트 & 폭 초과 (정수/검증) ★간판
- 분류 태그: arithmetic right shift on signed / incorrect bitmask.
- 증상: 음수
packed의>> 3은 부호 비트가 채워져 상위 필드가 오염.& 0x1F는 4비트 필드를 5비트로 추출해slotIndex0..31. - 근본 원인: 부호 없는 타입 + 정확한 폭(
& 0xF) 필요.
(C) 신뢰 비트를 와이어에서 + reserved 미검증 (서버 권위/검증) ★간판
- 분류 태그: client-controlled trust / missing reserved validation.
- 증상: 클라가 bit8 을 세우면 검증 우회. reserved 비트가 동작에 새어든다.
- 근본 원인: 권한·신뢰는 서버 내부 상태. 클라 비트로 검증 면제 불가.
(D) 검증 우회 후 배열 접근 — OOB (메모리)
- 분류 태그: out-of-bounds access.
- 증상: (B)(C) 결합 시
equip_[16..31]접근 → UB/크래시.
수정안 (정확한 코드)
#include <cstdint>
#include <cstddef>
#include <stdexcept>
ActionPacket Decode(const std::uint8_t* buf, std::size_t len, std::size_t offset)
{
if (!buf || offset + 2 > len)
throw std::runtime_error("buffer too short");
// 부호 없는 바이트로 결합(빅엔디안)
std::uint16_t packed =
static_cast<std::uint16_t>((static_cast<std::uint16_t>(buf[offset]) << 8)
| buf[offset + 1]);
if ((packed & 0xFF00u) != 0) // reserved 검증
throw std::runtime_error("reserved bits set");
ActionPacket p{};
p.actionType = packed & 0x7u;
p.slotIndex = (packed >> 3) & 0xFu; // 정확히 4비트
p.urgent = ((packed >> 7) & 1u) != 0;
p.serverTrusted = false; // 와이어에서 읽지 않음
if (p.slotIndex >= 16) throw std::runtime_error("slot out of range");
return p;
}
void Apply(const ActionPacket& p)
{
if (p.slotIndex < 0 || p.slotIndex >= 16) return; // 항상 검사
const std::string& name = equip_[p.slotIndex];
(void)name;
}
핵심: uint8_t/uint16_t 로 읽고 결합 → 부호 확장·좌시프트 UB 제거, 정확한 폭 마스크,
reserved 검증, 신뢰 비트 제거, 무조건 범위 검사.
더 나은 설계 (+ 트레이드오프)
- 엔디안/결합 헬퍼 캡슐화:
load_be16(const uint8_t*)같은 함수로 한 곳에서 처리. C++20 이면std::endian/std::byteswap활용. - 시프트 폭 안전성: 시프트량이 데이터에서 오면 타입 폭 이상 시프트는 UB(
x >> 32,1 << 32). 카운트를 항상 검증/클램프. (이 코드엔 없지만 흔한 자매 버그.) - 비트필드 정의를 한 곳에:
(shift,width)상수 또는constexpr추출 함수로 마스크 실수 차단.1u << width - 1처럼 우선순위 실수도 주의((1u << width) - 1). - 퍼징: 0..65535 전수 디코딩으로 UB/OOB/예외 전파 없음을 검증(ASan/UBSan 동반).
면접 포인트
char의 부호성은 구현 정의. 바이트 단위 파싱은unsigned char/uint8_t로. 음수 좌시프트는 UB.- 시프트 규칙: 타입 폭 이상 시프트(
x >> 32on 32-bit)는 UB. 카운트가 데이터면 검증. - 비트 추출은 부호 없는 타입 + 정확한 폭 마스크. 권한·신뢰는 와이어가 아니라 서버 상태.