diff --git a/Makefile b/Makefile index 5091102..a8834f5 100644 --- a/Makefile +++ b/Makefile @@ -399,7 +399,7 @@ bin/testTask: obj/testTask.o $(TASKADP) obj/calogHandle.o lib/libcalog.a lib/lib $(CC) $(LDFLAGS) -pthread -o $@ $^ $(LUALIBS) -lstdc++ -lm # export library test: publish from Lua, call by bare name from Lua + via callExport from JS. -bin/testExport: obj/testExport.o obj/calogExport.o lib/libcalog.a lib/liblua.a lib/libquickjs.a lib/libsquirrel.a lib/libs7.a | bin +bin/testExport: obj/testExport.o obj/calogExport.o lib/libcalog.a lib/liblua.a lib/libquickjs.a lib/libsquirrel.a lib/libs7.a lib/libmybasic.a | bin $(CC) $(LDFLAGS) -pthread -o $@ $^ $(LUALIBS) -lstdc++ -ldl -lm obj bin lib: diff --git a/libs/calogExport.c b/libs/calogExport.c index 2505ed7..0d20e18 100644 --- a/libs/calogExport.c +++ b/libs/calogExport.c @@ -1,6 +1,6 @@ // calogExport.c -- calog export library (see calogExport.h). A process-wide, mutex-guarded -// map of name -> reference-counted function value. export/unexport/call are natives; -// __exportResolve is the hook the per-engine dynamic-name resolvers call to look a name up. +// map of name -> reference-counted function value. calogExport/calogUnexport/calogCall are +// natives; __calogExportResolve is the hook the per-engine dynamic-name resolvers look up. // // The registry state is STATIC (not heap), and never freed: the four natives are reachable // (via any context, and via every Lua unknown-global lookup) right up until calogDestroy @@ -46,10 +46,10 @@ int32_t calogExportRegister(CalogT *calog) { pthread_mutex_lock(&gInitMutex); gRefCount++; pthread_mutex_unlock(&gInitMutex); - calogRegisterInline(calog, "export", exportPublish, NULL); - calogRegisterInline(calog, "unexport", exportRemove, NULL); - calogRegisterInline(calog, "call", exportCall, NULL); - calogRegisterInline(calog, "__exportResolve", exportResolve, NULL); + calogRegisterInline(calog, "calogExport", exportPublish, NULL); + calogRegisterInline(calog, "calogUnexport", exportRemove, NULL); + calogRegisterInline(calog, "calogCall", exportCall, NULL); + calogRegisterInline(calog, "__calogExportResolve", exportResolve, NULL); return calogOkE; } @@ -84,13 +84,13 @@ static int32_t exportCall(CalogValueT *args, int32_t argCount, CalogValueT *resu (void)userData; calogValueNil(result); if (argCount < 1 || args[0].type != calogStringE) { - return calogFail(result, calogErrArgE, "call expects (name, ...args)"); + return calogFail(result, calogErrArgE, "calogCall expects (name, ...args)"); } pthread_mutex_lock(&gMapMutex); index = exportFindLocked(args[0].as.s.bytes); if (index < 0) { pthread_mutex_unlock(&gMapMutex); - return calogFail(result, calogErrNotFoundE, "call: no such exported function"); + return calogFail(result, calogErrNotFoundE, "calogCall: no such exported function"); } // Hold a reference across the (unlocked) call so a concurrent unexport can't free it. fn = gEntries[index].fn; @@ -122,7 +122,7 @@ static int32_t exportPublish(CalogValueT *args, int32_t argCount, CalogValueT *r (void)userData; calogValueNil(result); if (argCount != 2 || args[0].type != calogStringE || args[1].type != calogFnE) { - return calogFail(result, calogErrArgE, "export expects (name, function)"); + return calogFail(result, calogErrArgE, "calogExport expects (name, function)"); } name = args[0].as.s.bytes; pthread_mutex_lock(&gMapMutex); @@ -142,7 +142,7 @@ static int32_t exportPublish(CalogValueT *args, int32_t argCount, CalogValueT *r grown = (ExportEntryT *)realloc(gEntries, (size_t)newCap * sizeof(ExportEntryT)); if (grown == NULL) { pthread_mutex_unlock(&gMapMutex); - return calogFail(result, calogErrOomE, "export: out of memory"); + return calogFail(result, calogErrOomE, "calogExport: out of memory"); } gEntries = grown; gCap = newCap; @@ -150,7 +150,7 @@ static int32_t exportPublish(CalogValueT *args, int32_t argCount, CalogValueT *r nameCopy = strdup(name); if (nameCopy == NULL) { pthread_mutex_unlock(&gMapMutex); - return calogFail(result, calogErrOomE, "export: out of memory"); + return calogFail(result, calogErrOomE, "calogExport: out of memory"); } calogFnRetain(args[1].as.fn); gEntries[gCount].name = nameCopy; @@ -167,7 +167,7 @@ static int32_t exportRemove(CalogValueT *args, int32_t argCount, CalogValueT *re (void)userData; calogValueNil(result); if (argCount != 1 || args[0].type != calogStringE) { - return calogFail(result, calogErrArgE, "unexport expects (name)"); + return calogFail(result, calogErrArgE, "calogUnexport expects (name)"); } pthread_mutex_lock(&gMapMutex); index = exportFindLocked(args[0].as.s.bytes); @@ -188,7 +188,7 @@ static int32_t exportResolve(CalogValueT *args, int32_t argCount, CalogValueT *r (void)userData; calogValueNil(result); if (argCount != 1 || args[0].type != calogStringE) { - return calogFail(result, calogErrArgE, "__exportResolve expects (name)"); + return calogFail(result, calogErrArgE, "__calogExportResolve expects (name)"); } pthread_mutex_lock(&gMapMutex); index = exportFindLocked(args[0].as.s.bytes); diff --git a/libs/calogExport.h b/libs/calogExport.h index ba721f4..6206dcb 100644 --- a/libs/calogExport.h +++ b/libs/calogExport.h @@ -1,14 +1,17 @@ // calogExport.h -- calog export library: publish script functions other scripts can call. // // Registers natives so a script can share a function by name across contexts and engines: -// export(name, fn) publish a function value under a global name -// unexport(name) remove it -// call(name, ...args) call an exported function by name -- works in EVERY engine +// calogExport(name, fn) publish a function value under a global name +// calogUnexport(name) remove it +// calogCall(name, ...args) call an exported function by name -- works in EVERY engine +// +// The natives are calog-prefixed so they never collide with an engine's reserved words +// (plain `export` is a JavaScript keyword; plain `call` is a my-basic keyword). // // On engines with a runtime unknown-name hook (Lua, Squirrel, JavaScript, s7), an exported // name is ALSO reachable by its BARE name, e.g. `luaExample(1, 2)`; engines that resolve -// names statically (Wren, Berry, my-basic) use call('luaExample', ...). Resolution is -// dynamic: a name exported at any time becomes callable immediately, and unexport takes +// names statically (Wren, Berry, my-basic) use calogCall('luaExample', ...). Resolution is +// dynamic: a name exported at any time becomes callable immediately, and calogUnexport takes // effect at once. // // An exported function is an ordinary calog function value, so a call runs in the exporter's diff --git a/src/broker.c b/src/broker.c index 27977c9..8281936 100644 --- a/src/broker.c +++ b/src/broker.c @@ -95,9 +95,18 @@ void calogForEach(CalogT *broker, void (*visit)(const CalogEntryT *entry, void * int64_t index; for (index = 0; index < broker->slotCount; index++) { - if (broker->slots[index].name != NULL) { - visit(&broker->slots[index], ud); + const char *name; + name = broker->slots[index].name; + if (name == NULL) { + continue; } + // Natives whose name begins with "__" are internal (e.g. the export resolver hook + // __calogExportResolve): still reachable through calogCall/calogLookup, but never + // exposed into an engine's global namespace, so scripts cannot see or shadow them. + if (name[0] == '_' && name[1] == '_') { + continue; + } + visit(&broker->slots[index], ud); } } diff --git a/src/calog.h b/src/calog.h index b9160e6..26390d3 100644 --- a/src/calog.h +++ b/src/calog.h @@ -139,6 +139,8 @@ void calogPump(CalogT *calog); // run pending script->native calls on THIS void calogSetErrorHandler(CalogT *calog, CalogErrorFnT fn, void *userData); // ---- natives ---- +// A name beginning with "__" is INTERNAL: it is callable via calogCall but is NOT exposed +// into any engine's global namespace, so scripts can neither see nor shadow it. int32_t calogRegister(CalogT *calog, const char *name, CalogNativeFnT fn, void *userData); // runs on the host thread int32_t calogRegisterInline(CalogT *calog, const char *name, CalogNativeFnT fn, void *userData); // runs on the calling script thread int32_t calogCall(CalogT *calog, const char *name, CalogValueT *args, int32_t argCount, CalogValueT *result); diff --git a/src/calogInternal.h b/src/calogInternal.h index 6e59cf9..fc93370 100644 --- a/src/calogInternal.h +++ b/src/calogInternal.h @@ -88,7 +88,8 @@ struct CalogT { CalogT *calogBrokerCreate(void); void calogBrokerDestroy(CalogT *calog); CalogEntryT *calogLookup(CalogT *calog, const char *name); -// Visit every registered entry (used by createInterpreter to expose all natives). +// Visit every registered entry EXCEPT internal "__"-prefixed ones (used by createInterpreter +// to expose natives to scripts; __ natives stay reachable via calogCall but are not exposed). // Safe only while the registry is frozen -- i.e. before any context starts. void calogForEach(CalogT *calog, void (*visit)(const CalogEntryT *entry, void *ud), void *ud); diff --git a/src/js/jsAdapter.c b/src/js/jsAdapter.c index 9e8e2a6..d344368 100644 --- a/src/js/jsAdapter.c +++ b/src/js/jsAdapter.c @@ -473,7 +473,7 @@ static int jsResolveOwnProperty(JSContext *ctx, JSPropertyDescriptor *desc, JSVa return 0; } calogValueNil(&result); - status = calogCall(context->broker, "__exportResolve", &nameArg, 1, &result); + status = calogCall(context->broker, "__calogExportResolve", &nameArg, 1, &result); calogValueFree(&nameArg); if (status != calogOkE || result.type != calogFnE) { calogValueFree(&result); diff --git a/src/lua/luaAdapter.c b/src/lua/luaAdapter.c index 597138c..261d2d0 100644 --- a/src/lua/luaAdapter.c +++ b/src/lua/luaAdapter.c @@ -338,7 +338,7 @@ static int luaGlobalResolve(lua_State *L) { return 1; } calogValueNil(&result); - status = calogCall(context->broker, "__exportResolve", &nameArg, 1, &result); + status = calogCall(context->broker, "__calogExportResolve", &nameArg, 1, &result); calogValueFree(&nameArg); if (status == calogOkE && result.type == calogFnE) { if (luaPushValueDepth(L, &result, 0) != calogOkE) { diff --git a/src/s7/s7Adapter.c b/src/s7/s7Adapter.c index 6ef4541..5bbcdb0 100644 --- a/src/s7/s7Adapter.c +++ b/src/s7/s7Adapter.c @@ -475,7 +475,7 @@ static s7_pointer s7ResolveUnbound(s7_scheme *sc, s7_pointer args) { return s7_unspecified(sc); } calogValueNil(&result); - status = calogCall(context->broker, "__exportResolve", &nameArg, 1, &result); + status = calogCall(context->broker, "__calogExportResolve", &nameArg, 1, &result); calogValueFree(&nameArg); if (status == calogOkE && result.type == calogFnE) { s7_let_set(sc, hookLet, s7_make_symbol(sc, "result"), s7FromValue(context, &result, 0)); diff --git a/src/squirrel/squirrelAdapter.c b/src/squirrel/squirrelAdapter.c index 9d534d8..46226e8 100644 --- a/src/squirrel/squirrelAdapter.c +++ b/src/squirrel/squirrelAdapter.c @@ -485,7 +485,7 @@ static SQInteger squirrelResolveGet(HSQUIRRELVM v) { return sq_throwerror(v, _SC("calog resolver: out of memory")); } calogValueNil(&result); - status = calogCall(context->broker, "__exportResolve", &nameArg, 1, &result); + status = calogCall(context->broker, "__calogExportResolve", &nameArg, 1, &result); calogValueFree(&nameArg); if (status == calogOkE && result.type == calogFnE) { status = squirrelPushValueDepth(v, &result, 0); diff --git a/tests/testExport.c b/tests/testExport.c index c7c58aa..390f96c 100644 --- a/tests/testExport.c +++ b/tests/testExport.c @@ -1,6 +1,6 @@ // testExport.c -- exercises the export library. One Lua context publishes a function with -// export(); another Lua context calls it by its BARE name (via the globals __index resolver); -// a JavaScript context calls it via call. All calls run in the exporter's context. +// calogExport(); another Lua context calls it by its BARE name (via the globals __index resolver); +// a JavaScript context calls it via calogCall. All calls run in the exporter's context. #define _POSIX_C_SOURCE 200809L @@ -140,6 +140,7 @@ int main(void) { CalogContextT *jsCaller; CalogContextT *s7Caller; CalogContextT *squirrelCaller; + CalogContextT *mbCaller; CalogContextT *reloadV1; CalogContextT *reloadV2; int32_t i; @@ -165,7 +166,7 @@ int main(void) { exporter = calogContextOpen(calog, &calogLuaEngine); calogContextEval(exporter, "local function adder(a, b) return a + b end\n" - "export('adder', adder)\n" + "calogExport('adder', adder)\n" "exporterReady()"); pumpUntilReady(); @@ -173,13 +174,14 @@ int main(void) { luaCaller = calogContextOpen(calog, &calogLuaEngine); calogContextEval(luaCaller, "report(1, adder(2, 3))\n" - "report(2, call('adder', 10, 20))\n" + "report(2, calogCall('adder', 10, 20))\n" + "report(10, __calogExportResolve == nil and 1 or 0)\n" // internal native must be hidden "done()"); // A JavaScript context reaches the same Lua function via call, then by BARE name. jsCaller = calogContextOpen(calog, &calogJsEngine); calogContextEval(jsCaller, - "report(3, call('adder', 100, 1));\n" + "report(3, calogCall('adder', 100, 1));\n" "report(8, adder(9, 1));\n" "done();"); @@ -192,14 +194,24 @@ int main(void) { "report(7, adder(50, 5));\n" "done();"); - pumpUntilDone(4); + // my-basic resolves names statically (no bare-name), so it reaches the export via + // calogCall -- which the calog prefix keeps clear of its CALL keyword. + mbCaller = calogContextOpen(calog, &calogMyBasicEngine); + calogContextEval(mbCaller, + "x = calogCall(\"adder\", 30, 5)\n" + "report(9, x)\n" + "done()"); + + pumpUntilDone(5); CHECK(atomic_load(&results[1]) == 5, "a Lua script called an exported function by its bare name"); - CHECK(atomic_load(&results[2]) == 30, "call reached the same exported function"); - CHECK(atomic_load(&results[3]) == 101, "a JavaScript script called the exported Lua function via call"); + CHECK(atomic_load(&results[2]) == 30, "calogCall reached the same exported function"); + CHECK(atomic_load(&results[3]) == 101, "a JavaScript script called the exported Lua function via calogCall"); CHECK(atomic_load(&results[8]) == 10, "a JavaScript script called the export by its bare name"); CHECK(atomic_load(&results[6]) == 105, "an s7 script called the export by its bare name"); CHECK(atomic_load(&results[7]) == 55, "a Squirrel script called the export by its bare name"); + CHECK(atomic_load(&results[9]) == 35, "a my-basic script reached the export via calogCall"); + CHECK(atomic_load(&results[10]) == 1, "the internal __calogExportResolve native is hidden from script globals"); CHECK(atomic_load(&errorCount) == 0, "no errors during export/import"); // Hot-reload: a script exports a function, is unloaded, then a NEW version re-exports the @@ -210,37 +222,38 @@ int main(void) { atomic_store(&exporterReady, false); calogContextEval(reloadV1, "local function greet() return 111 end\n" - "export('greet', greet)\n" + "calogExport('greet', greet)\n" "exporterReady()"); pumpUntilReady(); - calogContextEval(luaCaller, "report(4, call('greet'))\n done()"); - pumpUntilDone(5); + calogContextEval(luaCaller, "report(4, calogCall('greet'))\n done()"); + pumpUntilDone(6); calogContextClose(reloadV1); // greet stays exported, but its owner is gone reloadV2 = calogContextOpen(calog, &calogLuaEngine); atomic_store(&exporterReady, false); calogContextEval(reloadV2, "local function greet() return 222 end\n" - "export('greet', greet)\n" // releases the old, dead-owner greet + "calogExport('greet', greet)\n" // releases the old, dead-owner greet "exporterReady()"); pumpUntilReady(); - calogContextEval(luaCaller, "report(5, call('greet'))\n done()"); - pumpUntilDone(6); + calogContextEval(luaCaller, "report(5, calogCall('greet'))\n done()"); + pumpUntilDone(7); CHECK(atomic_load(&results[4]) == 111, "an exported function is callable before reload"); CHECK(atomic_load(&results[5]) == 222, "a reloaded script re-exports a new version after the old owner unloaded"); calogContextClose(reloadV2); // Release exports while the runtime + contexts are still alive, then tear the runtime down. calogExportShutdown(); - // Regression: after shutdown, an unknown-global read still routes through __exportResolve - // (via the _G __index) -- the static registry must keep it safe, not a freed-state UAF. + // Regression: after shutdown, an unknown-global read still routes through + // __calogExportResolve (via the _G __index) -- the static registry keeps it safe. calogContextEval(luaCaller, "local x = someUndefinedGlobalName\n done()"); - pumpUntilDone(7); - CHECK(atomic_load(&doneCount) >= 7, "resolving an unknown global after export shutdown is safe"); + pumpUntilDone(8); + CHECK(atomic_load(&doneCount) >= 8, "resolving an unknown global after export shutdown is safe"); calogContextClose(luaCaller); calogContextClose(jsCaller); calogContextClose(s7Caller); calogContextClose(squirrelCaller); + calogContextClose(mbCaller); calogContextClose(exporter); calogDestroy(calog);