Page MenuHomePhabricator

Scribunto `frame.args[]` single-item indexing (`__index`) fails for numeric keys within the ranges of ±10^14 to ±2^53 due to a format mismatch in serialization
Open, MediumPublicBUG REPORT

Description

In Lua code such as

for key, value1 in pairs(frame.args) do
    value2 = frame.args[key]
    ...

value2 will be nil when the argument name is a string containing an integer between 10^14 and 2^63, as with:

{{#invoke:module|func|100000000000000=foo}}

This may be surprising for users.

LuaEngine::getAllExpandedArguments() returns the arguments with the name in the array key, so PHP implicitly converts the name to an integer which is then converted to a Lua number. In mw.lua, getExpandedArgument() normalizes the name with name = tostring( name ), however in Lua 5.1 tostring() uses sprintf("%.14g") and thus the name is converted to 1e+14.

14 digits of precision is unusually few for a double-precision number, and this is bound to cause problems elsewhere. We can in principle make tostring() do whatever we want. Lua 5.3 introduced an integer subtype, allowing tostring() to convert large integers to strings without loss:

Lua 5.3.6  Copyright (C) 1994-2020 Lua.org, PUC-Rio
> =tostring(100000000000000)
100000000000000
> =tostring(100000000000000.0)
1e+14

And Lua 5.5 introduced iterative conversion of float to string with the goal of lossless conversion:

Lua 5.5.1  Copyright (C) 1994-2026 Lua.org, PUC-Rio
> tostring(100000000000000.0)
100000000000000.0

We can in principle backport something like this. Or we can upgrade Lua (T178146) which will implicitly fix it.

Alternatively:

  • __pairs could convert numeric keys to strings using an appropriate format. PHP could help with this by providing named and numbered arguments in separate arrays. This could improve performance since PPTemplateFrame_Hash::getArguments() needs to merge the two underlying arrays.
  • mw.lua's getExpandedArgument() could do a better job of conversion. But frame.args[1e14] is inherently weird and broken. Numbered arguments are allocated sequentially so cannot have large indexes. Named arguments have string names.
  • The argument name could be passed back to PHP as a number, since PHP does a better job of converting numbers to strings than Lua.

As a workaround, modules can convert the name to a string themselves, with e.g. ("%d"):format(key).

Event Timeline

Uzume triaged this task as Medium priority.EditedJul 12 2026, 1:38 AM
Uzume added a subscriber: tstarling.

The issue was introduced by @tstarling in https://gerrit.wikimedia.org/r/8437 submitted 2012-05-26. It currently resides at: includes/Engines/LuaCommon/lualib/mw.lua@line 230. Using tostring() for numbers, happens to work for integer values below ±10^14 because Lua converts these to integer-looking strings that PHP allows in its lookup function MediaWiki\Parser\PPFrame::getArgument(), however using strings instead of numbers also causes Lua-side argCache to be used in a way that is inconsistent with how they are cached via a pairs traversal.

It might make sense to attempt to populate the argCache in getExpandedArgument (which uses mw_interface.getExpandedArgument) and args_mt.__pairs (which uses mw_interface.getAllExpandedArguments) with both the number value and an integer-looking string generated by code like ("%d"):format(name), however one might want to ensure the value can be round-tripped by seeing if the value is the same when reconverted back to a number (because that string.format format clips off numbers). I imagine the most sane thing would be to ignore the integer-looking strings and just let them pass-through if given such, meaning they are cached separately.

The simplest fix would be to stop using tostring on number parameter keys in getExpandedArgument that way it should work and the arg caching should align with the existing code on the __pairs side.

Impact: This breaks core Lua table API consistency contracts. It silently neutralizes validation logic in custom frameworks and downstream code interacting with Module:Arguments, which relies entirely on indexed-lookups via __index to preserve lazy expansion.

Please be more specific about the impact. Are any actual humans affected by this issue?

I rewrote the task description since I am not a fan of that kind of AI slop.

Please be more specific about the impact. Are any actual humans affected by this issue?

I imagine not many users are using wikitext parameter names that look like numbers within the ranges affected but this can easily be seen in Scribunto with little issue if you look for it.

I agree with your assessment that frame.args[1e14] is "inherently weird and broken" but that is a direct result of Scribunto PHP code that does not preserve the difference in unnamed (numbered) vs. named arguments a la PPFrame::getNumberedArguments and PPFrame::getNamedArguments. This means that even though the parser knows the difference, Scribunto modules cannot tell the difference between: {{#invoke:module|func| a | b }} and {{#invoke:module|func| a |2= b }} (with the minor exception of looking at whitespace removal in the argument value when applicable like in this example). It is currently impossible to traverse just the unnamed (numbered) arguments in Scribunto modules because they have no ability to tell the difference between these.

Your alternative comments about __pairs possible performance improvements by returning separate arrays for numbered and named arguments goes straight to the heart of this. getAllExpandedArguments in includes/Engines/LuaCommon/LuaEngine.php could for example be replaced with a getExpandedUnnamedArguments and a getExpandedNamedArguments or similar.

I might write another ticket requesting an API that preserves unnamed (numbered) and named arguments when I get a chance. Apparently I need practice writing them.