# 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:
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 serNULLal asignar nuevo);currSize— tamaño actual del bloque o tipo de objeto (en Lua 5.2+ cuandoptr == 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).
Estos son los puntos clave de implementación:
- Cuando
newSize == 0— se libera la memoria yuseddisminuye encurrSize; - Cuando
ptr == NULL— se asigna un nuevo bloque,currSizese 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í:
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
pcallo un manejador de pánico. - No olvides restablecer las banderas de error. Después de manejar un error,
limitReachedpermanece 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
Aún no hay comentarios.