diff --git a/include/pocketpy/pocketpy.h b/include/pocketpy/pocketpy.h index bf7e8f3e..8d414209 100644 --- a/include/pocketpy/pocketpy.h +++ b/include/pocketpy/pocketpy.h @@ -78,6 +78,13 @@ typedef struct py_Callbacks { PY_MAYBENULL void (*gc_mark)(void (*f)(py_Ref val, void* ctx), void* ctx); /// Used by `PRINT_EXPR` bytecode. PY_MAYBENULL bool (*displayhook)(py_Ref val) PY_RAISE; + // open_file hook contributed by fdtd.io (Hector), 2026. + /// Consulted before a script-reachable file operation. `path` is the target path; + /// `mode` is the fopen mode string for `io.FileIO`, or the literal "delete" for + /// `os.remove`. Return true to allow the operation, false to reject it (the binding + /// then raises OSError). NULL (the default) allows everything, so existing embedders + /// are unaffected. Lets an embedder enforce its own path policy. + PY_MAYBENULL bool (*open_file)(const char* path, const char* mode); } py_Callbacks; /// A struct contains the application-level callbacks. diff --git a/src/interpreter/vm.c b/src/interpreter/vm.c index 1db9ef6d..8b3f4214 100644 --- a/src/interpreter/vm.c +++ b/src/interpreter/vm.c @@ -92,6 +92,7 @@ void VM__ctor(VM* self) { self->callbacks.print = pk_default_print; self->callbacks.flush = pk_default_flush; self->callbacks.getchr = pk_default_getchr; + self->callbacks.open_file = NULL; self->last_retval = *py_NIL(); self->unhandled_exc = *py_NIL(); diff --git a/src/modules/os.c b/src/modules/os.c index 7c366730..064c1906 100644 --- a/src/modules/os.c +++ b/src/modules/os.c @@ -71,6 +71,10 @@ static bool os_remove(int argc, py_Ref argv) { PY_CHECK_ARGC(1); PY_CHECK_ARG_TYPE(0, tp_str); const char* path = py_tostr(py_arg(0)); + // open_file policy hook: "delete" pseudo-mode for os.remove. + if(pk_current_vm->callbacks.open_file && !pk_current_vm->callbacks.open_file(path, "delete")) { + return OSError("os.remove not permitted: '%s'", path); + } int code = remove(path); if(code != 0) { const char* msg = strerror(errno); @@ -111,6 +115,11 @@ static bool io_FileIO__new__(int argc, py_Ref argv) { io_FileIO* ud = py_newobject(py_retval(), cls, 0, sizeof(io_FileIO)); ud->path = py_tostr(py_arg(1)); ud->mode = py_tostr(py_arg(2)); + // open_file policy hook: consulted with the fopen mode string before the open. + if(pk_current_vm->callbacks.open_file && + !pk_current_vm->callbacks.open_file(ud->path, ud->mode)) { + return OSError("file open not permitted: '%s' (mode '%s')", ud->path, ud->mode); + } ud->file = fopen(ud->path, ud->mode); if(ud->file == NULL) { const char* msg = strerror(errno);