Skip to content

Fix FormatIface out-of-bounds writes and the InterfaceSearch hang - #284

Open
makadore wants to merge 3 commits into
alliedmodders:masterfrom
makadore:fix/formatiface-and-gcc-build
Open

Fix FormatIface out-of-bounds writes and the InterfaceSearch hang#284
makadore wants to merge 3 commits into
alliedmodders:masterfrom
makadore:fix/formatiface-and-gcc-build

Conversation

@makadore

@makadore makadore commented Aug 24, 2026

Copy link
Copy Markdown

FormatIface writes out of bounds and mangles names, and InterfaceSearch on top of it can spin forever. Neither depends on the compiler; both reproduce under clang and GCC.

Names without a three digit suffix lose a character. The backwards scan stops on the last character instead of past it, so the version field is written one byte early and overwrites it. With the strlen(iface)+4 buffer ISmmAPI::FormatIface documents, ServerGameDLL comes back as ServerGameDL001, and a name shorter than the field is replaced outright — X becomes 001.

A trailing run shorter than three digits writes past the end. The size check asks for maxlength > strlen, while the write needs three digits and a terminator from where the digits start. ServerGameDLL5 into a 15-byte buffer puts two bytes past it; checked against a guard byte, ServerGameDLL55 and A1 do the same.

InterfaceSearch hangs on anything but a three digit suffix. It uses the return of FormatIface directly as its loop condition. FormatIface returns -1 when the version will not fit, -1 is true, so the loop calls the factory with an unchanged name and asks again, forever. It passes strlen+1, which is only ever enough for a name that already ends in three digits, so InterfaceSearch("ServerGameDLL", ...) and InterfaceSearch("ServerGameDLL5", ...) never return. Against a call cap:

input master after
ServerGameDLL005 -> ...008 found found
VEngineServer021 -> ...023 found found
ServerGameDLL hangs gives up
ServerGameDLL5 hangs gives up
ServerGameDLL55 gives up gives up

Tightening the size check makes FormatIface return -1 in one more case, which without the loop fix turns that last row into a hang too, so the two belong together.

Names ending in the usual three digits come out byte-for-byte identical, which covers every interface Metamod looks up itself. Verified on a HL2DM dedicated server: Metamod loads and SourceMod loads under it, identically before and after.

The third commit is separate: every failure path in _Load leaves a reason in the error buffer except the one where the plugin's own Load() returns false, so a plugin that refuses without setting a message is reported with no reason at all. With a plugin that does exactly that, on the same server:

before  [META] Failed to load plugin addons/metamod/bin/refuser:
after   [META] Failed to load plugin addons/metamod/bin/refuser: Plugin refused to load without giving a reason

Fixes #231.

One of the changes is only about GCC and you may not want it: std::unique_ptr<FILE, decltype(&::fclose)> trips -Werror=ignored-attributes, since glibc attributes fclose and an attributed type cannot be a template argument. CI has only ever run clang, so GCC is a configuration AMBuildScript handles but nothing tests, and master does not build with it — FormatIface's snprintf into a four-byte field was the other failure, which the fix above resolves anyway by formatting through a scratch buffer, truncating exactly as before. Happy to drop that commit if GCC is not something you want to carry.

Built and tested both ways: clang 18.1 and GCC 13.3, x86, against hl2sdk-hl2dm.

Two problems, both reachable through ISmmAPI::FormatIface, which documents
that the caller supplies strlen(iface)+4.

With no trailing digits the scan stopped on the last character rather than
past it, so the version field was written one byte early and overwrote it:
"ServerGameDLL" became "ServerGameDL001", and a name shorter than the field
disappeared entirely, "X" becoming "001".

With a trailing run shorter than three digits the size check was too weak.
It only demanded maxlength > strlen, while the write needs three digits and
a terminator from where the digits start, so "ServerGameDLL5" with a
15-byte buffer wrote two bytes past the end. Checked against a guard byte
past the buffer, both cases are gone; the check is now the one the write
actually needs.

Names ending in the usual three digits are byte-for-byte unchanged, which
is every interface metamod looks up itself, including through
InterfaceSearch, which passes strlen+1.

The version field is also formatted through a scratch buffer now. Anything
wider than three digits is still truncated exactly as before, but the
compiler can see the bound, so this no longer fails the build under
-Werror=format-truncation.
std::unique_ptr<FILE, decltype(&::fclose)> does not compile under GCC:
glibc attributes fclose with __nonnull__ and __wur, and an attributed type
as a template argument is -Werror=ignored-attributes. Spelling the deleter
out avoids it. CI builds with clang only, so this went unnoticed.

Separately, every failure path in _Load leaves a reason in the error buffer
except the one where the plugin's own Load() returns false, which is left
as the empty string _Load starts it as. A plugin that refuses without
setting a message is then reported with no reason at all, as in alliedmodders#231.
FormatIface returns -1 when the version field will not fit the buffer it is
given, and the search loop used its return directly as the condition. -1 is
true, so the loop calls the factory with an unchanged name and asks
FormatIface again, forever.

InterfaceSearch passes strlen+1, which is enough only for a name already
ending in three digits, so any other name hangs. "ServerGameDLL" and
"ServerGameDLL5" both spin today; with a guard byte and a call cap they
never come back. Taking only a positive result as "keep going" ends the
loop instead, and names ending in three digits, which is everything
metamod looks up itself, behave exactly as before.

Worth noting separately: strlen+1 does not meet the strlen+4 that
ISmmAPI::FormatIface documents, which is why the search gives up on names
without a three digit suffix rather than counting up from 001 as the
description suggests. Left alone here, since changing it changes which
interfaces get probed.
@makadore
makadore marked this pull request as draft August 24, 2026 22:36
@makadore makadore changed the title Fix FormatIface writing out of bounds, and the GCC build Fix FormatIface out-of-bounds writes and the InterfaceSearch hang Aug 24, 2026
@makadore
makadore marked this pull request as ready for review August 24, 2026 22:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Metamod lists an empty error when failing to load due to the ISmmPlugin::Load() not returning anything.

1 participant