JSONEncode and remotes may cause a crash given circular references

Here’s a simple program that doesn’t error, but also doesn’t necessarily work.

local a = {}
a[1] = a
print(game:GetService("HttpService"):JSONEncode(a))

It outputs ["*** certain entries belong to the same table ***"]

If there are multiple instances of such a problem then it will repeat this message. If I add a[2] = a then the output becomes ["*** certain entries belong to the same table ***","*** certain entries belong to the same table ***"]

Here’s a longer program that completely crashes studio because it tries to generate a string that’s millions of characters long.

local t = {}
for y = 1, 7 do
    t[y] = {}
    for x = 1, 7 do
        t[y][x] = {}
    end
end
for y = 1, 6 do
    for x = 1, 6 do
        table.insert(t[y][x], t[y+1][x])
        table.insert(t[y][x], t[y][x+1])
        table.insert(t[y+1][x], t[y][x])
        table.insert(t[y][x+1], t[y][x])
    end
end
game:GetService("HttpService"):JSONEncode(t)

If you were to try to pass such a table through a remote, it will cause a crash and not error. It would make more sense if it errored.

6 Likes

To show what’s going on and why it’s trying to generate a string that’s so long, here’s some more examples. I’ve replaced the repeated "*** certain entries belong to the same table ***" message with an x.

a = {} for i = 1, 5 do a[i] = {a} end

[[x],[x],[x],[x],[x]]

a = {} for i = 1, 5 do a[i] = {a, a} end

[[x,x],[x,x],[x,x],[x,x],[x,x]]

a = {} for i = 1, 5 do a[i] = {a, a[i-1]} end

[[x],[x,[x]],[x,[x,[x]]],[x,[x,[x,[x]]]],[x,[x,[x,[x,[x]]]]]]
In this case, a[i-1] doesn’t directly violate the rule because it doesn’t point to one of its own ancestors, so instead of encoding the warning message, it duplicates the entire encoding of a[i-1]. This recursive behavior is what makes it explode and crash.

I’ve converted the recursive behavior into some sequence generator functions.

powers of 2
local function pow2(n)
	assert(n%1 == 0 and n >= 0, "must be nonnegative integer")
	local a = {}
	for i = 1, n-1 do
		a[i] = {a, a[i-1], a[i-1]}
	end
	local _, count = game:GetService("HttpService"):JSONEncode(a):gsub('"*** certain entries belong to the same table ***"', "")
	return 1 + n + count
end
fibonacci sequence
local function fibonacci(n)
	assert(n%1 == 0 and n >= 0, "must be nonnegative integer")
	if n <= 2 then return n == 0 and 0 or 1 end
	local a = {}
	for i = 1, n-3 do
		a[i] = {a, a[i-1], a[i-2]}
	end
	local _, count = game:GetService("HttpService"):JSONEncode(a):gsub('"*** certain entries belong to the same table ***"', "")
	return n + count
end

you can easily crash studio with something like print(pow2(20)) because it will be generating a string that’s over 50 million characters long.

1 Like