C++를 사용한 Lua 샌드박스에서 메모리와 자원 제한
Lua를 C++ 애플리케이션에 통합할 때 스크립트 실행을 메인 시스템으로부터 격리할 필요가 자주 생깁니다. 특히 게임 엔진, 플러그인 시스템, 샌드박스 환경처럼 타사 코드가 악성적이거나 불안정할 수 있는 상황에서 이는 매우 중요합니다. 이러한 격리의 핵심 요소 중 하나가 바로 메모리 소비 제어입니다. 이 글에서는 인터프리터 상태에 통합된 커스텀 할당자를 활용해 Lua에 엄격한 메모리 사용 제한을 구현하는 방법을 설명합니다.
Lua가 메모리를 어떻게 관리하는가
Lua는 모든 메모리 작업을 외부 할당자에 위임합니다. 기본적으로 표준 realloc/free를 사용하지만, 언어 사양상 이를 사용자 정의 구현으로 교체할 수 있습니다. 이는 lua_State를 생성할 때 해당 함수 포인터를 전달함으로써 가능합니다. 중요한 점은 모든 할당, 크기 변경, 해제 요청이 이 할당자를 거친다는 것입니다—가비지 컬렉터 작업, 모듈 로딩, 테이블·문자열·함수 등의 생성을 포함해요.
할당자 함수는 다음과 같은 시그니처를 가집니다:
void *luaAlloc(void *ud, void *ptr, size_t currSize, size_t newSize);
ud— 사용자 데이터 (보통 메모리 컨트롤러 상태 포인터);ptr— 현재 블록 포인터 (새로 할당 시NULL일 수 있음);currSize— 현재 블록 크기 또는 객체 유형 (Lua 5.2+에서ptr == NULL일 때);newSize— 요청된 새 크기 (0이면 해제 의미).
이렇게 하면 메모리 할당에 대한 완전한 제어가 가능해지며, 사용량 추적과 제한이 쉬워집니다.
제한된 할당자 구현하기
할당된 메모리 양을 추적하기 위해 상태 구조체를 도입합니다:
struct LimitedAllocatorState {
size_t used {};
size_t limit {1 * 1024 * 1024}; // 1 MB by default
bool limitReached {false};
bool overflow {false};
bool isLimitEnabled() { return limit > 0; }
void disableLimit() { limit = 0; }
void resetErrorFlags() { limitReached = overflow = false; }
};
할당자는 새 요청(used - currSize + newSize)이 설정된 제한을 초과하는지 확인합니다. 초과 시 nullptr을 반환해 Lua에서 오류(예: not enough memory)를 발생시킵니다.
주요 구현 포인트는 다음과 같습니다:
newSize == 0일 때 — 메모리 해제,used가currSize만큼 감소;ptr == NULL일 때 — 새 블록 할당,currSize무시;- 정수 오버플로우 방지 필수;
- 할당자 상태는
lua_State보다 오래 살아야 함.
예제 함수:
void *limitedAlloc(void *ud, void *ptr, size_t currSize, size_t newSize) {
auto *allocState = static_cast<LimitedAllocatorState*>(ud);
if (!allocState) return nullptr;
if (ptr == nullptr) currSize = 0;
if (newSize == 0) {
if (ptr != nullptr) {
allocState->used = (allocState->used >= currSize)
? allocState->used - currSize : 0;
}
std::free(ptr);
return nullptr;
}
const size_t usedBase = (allocState->used >= currSize)
? allocState->used - currSize : 0;
if (newSize > SIZE_MAX - usedBase) {
allocState->overflow = true;
return nullptr;
}
const size_t newUsed = usedBase + newSize;
if (allocState->isLimitEnabled() && newUsed > allocState->limit) {
allocState->limitReached = true;
return nullptr;
}
void *newPtr = std::realloc(ptr, newSize);
if (newPtr) allocState->used = newUsed;
return newPtr;
}
C++ 런타임과의 통합
sol2 라이브러리(Lua C API의 인기 있는 래퍼)를 사용할 때 커스텀 할당자와 함께 상태를 생성하는 방법은 다음과 같습니다:
sol::state lua(sol::default_at_panic, limitedAlloc, &allocState);
클래스 필드 선언 순서를 지켜야 합니다: 할당자 상태는 sol::state 이후에 소멸되어야 하며, 그렇지 않으면 상태 소멸 시 이미 해제된 메모리에 접근합니다.
모든 로직을 관리하는 LuaRuntime 클래스로 감싸는 것을 권장합니다. 이 클래스는 다음을 제공합니다:
- 상태 생명주기 관리;
- 오류 재설정 및 제한 동적 변경 메서드;
- 쿼터 초과 여부 확인.
예제 인터페이스:
class LuaRuntime {
private:
lua::memory::LimitedAllocatorState allocatorState;
sol::state state;
public:
LuaRuntime(size_t memoryLimit)
: allocatorState({.limit = memoryLimit}),
state(sol::default_at_panic, lua::memory::limitedAlloc, &allocatorState) {}
bool hasAllocError() {
return allocatorState.limitReached || allocatorState.overflow;
}
void resetAllocErrors() {
allocatorState.resetErrorFlags();
}
bool setMemoryLimit(size_t limit) {
allocatorState.limit = limit;
return true;
}
};
제한 사항과 실무 팁
제한된 할당자를 사용할 때 유의할 주요 사항:
- 제한은 전체 상태에 공유됩니다. 하나의
lua_State에 여러 샌드박스를 사용하면 메모리 예산을 공유합니다. 엄격한 격리를 위해 샌드박스당 하나의 상태를 사용하세요. - 가비지 컬렉터도 메모리를 소비합니다. Lua에서 객체를 삭제한 후에도 GC 실행 중 일시적으로 사용량이 증가할 수 있습니다.
- 할당 오류가 항상 패닉을 유발하지 않습니다. Lua는 예외를 발생시키며, 이는
pcall이나 패닉 핸들러로 잡을 수 있습니다. - 오류 플래그 재설정을 잊지 마세요. 오류 처리 후
limitReached는 명시적 재설정 전까지 유지됩니다.
또한 긴 문자열 연결이나 깊은 재귀 같은 작업은 메모리 소비를 급증시킬 수 있습니다. 실제 워크로드로 테스트하는 것이 필수입니다.
주요 요점
- Lua는 커스텀 할당자를 통해 메모리 할당을 완전히 제어할 수 있습니다.
- 메모리 제한은 개별 스크립트가 아닌 전체 상태에 적용됩니다.
- 제한 초과 시 할당자가
nullptr을 반환해 Lua에서 오류를 발생시킵니다. - 할당자 상태는
lua_State보다 오래 살아야 합니다. - 정수 오버플로우 방지와 정확한 산술 연산이 안정적인 구현의 핵심입니다.
— Editorial Team
아직 댓글이 없습니다.