Yes, that's what I suspected. It seemed weird that I needed to un-scale the returned value before passing it to SetWidth. GetStringWidth is only used in conjunction with SetNewLineX in ZO code, maybe they both work with pixels.
Edit: I don't think it multiplies twice by the scale. If the text width is 100 UI units and GetStringWidth returns 150 pixels, but I use the value as UI units in SetWidth(150), the control width will end up being 225 pixels. I only considered the global scale, didn't check whether the control's scale is applied. This is my modified code, seems to work ok so far:
Lua Code:
local textWidth = tabControl:GetStringWidth(title) / GetUIGlobalScale()
tabControl:SetText(title)
tabControl:SetWidth(textWidth + TAB_TITLE_PADDING)