Skip to content

Commit 355b432

Browse files
authored
Merge pull request #449 from hanazuki/cycle
reject circular objects from native functions
2 parents 0878a24 + 3ddb775 commit 355b432

4 files changed

Lines changed: 48 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## UNRELEASED
44

5+
- Now circular references in the object returned from a native function are detected and reported as an exception, instead of crashing the VM from stack overflow.
6+
57
## v3.3.1 (2026-05-24)
68

79
- Fix: `importCallback` or `nativeCallback` returning a malformed result now rejects the evaluation with a `JsonnetError` instead of hanging indefinitely.

spec/binding_spec.cjs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,29 @@ describe('binding', () => {
294294
.toBeRejectedWithError(JsonnetError, /trap threw/);
295295
});
296296

297+
it('rejects a circular object returned from a native callback', async () => {
298+
const jsonnet = new Jsonnet();
299+
const circular = {};
300+
circular.self = circular;
301+
jsonnet.nativeCallback('circular', () => circular);
302+
303+
await expectAsync(jsonnet.evaluateSnippet(`std.native("circular")()`))
304+
.toBeRejectedWithError(/Converting circular structure/);
305+
});
306+
307+
it('accepts shared (non-circular) object references from a native callback', async () => {
308+
const jsonnet = new Jsonnet();
309+
const o = { x: 1 };
310+
jsonnet.nativeCallback('inArray', () => [o, o]);
311+
jsonnet.nativeCallback('inObject', () => ({ a: o, b: o }));
312+
313+
let j = await jsonnet.evaluateSnippet(`std.native("inArray")()`);
314+
expect(j).toBeJSON([{ x: 1 }, { x: 1 }]);
315+
316+
j = await jsonnet.evaluateSnippet(`std.native("inObject")()`);
317+
expect(j).toBeJSON({ a: { x: 1 }, b: { x: 1 } });
318+
});
319+
297320
it('reports throwing async native callback', async () => {
298321
const jsonnet = new Jsonnet();
299322

src/JsonValueConverter.cpp

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// SPDX-License-Identifier: MIT
22
#include "JsonValueConverter.hpp"
3+
#include <algorithm>
34
#include <memory>
45
#include <utility>
56

@@ -29,6 +30,12 @@ namespace nodejsonnet {
2930
}
3031

3132
JsonnetJsonValue *JsonValueConverter::toJsonnetJson(Napi::Value v) const {
33+
std::vector<Napi::Object> ancestors;
34+
return toJsonnetJsonImpl(v, ancestors);
35+
}
36+
37+
JsonnetJsonValue *JsonValueConverter::toJsonnetJsonImpl(
38+
Napi::Value v, std::vector<Napi::Object> &ancestors) const {
3239
if(v.IsBoolean()) {
3340
return vm->makeJsonBool(v.As<Napi::Boolean>());
3441
}
@@ -47,22 +54,34 @@ namespace nodejsonnet {
4754
}
4855
if(v.IsArray()) {
4956
auto const array = v.As<Napi::Array>();
57+
if(std::any_of(ancestors.begin(), ancestors.end(),
58+
[&](auto const &a) { return a.StrictEquals(array); })) {
59+
throw Napi::TypeError::New(array.Env(), "Converting circular structure to JSON");
60+
}
61+
ancestors.push_back(array);
5062
auto const json = vm->makeJsonArray();
5163
for(size_t i = 0, len = array.Length(); i < len; ++i) {
52-
vm->appendJsonArray(json, toJsonnetJson(array[i]));
64+
vm->appendJsonArray(json, toJsonnetJsonImpl(array[i], ancestors));
5365
}
66+
ancestors.pop_back();
5467
return json;
5568
}
5669
if(v.IsObject()) {
5770
auto const object = v.As<Napi::Object>();
71+
if(std::any_of(ancestors.begin(), ancestors.end(),
72+
[&](auto const &a) { return a.StrictEquals(object); })) {
73+
throw Napi::TypeError::New(object.Env(), "Converting circular structure to JSON");
74+
}
75+
ancestors.push_back(object);
5876
auto const json = vm->makeJsonObject();
5977
auto const props = object.GetPropertyNames();
6078
for(size_t i = 0, len = props.Length(); i < len; ++i) {
6179
auto const prop = props[i].ToString();
6280
if(object.HasOwnProperty(prop)) {
63-
vm->appendJsonObject(json, prop, toJsonnetJson(object.Get(prop)));
81+
vm->appendJsonObject(json, prop, toJsonnetJsonImpl(object.Get(prop), ancestors));
6482
}
6583
}
84+
ancestors.pop_back();
6685
return json;
6786
}
6887
return vm->makeJsonNull();

src/JsonValueConverter.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// SPDX-License-Identifier: MIT
22
#pragma once
33
#include <memory>
4+
#include <vector>
45
#include <napi.h>
56
#include "JsonnetVm.hpp"
67

@@ -14,6 +15,7 @@ namespace nodejsonnet {
1415

1516
private:
1617
std::shared_ptr<JsonnetVm> vm;
18+
JsonnetJsonValue *toJsonnetJsonImpl(Napi::Value v, std::vector<Napi::Object> &ancestors) const;
1719
};
1820

1921
}

0 commit comments

Comments
 (0)