Preface
This code looks innocent enough, but it might not be.
local tree = workspace:FindFirstChild("Tree")
local branch = tree:FindFirstChild("Branch")
-- We know there is a branch Part in the tree here
local branchColor = branch.Color
The takeaway is this: You should be making separate folders inside an Instance
for anything that will ever be moved or destroyed without its parent. If this isn’t doable yet, at least use a Folder
for character models.
All credit goes to the original discovery by @jakedies. I made this post only to add visibility, make the recommendation obvious for beginners, highlight the most dangerous interaction, and explicitly mention that indexing is impacted.
Children order has an exception
Children of a parent are normally ordered based on when they were parented to it, with the children parented earlier coming before the ones parented later. This is why indexing workspace.Tree
gives you the tree you inserted in Studio, and not the character model of the played named “Tree”. When a parent has more than 20 children and one is moved or destroyed, it is removed through a “fast removal” roughly similar to this Lua code:
local function fastRemove(children, index)
children[index] = children[#children]
children[#children] = nil
end
This means that a child parented to some Instance
later can be ordered before one that was parented earlier.
This applies everywhere:
- Indexing
thing.Child
,thing["Child"]
- Child returned by
thing:FindFirstChild()
-
thing:GetChildren()
order -
thing:GetDescendants()
order
Workspace:FindFirstChild(“ChildHere”) and Workspace.ChildHere is the most dangerous
By default, a character model’s name is the player’s name, and it is parented to Workspace
. This issue means that if any code moves or destroys any direct child of Workspace
, future accesses to a child of Workspace
, even one with a different name, can give you a character model instead. This means that bad actors can rename themselves to the names of other Workspace
children to cause your game to error.
Demonstration
local firstChild = Instance.new("Model", workspace)
for index = 1, 20 do
Instance.new("Model", workspace)
end
local targetValue = Instance.new("BoolValue")
targetValue.Name = "TargetValue"
targetValue.Parent = workspace
-- We expect to find targetValue when we do workspace:FindFirstChild("TargetValue"), not this
local fakeTargetValue = Instance.new("Model")
fakeTargetValue.Name = "TargetValue"
fakeTargetValue.Parent = workspace
-- This will cause fakeTargetValue to be swapped in child order with the first model we added
firstChild:Destroy()
-- Changing this to workspace.TargetValue won't help
local accessedInstance = workspace:FindFirstChild("TargetValue")
-- accessedInstance will be fakeTargetValue, and this will error!
local value = accessedInstance.Value
Prospects for this behavior being changed
“Properly” removing a child would be too expensive, so the core behavior here is unlikely to change. Roblox could eliminate the easiest to abuse case by adding a special Folder
for characters to be parented under in Workspace
, which would also have prevented the close call with the sibling instance order change.