Compare commits

...

6 Commits

Author SHA1 Message Date
Tesselmax Opensource
9c65c7f1b3
Merge 46cc281099bfad78ee697e700330314307235695 into 71f06c4ff1c80e015c3da7b97da439815ca0c80f 2026-07-30 12:54:23 +01:00
blueloveTH
71f06c4ff1 fix a bug of __init__ 2026-07-29 19:16:24 +08:00
blueloveTH
fed2bcea78 update to 19 2026-07-28 18:55:16 +08:00
blueloveTH
799829a8aa fix PK_THREAD_LOCAL 2026-07-28 18:42:34 +08:00
blueloveTH
a06e6ac6ba fix bugs 2026-07-27 03:18:48 +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
9 changed files with 107 additions and 61 deletions

View File

@ -56,11 +56,11 @@ jobs:
- name: Setup Clang - name: Setup Clang
uses: egor-tensin/setup-clang@v1 uses: egor-tensin/setup-clang@v1
with: with:
version: 17 version: 19
platform: x64 platform: x64
- name: Run Sanitizers - name: Run Sanitizers
run: | run: |
sudo apt-get install -y libclang-rt-17-dev sudo apt-get install -y libclang-rt-19-dev
bash build_g.sh bash build_g.sh
bash run_tests.sh bash run_tests.sh
rm -rf ./main rm -rf ./main

View File

@ -4,7 +4,7 @@
typedef struct PyObject PyObject; typedef struct PyObject PyObject;
typedef struct VM VM; typedef struct VM VM;
extern _Thread_local VM* pk_current_vm; extern PK_THREAD_LOCAL VM* pk_current_vm;
typedef struct py_TValue { typedef struct py_TValue {
py_Type type; py_Type type;

View File

@ -78,6 +78,13 @@ typedef struct py_Callbacks {
PY_MAYBENULL void (*gc_mark)(void (*f)(py_Ref val, void* ctx), void* ctx); PY_MAYBENULL void (*gc_mark)(void (*f)(py_Ref val, void* ctx), void* ctx);
/// Used by `PRINT_EXPR` bytecode. /// Used by `PRINT_EXPR` bytecode.
PY_MAYBENULL bool (*displayhook)(py_Ref val) PY_RAISE; 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; } py_Callbacks;
/// A struct contains the application-level callbacks. /// A struct contains the application-level callbacks.

View File

@ -17,7 +17,7 @@ rm -rf .coverage
mkdir .coverage mkdir .coverage
UNITS=$(find ./ -name "*.gcno") UNITS=$(find ./ -name "*.gcno")
llvm-cov-17 gcov ${UNITS} -r -s include/ -r -s src/ >> .coverage/coverage.txt llvm-cov-19 gcov ${UNITS} -r -s include/ -r -s src/ >> .coverage/coverage.txt
mv *.gcov .coverage mv *.gcov .coverage
rm *.gcda rm *.gcda

View File

@ -189,8 +189,8 @@ static Error* LexerError(Lexer* self, const char* fmt, ...) {
err->src = self->src; err->src = self->src;
PK_INCREF(self->src); PK_INCREF(self->src);
err->lineno = self->current_line; err->lineno = self->current_line;
const char* end = self->src->source->data + self->src->source->size; const char* p_end = self->src->source->data + self->src->source->size;
if(self->curr_char <= end && *self->curr_char == '\n') { err->lineno--; } if(self->curr_char <= p_end && *self->curr_char == '\n') { err->lineno--; }
va_list args; va_list args;
va_start(args, fmt); va_start(args, fmt);
vsnprintf(err->msg, sizeof(err->msg), fmt, args); vsnprintf(err->msg, sizeof(err->msg), fmt, args);
@ -282,9 +282,16 @@ static Error* _eat_string(Lexer* self, c11_sbuf* buff, char quote, enum StringTy
case 'b': c11_sbuf__write_char(buff, '\b'); break; case 'b': c11_sbuf__write_char(buff, '\b'); break;
case 'f': c11_sbuf__write_char(buff, '\f'); break; case 'f': c11_sbuf__write_char(buff, '\f'); break;
case 'v': c11_sbuf__write_char(buff, '\v'); break; case 'v': c11_sbuf__write_char(buff, '\v'); break;
// Special case for the often used \0 while we don't have full support for octal literals. // Special case for the often used \0 while we don't have full support for octal
// literals.
case '0': c11_sbuf__write_char(buff, '\0'); break; case '0': c11_sbuf__write_char(buff, '\0'); break;
case 'x': { case 'x': {
// check there are at least 2 chars can read
const char* p_end = self->src->source->data + self->src->source->size;
if(p_end - self->curr_char < 2) {
return LexerError(self, "invalid hex escape");
}
char hex[3] = {eatchar(self), eatchar(self), '\0'}; char hex[3] = {eatchar(self), eatchar(self), '\0'};
int code; int code;
if(sscanf(hex, "%x", &code) != 1 || code > 0xFF) { if(sscanf(hex, "%x", &code) != 1 || code > 0xFF) {

View File

@ -92,6 +92,7 @@ void VM__ctor(VM* self) {
self->callbacks.print = pk_default_print; self->callbacks.print = pk_default_print;
self->callbacks.flush = pk_default_flush; self->callbacks.flush = pk_default_flush;
self->callbacks.getchr = pk_default_getchr; self->callbacks.getchr = pk_default_getchr;
self->callbacks.open_file = NULL;
self->last_retval = *py_NIL(); self->last_retval = *py_NIL();
self->unhandled_exc = *py_NIL(); self->unhandled_exc = *py_NIL();
@ -579,8 +580,9 @@ FrameResult VM__vectorcall(VM* self, uint16_t argc, uint16_t kwargc, bool opcall
} }
if(p0->type == tp_type) { if(p0->type == tp_type) {
py_Type p0_type = py_totype(p0);
// [cls, NULL, args..., kwargs...] // [cls, NULL, args..., kwargs...]
py_Ref new_f = py_tpfindmagic(py_totype(p0), __new__); py_Ref new_f = py_tpfindmagic(p0_type, __new__);
assert(new_f && py_isnil(p0 + 1)); assert(new_f && py_isnil(p0 + 1));
bool is_default_new = new_f->type == tp_nativefunc && new_f->_cfunc == pk__object_new; bool is_default_new = new_f->type == tp_nativefunc && new_f->_cfunc == pk__object_new;
@ -598,14 +600,16 @@ FrameResult VM__vectorcall(VM* self, uint16_t argc, uint16_t kwargc, bool opcall
// NOTE: previously we use `get_unbound_method` but here we just use `tpfindmagic` // NOTE: previously we use `get_unbound_method` but here we just use `tpfindmagic`
// >> [cls, NULL, args..., kwargs...] // >> [cls, NULL, args..., kwargs...]
// >> py_retval() is the new instance // >> py_retval() is the new instance
py_Ref init_f = py_tpfindmagic(py_totype(p0), __init__); py_Ref init_f = py_tpfindmagic(p0_type, __init__);
if(init_f) { if(init_f) {
if(py_isinstance(py_retval(), p0_type)) {
// do an inplace patch // do an inplace patch
*p0 = *init_f; // __init__ *p0 = *init_f; // __init__
p0[1] = self->last_retval; // self p0[1] = self->last_retval; // self
// [__init__, self, args..., kwargs...] // [__init__, self, args..., kwargs...]
if(VM__vectorcall(self, argc, kwargc, false) == RES_ERROR) return RES_ERROR; if(VM__vectorcall(self, argc, kwargc, false) == RES_ERROR) return RES_ERROR;
*py_retval() = p0[1]; // restore the new instance *py_retval() = p0[1]; // restore the new instance
}
} else { } else {
if(is_default_new) { if(is_default_new) {
if(argc != 0 || kwargc != 0) { if(argc != 0 || kwargc != 0) {
@ -713,8 +717,16 @@ void ManagedHeap__mark(ManagedHeap* self) {
} }
} }
if(obj->type > tp_object) {
// NOTE: `defaultdict` -> `dict` -> `object`
// NOTE: native types must extend from `object`.
py_TypeInfo* ti = pk_typeinfo(obj->type);
while(ti->base != tp_object) {
ti = ti->base_ti;
}
void* ud = PyObject__userdata(obj); void* ud = PyObject__userdata(obj);
switch(obj->type) { switch(ti->index) {
case tp_list: { case tp_list: {
List* self = ud; List* self = ud;
for(int i = 0; i < self->length; i++) { for(int i = 0; i < self->length; i++) {
@ -763,4 +775,5 @@ void ManagedHeap__mark(ManagedHeap* self) {
} }
} }
} }
}
} }

View File

@ -71,6 +71,10 @@ static bool os_remove(int argc, py_Ref argv) {
PY_CHECK_ARGC(1); PY_CHECK_ARGC(1);
PY_CHECK_ARG_TYPE(0, tp_str); PY_CHECK_ARG_TYPE(0, tp_str);
const char* path = py_tostr(py_arg(0)); 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); int code = remove(path);
if(code != 0) { if(code != 0) {
const char* msg = strerror(errno); 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)); io_FileIO* ud = py_newobject(py_retval(), cls, 0, sizeof(io_FileIO));
ud->path = py_tostr(py_arg(1)); ud->path = py_tostr(py_arg(1));
ud->mode = py_tostr(py_arg(2)); 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); ud->file = fopen(ud->path, ud->mode);
if(ud->file == NULL) { if(ud->file == NULL) {
const char* msg = strerror(errno); const char* msg = strerror(errno);

View File

@ -5,7 +5,7 @@
#include "pocketpy/common/name.h" #include "pocketpy/common/name.h"
#include "pocketpy/interpreter/vm.h" #include "pocketpy/interpreter/vm.h"
_Thread_local VM* pk_current_vm; PK_THREAD_LOCAL VM* pk_current_vm;
static bool pk_initialized; static bool pk_initialized;
static bool pk_finalized; static bool pk_finalized;

View File

@ -168,3 +168,13 @@ class DerivedClass(BaseClass):
assert DerivedClass.f() == 'BaseClass' assert DerivedClass.f() == 'BaseClass'
# bad __init__
class A:
def __new__(cls, *args, **kwargs):
return 1
def __init__(self):
assert False
A()