Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 42 additions & 5 deletions core/deviceDrivers/matter/sbmd/SafeJSValue.h
Original file line number Diff line number Diff line change
Expand Up @@ -177,13 +177,24 @@ namespace barton
*/
SafeJSValue AddObject(const char *key)
{
JS_SetPropertyStr(ctx, Get(), key, JS_NewObject(ctx)); // create + attach in one step

#if defined(BCORE_USE_MQUICKJS)
// Create the child in its own statement first. Performing the allocation inside the
// JS_SetPropertyStr(...) argument list is unsafe under mquickjs's moving GC: JS_NewObject
// may relocate *this*, and because C++ leaves argument evaluation order unspecified, the
// parent handle Get() can be computed before that allocation and passed stale. mquickjs
// has no reference counting, so JS_SetPropertyStr does not consume a reference and it is
// safe to keep the child rooted across the attach.
SafeJSValue child(ctx, JS_NewObject(ctx));
JS_SetPropertyStr(ctx, Get(), key, child.Get());

// *this is rooted, so Get() is current. Re-read the child to obtain a current handle,
// then root it in its own slot so the returned wrapper is never a stale snapshot.
return SafeJSValue(ctx, JS_GetPropertyStr(ctx, Get(), key));
#elif defined(BCORE_USE_QUICKJS)
// quickjs is reference counted and never relocates, and JS_SetPropertyStr consumes the
// freshly created (+1) reference, so create-and-attach in one step is correct here.
JS_SetPropertyStr(ctx, Get(), key, JS_NewObject(ctx));

// JS_GetPropertyStr returns an owned (+1) reference; adopt it without an extra dup.
return SafeJSValue(ctx, JS_GetPropertyStr(ctx, Get(), key), Adopt {});
#endif
Expand All @@ -195,23 +206,49 @@ namespace barton
* key - the property name to assign.
* v - the unsigned 32-bit value to store.
*/
void SetUint32(const char *key, uint32_t v) { JS_SetPropertyStr(ctx, Get(), key, JS_NewUint32(ctx, v)); }
void SetUint32(const char *key, uint32_t v)
{
#if defined(BCORE_USE_MQUICKJS)
// Root the newly-created value across JS_SetPropertyStr(): that call can allocate and
// trigger GC, and mquickjs does not scan raw C locals.
SafeJSValue value(ctx, JS_NewUint32(ctx, v));
JS_SetPropertyStr(ctx, Get(), key, value.Get());
#elif defined(BCORE_USE_QUICKJS)
JS_SetPropertyStr(ctx, Get(), key, JS_NewUint32(ctx, v));
#endif
}
Comment thread
tleacmcsa marked this conversation as resolved.

/*
* Set property `key` on this object to a new int32 JS value.
*
* key - the property name to assign.
* v - the signed 32-bit value to store.
*/
void SetInt32(const char *key, int32_t v) { JS_SetPropertyStr(ctx, Get(), key, JS_NewInt32(ctx, v)); }
void SetInt32(const char *key, int32_t v)
{
#if defined(BCORE_USE_MQUICKJS)
SafeJSValue value(ctx, JS_NewInt32(ctx, v));
JS_SetPropertyStr(ctx, Get(), key, value.Get());
#elif defined(BCORE_USE_QUICKJS)
JS_SetPropertyStr(ctx, Get(), key, JS_NewInt32(ctx, v));
#endif
}
Comment thread
tleacmcsa marked this conversation as resolved.

/*
* Set property `key` on this object to a new JS string.
*
* key - the property name to assign.
* v - the null-terminated C string to store (copied into a JS string).
*/
void SetString(const char *key, const char *v) { JS_SetPropertyStr(ctx, Get(), key, JS_NewString(ctx, v)); }
void SetString(const char *key, const char *v)
{
#if defined(BCORE_USE_MQUICKJS)
SafeJSValue value(ctx, JS_NewString(ctx, v));
JS_SetPropertyStr(ctx, Get(), key, value.Get());
#elif defined(BCORE_USE_QUICKJS)
JS_SetPropertyStr(ctx, Get(), key, JS_NewString(ctx, v));
#endif
}
Comment thread
tleacmcsa marked this conversation as resolved.

/*
* Set property `key` on this object to JS null.
Expand Down
30 changes: 21 additions & 9 deletions core/deviceDrivers/matter/sbmd/SbmdDriver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
#define logFmt(fmt) "(%s): " fmt, __func__

#include "SbmdDriver.h"
#include "mquickjs/MQuickJsRuntime.h"
#include "mquickjs/SbmdLoader.h"

extern "C" {
Expand All @@ -44,19 +45,22 @@ namespace barton

SbmdDriver::~SbmdDriver()
{
// Held-reference cleanup contract:
// The handler references held while activated belong to the JSContext used to activate this
// driver. They can only be released via ReleaseHandlers (through Deactivate), which requires that
// JSContext to still be alive. The destructor deliberately does NOT release them because it has
// no access to the JSContext and cannot assume it still exists. Callers are therefore responsible
// for invoking Deactivate() before destroying an activated driver; reaching this destructor while
// still activated leaks the references (and is reported below).
// Handler references are rooted (as GC roots) at extraction/activation time, so even a
// loaded-but-never-activated registration can still own roots here.
if (registration && registration->activated)
{
icWarn("driver '%s' destroyed while still activated", registration->name.c_str());
}

// Abandon the held references without releasing them: releasing here would touch a
// context we cannot assume is still alive. Leaking matches the historical behaviour.
// A SafeJSValue holds a GC root as an intrusive node whose address is registered with the
// collector. Detaching abandons the node WITHOUT unlinking it, so if we detach here the node
// stays linked in the GC root list while its backing memory (this registration) is freed --
// the next collection then walks a dangling node and crashes. So detach only when the shared
// context is already gone (runtime shut down): then the GC list no longer exists and calling
// JS_DeleteGCRef would instead touch a dead context. While the context is alive, do nothing
// and let the member SafeJSValue destructors properly unlink each root via JS_DeleteGCRef.
if (registration && MQuickJsRuntime::GetSharedContext() == nullptr)
{
VisitHandlers([](SbmdHandler &entry) { entry.heldFn.Detach(); });
}
Comment thread
Copilot marked this conversation as resolved.
Outdated
Comment thread
tleacmcsa marked this conversation as resolved.
}
Expand Down Expand Up @@ -152,6 +156,14 @@ namespace barton

void SbmdDriver::HoldIfValid(JSContext *ctx, SbmdHandler &entry)
{
// Handlers are rooted at extraction time, so heldFn is normally already valid here. Keep
// that root rather than re-holding from the raw handler slot, which may have gone stale
// after GC relocations during extraction.
if (entry.heldFn.HasValue())
{
return;
}

if (JS_IsUndefined(entry.handler))
{
entry.heldFn = SafeJSValue {};
Expand Down
14 changes: 8 additions & 6 deletions core/deviceDrivers/matter/sbmd/SbmdRegistration.h
Original file line number Diff line number Diff line change
Expand Up @@ -85,13 +85,15 @@ namespace barton
SbmdHandler(SbmdHandler &&) = default;
SbmdHandler &operator=(SbmdHandler &&) = default;

// Raw function reference captured at load time. This is a deliberately unheld staging slot:
// a loaded-but-not-yet-activated registration owns no held references, so it can be moved,
// discarded on a failed activation, or destroyed without needing the runtime mutex. Only
// valid transiently after load; the driver promotes it into heldFn when activated.
// Raw function reference captured at load time. This is a transient staging slot: the loader
// immediately populates heldFn to root the function during extraction (preventing GC from
// relocating it while the rest of the driver is parsed), so this raw slot is only valid
// briefly and callers must always invoke via Fn(). The held reference persists until the
// handler is reset (e.g., driver deactivation).
JSValue handler = JS_UNDEFINED;
// Held function reference. Empty until the driver is activated, then keeps the function alive
// until deactivation. SafeJSValue always yields the current function object, so always invoke via Fn().
// Held function reference. Populated immediately during load/extraction to keep the function
// alive across GC operations. SafeJSValue always yields the current function object, so always
// invoke via Fn().
SafeJSValue heldFn;
SbmdSupplements supplements;

Expand Down
35 changes: 21 additions & 14 deletions core/deviceDrivers/matter/sbmd/mquickjs/MQuickJsRuntime.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@

#include "MQuickJsRuntime.h"
#include "SbmdJsUtil.h"
#include "matter/sbmd/SafeJSValue.h"

#include <chrono>
#include <cstdlib>
Expand Down Expand Up @@ -221,51 +222,57 @@ bool MQuickJsRuntime::CheckAndClearPendingException(JSContext *ctx, std::string
return false;
}

JSValue pendingEx = JS_GetException(ctx);
JSValue pendingExRaw = JS_GetException(ctx);

// JS_GetException returns JS_NULL or JS_UNDEFINED when no exception is pending
bool hasException = !JS_IsNull(pendingEx) && !JS_IsUndefined(pendingEx) && !JS_IsUninitialized(pendingEx);
bool hasException = !JS_IsNull(pendingExRaw) && !JS_IsUndefined(pendingExRaw) && !JS_IsUninitialized(pendingExRaw);

if (!hasException)
{
return false;
}

// mquickjs uses a moving/compacting GC: every JS allocation (including each JS_GetPropertyStr /
// JS_ToCString below) can relocate live objects, invalidating any raw JSValue held in a C
// local. Root the exception so it stays valid across the multiple property reads used to build
// the diagnostic message; otherwise the second and later reads dereference a stale pointer.
Comment thread
cleithner-comcast marked this conversation as resolved.
Outdated
SafeJSValue pendingEx(ctx, pendingExRaw);

// Extract exception message if caller wants it
if (outExceptionMsg)
{
std::string exMsg;

// Try ToCString for string exceptions
if (JS_IsString(ctx, pendingEx))
if (JS_IsString(ctx, pendingEx.Get()))
{
JSCStringBuf buf;
const char *str = JS_ToCString(ctx, pendingEx, &buf);
const char *str = JS_ToCString(ctx, pendingEx.Get(), &buf);
if (str)
{
exMsg = str;
}
}
else if (JS_IsPtr(pendingEx))
else if (JS_IsPtr(pendingEx.Get()))
{
// Try "message" property for Error objects
JSValue msgVal = JS_GetPropertyStr(ctx, pendingEx, "message");
if (JS_IsString(ctx, msgVal))
SafeJSValue msgVal(ctx, JS_GetPropertyStr(ctx, pendingEx.Get(), "message"));
if (JS_IsString(ctx, msgVal.Get()))
{
JSCStringBuf buf;
const char *msgStr = JS_ToCString(ctx, msgVal, &buf);
const char *msgStr = JS_ToCString(ctx, msgVal.Get(), &buf);
if (msgStr)
{
exMsg = msgStr;
}
}

// Also try to get stack trace for debugging
JSValue stackVal = JS_GetPropertyStr(ctx, pendingEx, "stack");
if (JS_IsString(ctx, stackVal))
SafeJSValue stackVal(ctx, JS_GetPropertyStr(ctx, pendingEx.Get(), "stack"));
if (JS_IsString(ctx, stackVal.Get()))
{
JSCStringBuf buf;
const char *stackStr = JS_ToCString(ctx, stackVal, &buf);
const char *stackStr = JS_ToCString(ctx, stackVal.Get(), &buf);
if (stackStr)
{
if (!exMsg.empty())
Expand All @@ -280,9 +287,9 @@ bool MQuickJsRuntime::CheckAndClearPendingException(JSContext *ctx, std::string
// useful stack (e.g. exceptions raised from native bindings).
for (const char *prop : {"name", "fileName", "lineNumber"})
{
JSValue propVal = JS_GetPropertyStr(ctx, pendingEx, prop);
SafeJSValue propVal(ctx, JS_GetPropertyStr(ctx, pendingEx.Get(), prop));
JSCStringBuf buf;
const char *propStr = JS_ToCString(ctx, propVal, &buf);
const char *propStr = JS_ToCString(ctx, propVal.Get(), &buf);
if (propStr)
{
exMsg += " | ";
Expand All @@ -296,7 +303,7 @@ bool MQuickJsRuntime::CheckAndClearPendingException(JSContext *ctx, std::string
{
// Try ToCString as fallback for other types
JSCStringBuf buf;
const char *str = JS_ToCString(ctx, pendingEx, &buf);
const char *str = JS_ToCString(ctx, pendingEx.Get(), &buf);
if (str)
{
exMsg = str;
Expand Down
28 changes: 28 additions & 0 deletions core/deviceDrivers/matter/sbmd/mquickjs/MQuickJsStdlib.c
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,34 @@ static JSValue js_gc(JSContext *ctx, JSValue *this_val, int argc, JSValue *argv)
return JS_UNDEFINED;
}

/*
* Date constructor compatibility shim.
*
* Newer generated mqjs_stdlib.h references js_date_constructor, but some
* mquickjs builds do not export an internal implementation symbol. Provide
* the constructor here so the generated stdlib table can always bind it.
Comment thread
cleithner-comcast marked this conversation as resolved.
Outdated
*/
static JSValue js_date_constructor(JSContext *ctx, JSValue *this_val, int argc, JSValue *argv)
{
double epochMs;

if (argc > 0)
{
if (JS_ToNumber(ctx, &epochMs, argv[0]) != 0)
{
return JS_EXCEPTION;
}
}
else
{
struct timeval tv;
gettimeofday(&tv, NULL);
epochMs = (double) tv.tv_sec * 1000.0 + (double) tv.tv_usec / 1000.0;
}

return JS_NewDate(ctx, epochMs);
}

/*
* Date.now() implementation - returns current time in milliseconds.
*/
Expand Down
25 changes: 17 additions & 8 deletions core/deviceDrivers/matter/sbmd/mquickjs/SbmdHandlerInvoker.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,11 @@ namespace barton
std::optional<ParsedResult>
SbmdHandlerInvoker::InvokeHandler(JSContext *ctx, JSValue handler, const SafeJSValue &args)
{
if (JS_IsUndefined(handler))
// Root the handler function: JS_StackCheck below may allocate (grow the JS stack) and the
// moving GC would then relocate an unrooted handler, leaving the pushed argument stale.
SafeJSValue handlerRooted(ctx, handler);

if (JS_IsUndefined(handlerRooted.Get()))
{
icError("handler is undefined");
return std::nullopt;
Expand All @@ -180,7 +184,7 @@ namespace barton

// Stack order for JS_Call: arg, func, this
JS_PushArg(ctx, args.Get());
JS_PushArg(ctx, handler);
JS_PushArg(ctx, handlerRooted.Get());
JS_PushArg(ctx, JS_NULL);

// Arm the execution timeout
Expand Down Expand Up @@ -368,6 +372,9 @@ namespace barton
const std::string &tlvBase64,
JSValue handlerContext)
{
// Root handlerContext: the arg-building allocations below can relocate it under the
// moving GC before it is attached.
SafeJSValue handlerContextRooted(ctx, handlerContext);
SafeJSValue args = BuildBaseArgs(ctx, hctx);

SafeJSValue response = args.AddObject(SBMD_KEY_RESPONSE);
Expand All @@ -383,9 +390,9 @@ namespace barton
response.SetNull(SBMD_KEY_DATA);
}

if (!JS_IsUndefined(handlerContext))
if (!JS_IsUndefined(handlerContextRooted.Get()))
{
args.SetValue(SBMD_KEY_HANDLER_CONTEXT, handlerContext);
args.SetValue(SBMD_KEY_HANDLER_CONTEXT, handlerContextRooted.Get());
}
else
{
Expand All @@ -402,16 +409,17 @@ namespace barton
const std::string &tlvBase64,
JSValue handlerContext)
{
SafeJSValue handlerContextRooted(ctx, handlerContext);
SafeJSValue args = BuildBaseArgs(ctx, hctx);

SafeJSValue attribute = args.AddObject(SBMD_KEY_ATTRIBUTE);
attribute.SetUint32(SBMD_KEY_CLUSTER_ID, clusterId);
attribute.SetUint32(SBMD_KEY_ATTRIBUTE_ID, attributeId);
attribute.SetString(SBMD_KEY_VALUE, tlvBase64.c_str());

if (!JS_IsUndefined(handlerContext))
if (!JS_IsUndefined(handlerContextRooted.Get()))
{
args.SetValue(SBMD_KEY_HANDLER_CONTEXT, handlerContext);
args.SetValue(SBMD_KEY_HANDLER_CONTEXT, handlerContextRooted.Get());
}
else
{
Expand All @@ -428,6 +436,7 @@ namespace barton
int32_t matterCode,
JSValue handlerContext)
{
SafeJSValue handlerContextRooted(ctx, handlerContext);
SafeJSValue args = BuildBaseArgs(ctx, hctx);

SafeJSValue error = args.AddObject(SBMD_KEY_ERROR);
Expand All @@ -443,9 +452,9 @@ namespace barton
error.SetNull(SBMD_KEY_MATTER_CODE);
}

if (!JS_IsUndefined(handlerContext))
if (!JS_IsUndefined(handlerContextRooted.Get()))
{
args.SetValue(SBMD_KEY_HANDLER_CONTEXT, handlerContext);
args.SetValue(SBMD_KEY_HANDLER_CONTEXT, handlerContextRooted.Get());
}
else
{
Expand Down
Loading
Loading