Skip to content

Commit ee89b19

Browse files
committed
Add audit hook-based sandbox for Python workers
Implement sandboxing for Python workers using Python's audit hook mechanism (PEP 578). This allows per-worker configuration of blocked operations: - file_write: Blocks open() with write/append/create modes - file_read: Blocks all file read operations - subprocess: Blocks subprocess.Popen, os.system, os.exec*, os.spawn* - network: Blocks socket.* operations - ctypes: Blocks ctypes module (memory access) - import: Blocks non-whitelisted imports - exec: Blocks compile(), exec(), eval() Features: - Strict preset (blocks subprocess, network, ctypes, file_write) - Per-worker sandbox policy with dynamic enable/disable - Import whitelist support - Thread-safe policy updates via atomic flags and mutex - High-level py:call_sandboxed/4,5 API - Documentation in docs/sandbox.md
1 parent 84ced63 commit ee89b19

File tree

8 files changed

+1653
-4
lines changed

8 files changed

+1653
-4
lines changed

c_src/py_nif.c

Lines changed: 113 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
#include "py_nif.h"
4040
#include "py_asgi.h"
4141
#include "py_wsgi.h"
42+
#include "py_sandbox.h"
4243

4344
/* ============================================================================
4445
* Global state definitions
@@ -138,6 +139,7 @@ static ERL_NIF_TERM build_suspended_result(ErlNifEnv *env, suspended_state_t *su
138139
#include "py_event_loop.c"
139140
#include "py_asgi.c"
140141
#include "py_wsgi.c"
142+
#include "py_sandbox.c"
141143

142144
/* ============================================================================
143145
* Resource callbacks
@@ -155,6 +157,12 @@ static void worker_destructor(ErlNifEnv *env, void *obj) {
155157
close(worker->callback_pipe[1]);
156158
}
157159

160+
/* Clean up sandbox policy */
161+
if (worker->sandbox != NULL) {
162+
sandbox_policy_destroy(worker->sandbox);
163+
worker->sandbox = NULL;
164+
}
165+
158166
/* Only clean up Python state if Python is still initialized */
159167
if (worker->thread_state != NULL && g_python_initialized) {
160168
PyEval_RestoreThread(worker->thread_state);
@@ -415,6 +423,13 @@ static ERL_NIF_TERM nif_py_init(ErlNifEnv *env, int argc, const ERL_NIF_TERM arg
415423
/* Detect execution mode based on Python version and build */
416424
detect_execution_mode();
417425

426+
/* Initialize sandbox system (audit hooks) */
427+
if (init_sandbox_system() < 0) {
428+
Py_Finalize();
429+
g_python_initialized = false;
430+
return make_error(env, "sandbox_init_failed");
431+
}
432+
418433
/* Save main thread state and release GIL for other threads */
419434
g_main_thread_state = PyEval_SaveThread();
420435

@@ -535,9 +550,6 @@ static ERL_NIF_TERM nif_finalize(ErlNifEnv *env, int argc, const ERL_NIF_TERM ar
535550
* ============================================================================ */
536551

537552
static ERL_NIF_TERM nif_worker_new(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
538-
(void)argc;
539-
(void)argv;
540-
541553
if (!g_python_initialized) {
542554
return make_error(env, "python_not_initialized");
543555
}
@@ -547,6 +559,28 @@ static ERL_NIF_TERM nif_worker_new(ErlNifEnv *env, int argc, const ERL_NIF_TERM
547559
return make_error(env, "alloc_failed");
548560
}
549561

562+
/* Initialize sandbox to NULL */
563+
worker->sandbox = NULL;
564+
565+
/* Parse options if provided */
566+
if (argc > 0 && enif_is_map(env, argv[0])) {
567+
ERL_NIF_TERM sandbox_key = enif_make_atom(env, "sandbox");
568+
ERL_NIF_TERM sandbox_opts;
569+
if (enif_get_map_value(env, argv[0], sandbox_key, &sandbox_opts)) {
570+
/* Create and configure sandbox policy */
571+
worker->sandbox = sandbox_policy_new();
572+
if (worker->sandbox == NULL) {
573+
enif_release_resource(worker);
574+
return make_error(env, "sandbox_alloc_failed");
575+
}
576+
if (parse_sandbox_options(env, sandbox_opts, worker->sandbox) < 0) {
577+
sandbox_policy_destroy(worker->sandbox);
578+
enif_release_resource(worker);
579+
return make_error(env, "invalid_sandbox_options");
580+
}
581+
}
582+
}
583+
550584
/* Acquire GIL to create thread state */
551585
PyGILState_STATE gstate = PyGILState_Ensure();
552586

@@ -993,6 +1027,77 @@ static ERL_NIF_TERM nif_send_callback_response(ErlNifEnv *env, int argc, const E
9931027
return ATOM_OK;
9941028
}
9951029

1030+
/* ============================================================================
1031+
* Sandbox control NIFs
1032+
* ============================================================================ */
1033+
1034+
static ERL_NIF_TERM nif_sandbox_set_policy(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
1035+
(void)argc;
1036+
py_worker_t *worker;
1037+
1038+
if (!enif_get_resource(env, argv[0], WORKER_RESOURCE_TYPE, (void **)&worker)) {
1039+
return make_error(env, "invalid_worker");
1040+
}
1041+
1042+
/* Create policy if it doesn't exist */
1043+
if (worker->sandbox == NULL) {
1044+
worker->sandbox = sandbox_policy_new();
1045+
if (worker->sandbox == NULL) {
1046+
return make_error(env, "sandbox_alloc_failed");
1047+
}
1048+
}
1049+
1050+
/* Update policy with new options */
1051+
if (sandbox_policy_update(env, worker->sandbox, argv[1]) < 0) {
1052+
return make_error(env, "invalid_policy");
1053+
}
1054+
1055+
return ATOM_OK;
1056+
}
1057+
1058+
static ERL_NIF_TERM nif_sandbox_enable(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
1059+
(void)argc;
1060+
py_worker_t *worker;
1061+
1062+
if (!enif_get_resource(env, argv[0], WORKER_RESOURCE_TYPE, (void **)&worker)) {
1063+
return make_error(env, "invalid_worker");
1064+
}
1065+
1066+
char enabled[16];
1067+
if (!enif_get_atom(env, argv[1], enabled, sizeof(enabled), ERL_NIF_LATIN1)) {
1068+
return make_error(env, "invalid_enabled");
1069+
}
1070+
1071+
bool enable = (strcmp(enabled, "true") == 0);
1072+
1073+
if (worker->sandbox == NULL) {
1074+
if (!enable) {
1075+
/* Already disabled - no sandbox exists */
1076+
return ATOM_OK;
1077+
}
1078+
/* Need to enable but no sandbox - create one with empty policy */
1079+
worker->sandbox = sandbox_policy_new();
1080+
if (worker->sandbox == NULL) {
1081+
return make_error(env, "sandbox_alloc_failed");
1082+
}
1083+
}
1084+
1085+
sandbox_policy_set_enabled(worker->sandbox, enable);
1086+
return ATOM_OK;
1087+
}
1088+
1089+
static ERL_NIF_TERM nif_sandbox_get_policy(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
1090+
(void)argc;
1091+
py_worker_t *worker;
1092+
1093+
if (!enif_get_resource(env, argv[0], WORKER_RESOURCE_TYPE, (void **)&worker)) {
1094+
return make_error(env, "invalid_worker");
1095+
}
1096+
1097+
ERL_NIF_TERM policy_map = sandbox_policy_to_term(env, worker->sandbox);
1098+
return enif_make_tuple2(env, ATOM_OK, policy_map);
1099+
}
1100+
9961101
/* ============================================================================
9971102
* Async worker NIFs
9981103
* ============================================================================ */
@@ -1827,6 +1932,11 @@ static ErlNifFunc nif_funcs[] = {
18271932
{"send_callback_response", 2, nif_send_callback_response, 0},
18281933
{"resume_callback", 2, nif_resume_callback, 0},
18291934

1935+
/* Sandbox support */
1936+
{"sandbox_set_policy", 2, nif_sandbox_set_policy, 0},
1937+
{"sandbox_enable", 2, nif_sandbox_enable, 0},
1938+
{"sandbox_get_policy", 1, nif_sandbox_get_policy, 0},
1939+
18301940
/* Async worker management */
18311941
{"async_worker_new", 0, nif_async_worker_new, 0},
18321942
{"async_worker_destroy", 1, nif_async_worker_destroy, 0},

c_src/py_nif.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,9 @@
102102
#include <sys/select.h>
103103
/** @} */
104104

105+
/* Forward declaration for sandbox policy */
106+
typedef struct sandbox_policy_t sandbox_policy_t;
107+
105108
/* ============================================================================
106109
* Feature Detection Macros
107110
* ============================================================================ */
@@ -239,6 +242,9 @@ typedef struct {
239242

240243
/** @brief Environment for building callback messages */
241244
ErlNifEnv *callback_env;
245+
246+
/** @brief Sandbox policy (NULL if no sandboxing) */
247+
sandbox_policy_t *sandbox;
242248
} py_worker_t;
243249

244250
/**

0 commit comments

Comments
 (0)