Thank you, this is exactly what it is, I never knew this somehow haha. I guess I’ve never actually tried to use more than three values from an iterator.
Basically, I just need to define some code that will invoke what every metamethod would normally do, and I need to cover every case, no matter the inputs/outputs. I don’t need any access to the metamethod at all, or even what it returns, as long as sandboxed code can’t access what it returns either.
For example, the functionality of each metamethod can be fully described like so:
__index
- return target[index]
__call
- return target(...)
__len
- #target
__add
- return target + subject
__newindex
- target[index] = value
(no return value)
etc, and apparently, since there are only three results, in this case,
__iter
- for a, b, c in target do
(with some coroutine magic)
What is nice about this is that even for something that isn’t a metamethod or behaves completely differently like __metatable
or __mode
, it generalizes:
__metatable
- getmetatable(target)
__mode
- nothing, you can’t access the value of __mode
unless you have a reference to the metatable (just like any other metamethod)
All I need to do is gaurantee I can invoke the above, and insert my own code before and after. This allows me capture and modify the values entering the sandbox, and the values exiting, which essentially means I have complete control over everything the code running inside may/may not do.
My usage of “safely” is very misleading and I didn’t really think it through haha. The thought process was return getmetatable(target).__iter(target)
would describe the metamethod except when target
had __metatable
set on its metatable, and that is “unsafe” because I am not describing the metamethod in a way that I can manage the inputs and outputs. (So, I guess, it literally is an “unsafe” way to represent it in my sandbox, but that makes zero sense without any context whatsoever)
Thank you for the reply, I apologize for my confusion.
P.S.
Here is my current solution as implemented in my code, which I believe covers every case correctly now, if you’re curious about what I am actually even doing.
The rawequal
check covers the fact that the methods being called (:Import()
/:GetClean()
) are capable of returning nil
, and the result is what the value should be functionally equivalent to.
(Except for something functionally equivalent to pairs
where the value becomes nil
, but, there wouldn’t really a be a case could solve this no matter what I do)
-- External -> Sandbox
self:CheckTermination()
self:TrackThread()
local real = self:GetClean(object)
return self:Import(coroutine.wrap(function(object)
local real = self:GetClean(object)
for index, value, extra in real do
index = self:Import(index)
if not rawequal(index, nil) then
coroutine.yield(index, self:Import(value), self:Import(extra))
end
end
end)), self:Import(real)
-- Sandbox -> External
self:CheckTermination()
self:TrackThread()
local real = self:GetClean(object)
return coroutine.wrap(function(object)
local real = self:GetClean(object)
for index, value, extra in real do
index = self:GetClean(index)
if not rawequal(index, nil) then
coroutine.yield(index, self:GetClean(value), self:GetClean(extra))
end
end
end), real
To cover cases that use getmetatable
, rawset
, rawget
, etc, I just wrap functions too. I don’t even need to re-define them, it just works!