Loading
Commits on Source 75
-
cznic authored
-
cznic authored
-
cznic authored
-
cznic authored
-
cznic authored
-
cznic authored
-
cznic authored
-
cznic authored
-
cznic authored
-
cznic authored
-
cznic authored
-
cznic authored
-
cznic authored
-
cznic authored
-
cznic authored
-
cznic authored
-
cznic authored
-
cznic authored
-
cznic authored
-
cznic authored
-
cznic authored
-
cznic authored
-
cznic authored
-
cznic authored
-
cznic authored
The eval-timeout deadline (SetEvalTimeout) is an absolute wall-clock time armed by configureInterrupt, which only the Eval* functions call. Call, CallValue and ExecutePendingJobs run JS too but did not re-arm it, so the deadline set by the last Eval kept ticking through idle wall-clock time; once the timeout elapsed, every later Call or drain was interrupted (InternalError: interrupted) even though it had not itself run long. An embedding host that dispatches events via Call after an idle period thus saw JS stop working permanently. Re-arm the deadline in these three entry points, as Eval does; a call that genuinely runs past the timeout is still interrupted. Co-Authored-By:Claude Opus 4.8 (1M context) <noreply@anthropic.com>
-
cznic authored
Co-Authored-By:Claude Opus 4.8 (1M context) <noreply@anthropic.com>
-
cznic authored
The test set a 50ms eval timeout and then invoked quick(), a 100000-iteration loop, expecting the re-armed Call to complete. On slow (emulated) builders the loop itself takes longer than 50ms — ~150ms measured on linux/s390x — so the freshly re-armed deadline legitimately expired mid-loop and the Call was interrupted. That is quick()'s runtime racing the budget, not a re-arm bug. Decouple the assertion from host speed: create the stale deadline with a short timeout as before, then widen the budget (SetEvalTimeout only records the duration; the live deadline stays stale until an entry point re-arms it) before dispatching. A correct Call/ExecutePendingJobs re-arms to the wide budget and any bounded loop finishes well inside it on any hardware; a regressed one keeps the stale, already-elapsed deadline and is interrupted. The runaway guard keeps a short budget so a genuine infinite loop is still interrupted. Verified on linux/s390x: passes 15/15, and still fails as intended when the re-arm in Call/ExecutePendingJobs is reverted. Co-Authored-By:Claude Opus 4.8 (1M context) <noreply@anthropic.com>
-
cznic authored
-
crackcomm authored
-
crackcomm authored
-
cznic authored
Value.Call went straight to VM.call, which does not arm the eval-timeout deadline — every public entry point does that itself. It therefore inherited whatever absolute deadline the last Eval left behind, reintroducing for Value.Call the bug 93986f35 fixed for Call, CallValue and ExecutePendingJobs. After idling past the timeout every callee reaching an interrupt check was interrupted before doing any work, and stayed that way until some other entry point happened to re-arm. Measured with a 50ms timeout and a 120ms idle: a 1e3-iteration loop still ran, 1e4 iterations, a 20k string concat and a JSON round trip of 5k objects all failed. Only callees finishing before the first interrupt check appeared to work, which is what made the omission easy to miss — the whole test suite passed. Also: - Add Value.CallValue, so a function Value can be called for a Value result the way Eval/EvalValue and Call/CallValue pair elsewhere. Calling a function Value and keeping the result native was not possible before. - Report calling a Value with no VM, eg. UndefinedValue or one already freed, as an error rather than panicking on the nil vm. - Reject a reference counted 'this' from a different VM, the same test convertArgs already applies to arguments. Such a 'this' silently read another runtime's heap. - Document that a Go error argument is not converted to a Javascript Error but marshaled to JSON, which for the standard error types yields '{}'. The doc example passed errors.New("fail") and lost the message. - Document that a Value handed to a function registered by RegisterFunc is valid only for the duration of that call, so the promise resolve/reject use case the method was added for needs Dup to retain it. - Deprecate Value.GetProperty, whose 'this' argument is vestigial — the property is always looked up on the receiver — and add Value.Property without it. Tests for all of the above, including the promise use case end to end, wired into TestMemgrind2 for leak coverage. TestMemgrind2 also ran TestGetPropertyValue twice instead of covering TestGetProperty. Co-Authored-By:
Claude Opus 4.8 (1M context) <noreply@anthropic.com>
-
cznic authored
!7 fixed a real bug: passing Javascript null to a function registered by RegisterFunc panicked in reflect for every parameter type except Value, because m.value returns a nil any for null and reflect.ValueOf(nil).Type() panics. It hit RegisterHostFunc and the wantThis path too, so any script could crash the host process just by calling f(null), and the panic leaked memory even when recovered — there was no way to work around it. Restrict the conversion to parameters that can actually hold nil. Using reflect.Zero(typ) for every type also made null land in a non-nilable parameter as that type's zero value, so f(null) became indistinguishable from f(""), f(0) or f(false), and null silently succeeded where undefined already threw a TypeError for the very same parameter. null now converts to nil for the nilable kinds and is a catchable type error otherwise, which is what undefined has always done. Also fix jsValue, which had no case for reflect.Bool: a registered function returning a bool, or an any holding one, failed with "internal error: bool" even though bool converts in every other direction — convertArgs accepts it, m.value produces it and newBool already exists. A registered function returning a map still fails the same way; that one needs a decision about the representation, so it is left alone for now. Tests for all of it, wired into TestMemgrind2. Co-Authored-By:
Claude Opus 4.8 (1M context) <noreply@anthropic.com>
-
cznic authored
jsValue had no case for reflect.Map, so a registered function returning one failed with "internal error: map" — the same gap the preceding commit fixed for bool. Convert map values one by one with jsValue, the way jsArray already converts slice elements, rather than through the JSON round trip used for structs. The JSON route is lossy in ways that matter for a map, which is the natural shape for binding a host object. A *big.Int comes back as a double, turning 1<<62 from 4611686018427387904 into 4611686018427388000, and a Value does not survive at all: Value.MarshalJSON returns what JSON.stringify returns for a function, the bare text "undefined", which is not valid JSON, so the whole conversion fails with "invalid character 'u'". Converted one by one a *big.Int stays a BigInt and a function Value stays live and callable. Keys become property names: strings directly and integer keys stringified, the way JSON does it and the way Javascript itself treats obj[1] and obj["1"] as the same property. Any other key kind is a catchable error. A nil map is null, like a nil pointer and a nil interface already are. The keys are sorted. Javascript property order is observable through Object.keys, JSON.stringify and for-in while Go map iteration order is randomized, so without sorting the same map produced a different object from run to run. encoding/json sorts map keys for the same reason. Like jsArray, jsMap takes over the reference of a Value it is handed, so a Go function putting one in a map has to Dup per call. Co-Authored-By:Claude Opus 4.8 (1M context) <noreply@anthropic.com>
-
cznic authored
-
cznic authored
-
cznic authored
-
cznic authored
-
cznic authored
-
cznic authored
-
cznic authored
-
cznic authored
-
cznic authored
-
cznic authored
-
cznic authored
-
cznic authored
-
cznic authored
-
cznic authored
-
cznic authored
-
cznic authored
-
cznic authored
-
cznic authored
-
cznic authored
-
cznic authored
-
cznic authored
-
cznic authored
-
cznic authored
-
cznic authored
-
cznic authored
-
cznic authored
Eval, Call and the other eval-family methods now return an *Error carrying the exception's Name, Message, Stack, LineNumber, ColumnNumber and FileName. Error() reports the same string as before, so existing callers are unaffected. ErrorFromValue exposes the same information for an error Value held directly. Based on a patch by crackcomm (#13). Co-Authored-By:
Claude Opus 4.8 (1M context) <noreply@anthropic.com>
-
crackcomm authored
-
cznic authored
- newCallback: return a portable zero JSValue instead of the composite literal lib.TJSValue{}, which does not compile on 386/arm where TJSValue is a uint64 (broke build_all_targets). - Value.Then: call configureInterrupt() before running JS, matching the other eval/call entry points, so the eval timeout applies. - PromiseCapability.Dup: drop the always-nil error return; return just *PromiseCapability, matching Value.Dup. - Value.Then: harden the cleanup defer to key off magics (initialised to -1) rather than jv != undefined, so a newCallback failure on the second handler cannot releaseMagic(0). Co-Authored-By:Claude Opus 4.8 (1M context) <noreply@anthropic.com>
-
crackcomm authored
-
crackcomm authored
√ quickjs % go test -run 'TestThen|TestNewPromiseCapability|TestGoFuncReturnsPromise' --- FAIL: TestThen (0.00s) --- FAIL: TestThen/fulfill_and_bypass_reject (0.00s) promise_test.go:237: fulfill="" reject="", want fulfill="good" reject="" FAIL exit status 1 FAIL modernc.org/quickjs 1.197s ?1 quickjs % go test -run 'TestThen|TestNewPromiseCapability|TestGoFuncReturnsPromise' --- FAIL: TestThenLeak (1.07s) promise_test.go:304: Resolve failed at iteration 33802: cannot call a non-function FAIL exit status 1 FAIL modernc.org/quickjs 1.110s Root cause (both tests): uintptr(unsafe.Pointer(&stackVar)) to ccgo-transpiled C code. Go can grow the stack at any safe point — when it does, the uintptr still points to the old freed stack location. Two places had this: 1. ensureCallbackClass (quickjs.go:2105) — TJSClassDef on Go stack → when Fcall pointer went stale, callback objects silently became non-callable → _perform_promise_then set handler to undefined → job ran without calling Go → fulfillStr stayed empty. 2. NewPromiseCapability (promise.go:65) — [2]TJSValue resolving-funcs buffer on Go stack → when stack grew, resolve/reject functions got written to freed memory → resolve function's tag corrupted → "cannot call a non-function". Fix: Both now allocate on the C heap via libc.Xcalloc instead of the Go stack. C heap memory never moves. -
crackcomm authored
-
cznic authored
The C-heap buffer sizing added in NewPromiseCapability and Then used unsafe.Sizeof(lib.TJSValue{}); the lib.TJSValue{} composite literal does not compile on 386/arm, where TJSValue is a uint64. Use the portable sizeofJsValue constant (defined in quickjs32.go/quickjs64.go for exactly this reason), restoring build_all_targets on linux/386 and linux/arm. Co-Authored-By:Claude Opus 4.8 (1M context) <noreply@anthropic.com>
-
crackcomm authored
-
crackcomm authored
Previously, ensureCallbackClass re-allocated C strings, class definitions, and attempted to re-register callbackClassID with JS_NewClass on every callback creation (e.g. Value.Then). Duplicate class registration calls silently failed in QuickJS, causing unnecessary heap allocation overhead. This change: - Adds a callbackClassInit boolean flag to runtime to ensure the JS callback class is registered at most once per runtime on demand. - Checks and propagates error return values from JS_NewClassID and JS_NewClass. - Includes TestThenLeak in the memgrind test suite to verify no memory leaks occur during Promise callback execution.
-
cznic authored
-
cznic authored
freeMagic is now a slice, which unlike the old map does not dedup, so a double release would queue the same magic twice and later hand it to two live callbacks at once: the second registration overwrites the first in goFuncs, then the first callback object's finalizer releases a magic the second is still using. Guard the append on the magic actually being registered, restoring the idempotency the map provided. This also makes a stray releaseMagic(0) a no-op, as magic 0 is never allocated. Also move the libc.Xfree calls in Close back outside goFuncsMu. That mutex is process-global and callGo takes it on every JS->Go call, and callbackFinalizer now re-enters it, so keeping libc calls (and anything that can run JS) out of its critical section avoids both the added contention and a self-deadlock hazard, sync.Mutex not being reentrant. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
cznic authored
Formatting the message on the Go side and then handing it to JS_ThrowTypeError as the format, with a NULL va_list, turns every '%' left in it into a conversion specification: "%s" prints "(null)", "%d" prints 0, "%%" collapses to "%", and "%n" stores through the NULL pointer pop_arg returns from the empty va_list, killing the process. Messages embed module names and error texts originating in the script, so with a loader reporting back the name it was given, import {} from "%n"; is enough to bring the host down. Pass a literal "%s" as the format and the message as its NUL terminated C string argument instead. That is also what QuickJS does from C. TestThrowMessage covers this along with the over-read that formatting on the Go side fixes: a Go string handed to libc.VaList lands in the va_list as its {ptr, len} header, C reads the first word as a char* and, Go strings not being NUL terminated, runs past the end of the string appending whatever sits next to it on the Go heap. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>