Add py_Callbacks.open_file hook for embedder file-access policy

Adds an optional open_file callback to py_Callbacks, consulted before
script-reachable file operations:

  - io.FileIO(path, mode): called with the fopen mode string before the open
  - os.remove(path): called with the literal "delete"

Returning false rejects the operation and raises OSError; returning true
(or leaving the callback NULL, which is the default) allows it. Existing
embedders are unaffected -- the default is NULL and behavior is unchanged.

This lets an embedder enforce its own path policy using its own
canonicalization instead of reimplementing path checks inside the VM.
This commit is contained in:
Tesselmax 2026-07-16 11:27:57 -07:00
parent e144785436
commit 46cc281099
3 changed files with 17 additions and 0 deletions

View File

@ -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.

View File

@ -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();

View File

@ -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);