Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 162 additions & 18 deletions c_src/py_nif.c
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
#include "py_nif.h"
#include "py_asgi.h"
#include "py_wsgi.h"
#include "py_sandbox.h"

/* ============================================================================
* Global state definitions
Expand Down Expand Up @@ -138,6 +139,7 @@ static ERL_NIF_TERM build_suspended_result(ErlNifEnv *env, suspended_state_t *su
#include "py_event_loop.c"
#include "py_asgi.c"
#include "py_wsgi.c"
#include "py_sandbox.c"

/* ============================================================================
* Resource callbacks
Expand All @@ -155,6 +157,20 @@ static void worker_destructor(ErlNifEnv *env, void *obj) {
close(worker->callback_pipe[1]);
}

/* Restore builtins if this worker used disable_builtins */
if (worker->sandbox != NULL && worker->sandbox->disable_builtins &&
worker->globals != NULL && g_python_initialized) {
PyGILState_STATE gstate = PyGILState_Ensure();
sandbox_restore_builtins(worker->globals);
PyGILState_Release(gstate);
}

/* Clean up sandbox policy */
if (worker->sandbox != NULL) {
sandbox_policy_destroy(worker->sandbox);
worker->sandbox = NULL;
}

/* Only clean up Python state if Python is still initialized */
if (worker->thread_state != NULL && g_python_initialized) {
PyEval_RestoreThread(worker->thread_state);
Expand Down Expand Up @@ -415,6 +431,13 @@ static ERL_NIF_TERM nif_py_init(ErlNifEnv *env, int argc, const ERL_NIF_TERM arg
/* Detect execution mode based on Python version and build */
detect_execution_mode();

/* Initialize sandbox system (audit hooks) */
if (init_sandbox_system() < 0) {
Py_Finalize();
g_python_initialized = false;
return make_error(env, "sandbox_init_failed");
}

/* Save main thread state and release GIL for other threads */
g_main_thread_state = PyEval_SaveThread();

Expand Down Expand Up @@ -476,21 +499,25 @@ static ERL_NIF_TERM nif_finalize(ErlNifEnv *env, int argc, const ERL_NIF_TERM ar
return ATOM_OK;
}

/* Clean up thread worker system */
thread_worker_cleanup();

/* Clean up ASGI and WSGI scope key caches */
PyGILState_STATE gstate = PyGILState_Ensure();
asgi_scope_cleanup();
wsgi_scope_cleanup();

/* Clean up numpy type cache */
Py_XDECREF(g_numpy_ndarray_type);
g_numpy_ndarray_type = NULL;
/* IMPORTANT: Since Py_Finalize() is disabled (see below), we must NOT
* reset global state flags or cleanup Python-side resources. The Python
* interpreter persists for the lifetime of the Erlang process, so:
*
* 1. g_python_initialized remains false to allow "re-init" to work
* 2. But ASGI/WSGI scope state, sandbox state, etc. remain valid
* 3. On "re-init", the existing state is reused via the _initialized flags
*
* This approach allows test suites to stop/start the erlang_python application
* without corrupting the Python interpreter state.
*
* NOTE: Executor threads and thread workers ARE cleaned up since they are
* Erlang/C-side resources that need to be recreated on restart.
*/

PyGILState_Release(gstate);
/* Clean up thread worker system - these are C-side resources */
thread_worker_cleanup();

/* Stop executors based on mode */
/* Stop executors based on mode - these are C-side threads */
switch (g_execution_mode) {
case PY_MODE_FREE_THREADED:
/* No executor to stop */
Expand All @@ -510,7 +537,7 @@ static ERL_NIF_TERM nif_finalize(ErlNifEnv *env, int argc, const ERL_NIF_TERM ar
break;
}

/* Restore main thread state before finalizing */
/* Restore main thread state before "finalizing" */
if (g_main_thread_state != NULL) {
PyEval_RestoreThread(g_main_thread_state);
g_main_thread_state = NULL;
Expand All @@ -521,9 +548,26 @@ static ERL_NIF_TERM nif_finalize(ErlNifEnv *env, int argc, const ERL_NIF_TERM ar
* The process will clean up resources on exit, so we skip finalization.
*
* Note: If explicit cleanup is needed in the future, consider using
* Py_FinalizeEx() or manually clearing atexit handlers before finalize. */
* Py_FinalizeEx() or manually clearing atexit handlers before finalize.
*
* IMPORTANT: Do NOT call cleanup functions for Python-side resources here:
* - cleanup_sandbox_system() - audit hook must persist
* - asgi_scope_cleanup() - interned strings must persist
* - wsgi_scope_cleanup() - interned strings must persist
* - Py_XDECREF(g_numpy_ndarray_type) - cached type must persist
*
* These resources remain valid because Py_Finalize() is not called.
*/
#if 0
Py_Finalize();
/* Only if Py_Finalize() is re-enabled, uncomment these cleanups: */
PyGILState_STATE gstate = PyGILState_Ensure();
asgi_scope_cleanup();
wsgi_scope_cleanup();
Py_XDECREF(g_numpy_ndarray_type);
g_numpy_ndarray_type = NULL;
cleanup_sandbox_system();
PyGILState_Release(gstate);
#endif
g_python_initialized = false;

Expand All @@ -535,9 +579,6 @@ static ERL_NIF_TERM nif_finalize(ErlNifEnv *env, int argc, const ERL_NIF_TERM ar
* ============================================================================ */

static ERL_NIF_TERM nif_worker_new(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
(void)argc;
(void)argv;

if (!g_python_initialized) {
return make_error(env, "python_not_initialized");
}
Expand All @@ -547,6 +588,28 @@ static ERL_NIF_TERM nif_worker_new(ErlNifEnv *env, int argc, const ERL_NIF_TERM
return make_error(env, "alloc_failed");
}

/* Initialize sandbox to NULL */
worker->sandbox = NULL;

/* Parse options if provided */
if (argc > 0 && enif_is_map(env, argv[0])) {
ERL_NIF_TERM sandbox_key = enif_make_atom(env, "sandbox");
ERL_NIF_TERM sandbox_opts;
if (enif_get_map_value(env, argv[0], sandbox_key, &sandbox_opts)) {
/* Create and configure sandbox policy */
worker->sandbox = sandbox_policy_new();
if (worker->sandbox == NULL) {
enif_release_resource(worker);
return make_error(env, "sandbox_alloc_failed");
}
if (parse_sandbox_options(env, sandbox_opts, worker->sandbox) < 0) {
sandbox_policy_destroy(worker->sandbox);
enif_release_resource(worker);
return make_error(env, "invalid_sandbox_options");
}
}
}

/* Acquire GIL to create thread state */
PyGILState_STATE gstate = PyGILState_Ensure();

Expand All @@ -562,6 +625,11 @@ static ERL_NIF_TERM nif_worker_new(ErlNifEnv *env, int argc, const ERL_NIF_TERM
PyObject *builtins = PyEval_GetBuiltins();
PyDict_SetItemString(worker->globals, "__builtins__", builtins);

/* Apply builtin restrictions if configured */
if (worker->sandbox != NULL && worker->sandbox->disable_builtins) {
sandbox_apply_builtin_restrictions(worker->globals);
}

/* Import erlang module into worker's namespace for callbacks */
PyObject *erlang_module = PyImport_ImportModule("erlang");
if (erlang_module != NULL) {
Expand Down Expand Up @@ -993,6 +1061,77 @@ static ERL_NIF_TERM nif_send_callback_response(ErlNifEnv *env, int argc, const E
return ATOM_OK;
}

/* ============================================================================
* Sandbox control NIFs
* ============================================================================ */

static ERL_NIF_TERM nif_sandbox_set_policy(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
(void)argc;
py_worker_t *worker;

if (!enif_get_resource(env, argv[0], WORKER_RESOURCE_TYPE, (void **)&worker)) {
return make_error(env, "invalid_worker");
}

/* Create policy if it doesn't exist */
if (worker->sandbox == NULL) {
worker->sandbox = sandbox_policy_new();
if (worker->sandbox == NULL) {
return make_error(env, "sandbox_alloc_failed");
}
}

/* Update policy with new options */
if (sandbox_policy_update(env, worker->sandbox, argv[1]) < 0) {
return make_error(env, "invalid_policy");
}

return ATOM_OK;
}

static ERL_NIF_TERM nif_sandbox_enable(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
(void)argc;
py_worker_t *worker;

if (!enif_get_resource(env, argv[0], WORKER_RESOURCE_TYPE, (void **)&worker)) {
return make_error(env, "invalid_worker");
}

char enabled[16];
if (!enif_get_atom(env, argv[1], enabled, sizeof(enabled), ERL_NIF_LATIN1)) {
return make_error(env, "invalid_enabled");
}

bool enable = (strcmp(enabled, "true") == 0);

if (worker->sandbox == NULL) {
if (!enable) {
/* Already disabled - no sandbox exists */
return ATOM_OK;
}
/* Need to enable but no sandbox - create one with empty policy */
worker->sandbox = sandbox_policy_new();
if (worker->sandbox == NULL) {
return make_error(env, "sandbox_alloc_failed");
}
}

sandbox_policy_set_enabled(worker->sandbox, enable);
return ATOM_OK;
}

static ERL_NIF_TERM nif_sandbox_get_policy(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
(void)argc;
py_worker_t *worker;

if (!enif_get_resource(env, argv[0], WORKER_RESOURCE_TYPE, (void **)&worker)) {
return make_error(env, "invalid_worker");
}

ERL_NIF_TERM policy_map = sandbox_policy_to_term(env, worker->sandbox);
return enif_make_tuple2(env, ATOM_OK, policy_map);
}

/* ============================================================================
* Async worker NIFs
* ============================================================================ */
Expand Down Expand Up @@ -1827,6 +1966,11 @@ static ErlNifFunc nif_funcs[] = {
{"send_callback_response", 2, nif_send_callback_response, 0},
{"resume_callback", 2, nif_resume_callback, 0},

/* Sandbox support */
{"sandbox_set_policy", 2, nif_sandbox_set_policy, 0},
{"sandbox_enable", 2, nif_sandbox_enable, 0},
{"sandbox_get_policy", 1, nif_sandbox_get_policy, 0},

/* Async worker management */
{"async_worker_new", 0, nif_async_worker_new, 0},
{"async_worker_destroy", 1, nif_async_worker_destroy, 0},
Expand Down
6 changes: 6 additions & 0 deletions c_src/py_nif.h
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@
#include <sys/select.h>
/** @} */

/* Forward declaration for sandbox policy */
typedef struct sandbox_policy_t sandbox_policy_t;

/* ============================================================================
* Feature Detection Macros
* ============================================================================ */
Expand Down Expand Up @@ -239,6 +242,9 @@ typedef struct {

/** @brief Environment for building callback messages */
ErlNifEnv *callback_env;

/** @brief Sandbox policy (NULL if no sandboxing) */
sandbox_policy_t *sandbox;
} py_worker_t;

/**
Expand Down
Loading
Loading