Tag names containing null characters allow duplicate tags

Reproduction Steps

When a tag name contains a null character, the name is truncated:

CollectionService:AddTag(instance, "ABC\0DEF")
print(CollectionService:HasTag(instance, "ABC\0DEF")) --> false
print(CollectionService:HasTag(instance, "ABC")) --> true

To ensure correctness, an error should be thrown instead.

Furthermore, if a tag is truncated, it can be added to an instance multiple times:

CollectionService:AddTag(instance, "A\0")
CollectionService:AddTag(instance, "A\0")
CollectionService:AddTag(instance, "A\0")
print(CollectionService:GetTags(instance)
--> {
--     [1] = "A",
--     [2] = "A",
--     [3] = "A"
-- }

These duplicate tags are saved within serialized formats. When decoding tags, duplicates should be collapsed down to one.

The following script reproduces the issue:

local CollectionService = game:GetService("CollectionService")

-- Call GetTags, expect result to be equivalent to `want`.
local function passTags(inst, want)
	local got = CollectionService:GetTags(inst)
	if #got == #want then
		local ok = true
		for i, v in ipairs(want) do
			if got[i] ~= v then
				ok = false
				break
			end
		end
		if ok then
			return
		end
	end
	local line = debug.info(2, "l")
	print(string.format("line %d: tags: {%s} ~= {%s}", line, table.concat(got,", "), table.concat(want,", ")))
end

-- Call AddTag, expect no error.
local function passAdd(inst, tag)
	local ok, err = pcall(CollectionService.AddTag, CollectionService, inst, tag)
	if ok then
		return
	end
	local line = debug.info(2, "l")
	print(string.format("line %d: add %q: %s", line, tag, tostring(err)))
end

-- Call AddTag, expect error.
local function failAdd(inst, tag)
	local ok = pcall(CollectionService.AddTag, CollectionService, inst, tag)
	if not ok then
		return
	end
	local line = debug.info(2, "l")
	print(string.format("line %d: add %q: expected error", line, tag))
end

---- Tests

local inst = Instance.new("Folder")
passTags(inst, {})

passAdd(inst, "A")
passAdd(inst, "A")
passTags(inst, {"A"})

passAdd(inst, "B")
passTags(inst, {"A", "B"})

failAdd(inst, "Z\0")
passTags(inst, {"A", "B"})

failAdd(inst, "Z\0")
passTags(inst, {"A", "B"})

failAdd(inst, "Z\0")
passTags(inst, {"A", "B"})

passAdd(inst, "C")
passTags(inst, {"A", "B", "C"})

Expected Behavior

The script prints nothing, indicating no assertions have failed.

Actual Behavior

The following failed assertions are printed:

line 51: add "Z\000": expected error
line 52: tags: {A, B, Z} ~= {A, B}
line 54: add "Z\000": expected error
line 55: tags: {A, B, Z, Z} ~= {A, B}
line 57: add "Z\000": expected error
line 58: tags: {A, B, Z, Z, Z} ~= {A, B}
line 61: tags: {A, B, Z, Z, Z, C} ~= {A, B, C}

Issue Area: Engine
Issue Type: Other
Impact: Low
Frequency: Constantly
Date First Experienced: 2022-04-06
Date Last Experienced: 2022-04-22

2 Likes

Just following up!

We’ve filled a ticket into our internal database for this issue, and will come back with an update as soon as we have news!

Thanks for the report!

2 Likes

It’s been a little while but I just wanted to follow-up and say this was fixed a little while ago so you shouldn’t experience this anymore.

Unfortunately, duplicate tags are still deserialized as-is.

Here’s an example model with two “A” tags:

DuplicateTags.rbxmx (632 Bytes)

Calling GetTags with this instance will return {"A", "A"}.

2 Likes

Are you still having this problem? We hope the bug is fixed now.

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.