Volver al inicio

Límite de memoria en sandbox de Lua en C++

El artículo describe la implementación del control de memoria en sandbox de Lua en C++ usando un asignador personalizado. Cubre características arquitectónicas, ejemplos de código y recomendaciones prácticas para desarrolladores.

Control de memoria en Lua: aislamiento de scripts en C++
Advertisement 728x90

# Limitación de memoria y recursos en un sandbox de Lua con C++

Al integrar Lua en aplicaciones de C++, a menudo surge la necesidad de aislar la ejecución de scripts del sistema principal. Esto es especialmente crítico en escenarios donde el código de terceros podría ser malicioso o inestable, por ejemplo, en motores de juegos, sistemas de plugins o entornos sandbox. Un aspecto clave de dicho aislamiento es el control del consumo de memoria. Este artículo explica cómo implementar un límite estricto de uso de memoria en Lua mediante un asignador personalizado integrado en el estado del intérprete.

Cómo gestiona la memoria Lua

Lua delega todas las operaciones de memoria a un asignador externo. Por defecto, utiliza el realloc/free estándar, pero el lenguaje permite reemplazarlo por tu propia implementación. Esto se logra pasando un puntero a la función al crear lua_State. Importante: todas las solicitudes de asignación, redimensionamiento y liberación pasan por este asignador, incluidas las operaciones del recolector de basura, la carga de módulos y la creación de tablas, cadenas, funciones, etc.

La función asignadora tiene la siguiente signatura:

Google AdInline article slot
void *luaAlloc(void *ud, void *ptr, size_t currSize, size_t newSize);
  • ud — datos de usuario (típicamente un puntero al estado del controlador de memoria);
  • ptr — puntero al bloque actual (puede ser NULL al asignar nuevo);
  • currSize — tamaño actual del bloque o tipo de objeto (en Lua 5.2+ cuando ptr == NULL);
  • newSize — nuevo tamaño solicitado (0 significa liberar).

Esto proporciona control total sobre la asignación de memoria, lo que permite rastrear y limitar su uso.

Implementación de un asignador limitado

Para rastrear la cantidad de memoria asignada, introducimos una estructura de estado:

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; }
};

El asignador verifica si la nueva solicitud (used - currSize + newSize) excede el límite establecido. Si es así, devuelve nullptr, lo que provoca un error en Lua (por ejemplo, not enough memory).

Google AdInline article slot

Estos son los puntos clave de implementación:

  • Cuando newSize == 0 — se libera la memoria y used disminuye en currSize;
  • Cuando ptr == NULL — se asigna un nuevo bloque, currSize se ignora;
  • La protección contra desbordamiento de enteros es obligatoria;
  • El estado del asignador debe sobrevivir al lua_State.

Ejemplo de función:

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;
}

Integración con el runtime de C++

Al usar la biblioteca sol2 (un envoltorio popular sobre la API C de Lua), crear un estado con un asignador personalizado se ve así:

Google AdInline article slot
sol::state lua(sol::default_at_panic, limitedAlloc, &allocState);

Es importante seguir el orden de declaración de campos en la clase: el estado del asignador debe destruirse después de sol::state; de lo contrario, la destrucción del estado accederá a memoria ya liberada.

Se recomienda envolver toda la lógica en una clase LuaRuntime que:

  • Gestiona el ciclo de vida del estado;
  • Proporciona métodos para restablecer errores y cambiar el límite sobre la marcha;
  • Permite verificar si se ha excedido la cuota.

Ejemplo de interfaz:

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;
    }
};

Limitaciones y recomendaciones prácticas

Varias notas importantes al trabajar con un asignador limitado:

  • El límite se comparte en todo el estado. Si usas múltiples sandboxes en un lua_State, comparten un único presupuesto de memoria. Para un aislamiento estricto, usa un estado por sandbox.
  • El recolector de basura también consume memoria. Incluso después de eliminar objetos en Lua, el uso de memoria puede aumentar temporalmente durante las ejecuciones de GC.
  • Los errores de asignación no siempre provocan un pánico. Lua genera una excepción, que se puede capturar mediante pcall o un manejador de pánico.
  • No olvides restablecer las banderas de error. Después de manejar un error, limitReached permanece establecido hasta que se restablezca explícitamente.

También ten en cuenta que ciertas operaciones (por ejemplo, concatenar cadenas largas o recursión profunda) pueden causar picos bruscos en el consumo de memoria. Probar con cargas de trabajo realistas es esencial.

Resumen de puntos clave

  • Lua permite control total sobre la asignación de memoria mediante un asignador personalizado.
  • El límite de memoria se aplica a todo el estado, no a scripts individuales.
  • Cuando se excede el límite, el asignador devuelve nullptr, lo que provoca un error en Lua.
  • El estado del asignador debe sobrevivir al lua_State.
  • La protección contra desbordamiento de enteros y la aritmética correcta son esenciales para una implementación confiable.

— Editorial Team

Advertisement 728x90

Leer después