홈으로 돌아가기

C++에서의 Lua 샌드박스 메모리 제한

이 기사는 C++에서 사용자 정의 할당자를 사용한 Lua 샌드박스 메모리 제어 구현을 설명합니다. 아키텍처 특징, 코드 예제, 개발자를 위한 실전 권장 사항을 다룹니다.

Lua 메모리 제어: C++에서의 스크립트 격리
Advertisement 728x90

C++를 사용한 Lua 샌드박스에서 메모리와 자원 제한

Lua를 C++ 애플리케이션에 통합할 때 스크립트 실행을 메인 시스템으로부터 격리할 필요가 자주 생깁니다. 특히 게임 엔진, 플러그인 시스템, 샌드박스 환경처럼 타사 코드가 악성적이거나 불안정할 수 있는 상황에서 이는 매우 중요합니다. 이러한 격리의 핵심 요소 중 하나가 바로 메모리 소비 제어입니다. 이 글에서는 인터프리터 상태에 통합된 커스텀 할당자를 활용해 Lua에 엄격한 메모리 사용 제한을 구현하는 방법을 설명합니다.

Lua가 메모리를 어떻게 관리하는가

Lua는 모든 메모리 작업을 외부 할당자에 위임합니다. 기본적으로 표준 realloc/free를 사용하지만, 언어 사양상 이를 사용자 정의 구현으로 교체할 수 있습니다. 이는 lua_State를 생성할 때 해당 함수 포인터를 전달함으로써 가능합니다. 중요한 점은 모든 할당, 크기 변경, 해제 요청이 이 할당자를 거친다는 것입니다—가비지 컬렉터 작업, 모듈 로딩, 테이블·문자열·함수 등의 생성을 포함해요.

할당자 함수는 다음과 같은 시그니처를 가집니다:

Google AdInline article slot
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)를 발생시킵니다.

Google AdInline article slot

주요 구현 포인트는 다음과 같습니다:

  • newSize == 0일 때 — 메모리 해제, usedcurrSize만큼 감소;
  • 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의 인기 있는 래퍼)를 사용할 때 커스텀 할당자와 함께 상태를 생성하는 방법은 다음과 같습니다:

Google AdInline article slot
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

Advertisement 728x90

다음 읽기