The following program, despite never adding a function to this table, sometimes has a function in the table. This was happening in a game of mine and I finally decided to try reproducing it with a tiny program and I have been able to.
cls() nds={flr(rnd(4))} for i=1,1000 do for k=1,flr(rnd(10)) do add(nds,flr(rnd(5))) end for j=1,1000 do nds={flr(rnd(4))} for d in all(nds) do if type(d)=='function' then print("function") end end end end |
I would never expect this program to ever print "function" yet, it does, and intermittently.
Adding additional statements (such as printing the values of I and J) seems to alter the number of function collisions.
We've been discussing this on the Discord since last night. The truly trippy thing about it is that it appears Pico-8 is behaving nondeterministically:
cls() total = 0 for i=1,2000 do for j=1,2000 do srand(0xb2e6.0fa3) nds={flr(rnd(4))} for d in all(nds) do if type(d)=='function' then print("function") total+=1 end end total+=0 end end print(total) |
Notice that in this version, the RNG seed is being reset every time inside the innermost loop. And yet, in 26 out of the 4,000,000 iterations, type(d)=='function'.
Further weirdness: see what happens when you remove the seemingly extraneous line "total+=0"
EDIT: Actually, you can remove the call to rnd() entirely (nds={flr(0)}) and flr will still return a function as its second return value in 14 out of 4,000,000 iterations. Spooky stuff here.
The following reproduces:
cls() for i=1,1000 do for j=1,1000 do ndxs={rnd(1)} if (type(ndxs[2]) == 'function') then print("ohno") end end end |
The following does not:
cls() for i=1,1000 do for j=1,1000 do ndxs={(rnd(1))} if (type(ndxs[2]) == 'function') then print("ohno") end end end |
Note the extra parens around the rnd(1).
These make it so that if rnd(1) returns multiple values, only the first one is taken.
This workaround works in the original case as well.
So looks like some functions return an extra function as a second return value? (Could be C code mishaps)
Yeah, this will be the native C routine telling the interpreter that it's returning two (or more) values, but only filling one in and letting the other one be uninitialized memory, probably.
I did some testing and you seem to get the extra function value back from rnd() once every other frame, no matter what the framerate is.
Probably doesn't matter, since the real fix is to make sure it only says it's returning one value.
By the way, the function being returned seems to be the main loop that wraps around our editable source. So, uh, @zep ... that seems very likely to be exploitable in bad ways. You really ought to patch this asap.
Fixed for 0.1.12d. That was an adventure.
I was incorrectly pushing the program object to the Lua stack after skipping a frame due to going over cpu. You'd think that would be quite a noticeable mistake, but the extra stack item is (it seems!) normally consumed somewhere harmlessly, and the stack doesn't grow. But in this case, the program is being interrupted just after a function pushes its return values to the stack, and the extra stack item is tagging along for the ride.
This will also need a cart version bump to fix properly, but I think it's rare enough that I'll put it off until beta. (So, it will be fixed in 0.1.12d, but 0.1.12c users will still be able to load and run 0.1.12d carts and potentially experience the bug)
@Felice I couldn't see the danger in getting the function back, except for being able to send PICO-8 into an infinite loop -- a hard freeze, but no corruption or access to out-of-bounds memory as far as I can tell. Either way I'm planning to release 0.1.12d soon, but perhaps there is something more dangerous I'm missing?
Way to track it down. One of those rare occurrence bugs! We initially didn't believe it was really happening. :D :P
[Please log in to post a comment]