Compare commits

...

5 Commits

Author SHA1 Message Date
Tesselmax Opensource
d7520ab48a
Merge 46cc281099bfad78ee697e700330314307235695 into 7baf64e67c7c88f736aff9a3329c69fe4723a137 2026-08-06 00:14:13 +08:00
blueloveTH
7baf64e67c Update vm.c 2026-08-06 00:13:25 +08:00
blueloveTH
7435251873 Update os.c 2026-08-05 23:41:56 +08:00
blueloveTH
5546291c91 add PermissionError 2026-08-05 23:31:22 +08:00
Tesselmax
46cc281099 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.
2026-07-16 12:51:12 -07:00
3 changed files with 20 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.
@ -864,6 +871,7 @@ enum py_PredefinedType {
tp_SyntaxError,
tp_RecursionError,
tp_OSError,
tp_PermissionError,
tp_NotImplementedError,
tp_TypeError,
tp_IndexError,

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();
@ -195,6 +196,7 @@ void VM__ctor(VM* self) {
INJECT_BUILTIN_EXC(SyntaxError, tp_Exception);
INJECT_BUILTIN_EXC(RecursionError, tp_Exception);
INJECT_BUILTIN_EXC(OSError, tp_Exception);
INJECT_BUILTIN_EXC(PermissionError, tp_Exception);
INJECT_BUILTIN_EXC(NotImplementedError, tp_Exception);
INJECT_BUILTIN_EXC(TypeError, tp_Exception);
INJECT_BUILTIN_EXC(IndexError, tp_Exception);

View File

@ -48,6 +48,7 @@ static bool os_chdir(int argc, py_Ref argv) {
}
static bool os_getcwd(int argc, py_Ref argv) {
PY_CHECK_ARGC(0);
char buf[1024];
if(!platform_getcwd(buf, sizeof(buf))) return OSError("getcwd() failed");
py_newstr(py_retval(), buf);
@ -71,6 +72,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 +116,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);