spk/speak commands crash client game if used unproperly!
i recently was fluffing with the spk/speak commands and by curiosity i found a client crash bug, which if you execute this command in your console spk "one(e/30)" it will result in crashing your game assuming its because the division character.
I just confirmed this in Day of Defeat w/ Linux Client:
spk "hello(e30)" ➔ No client crash
spk "hello(e/30)" ➔ Client crashed
@shawns-valve I've confirmed this issue still exists in the latest Half-Life build 19:06:31 Oct 7 2024 (10210) on Windows and Linux. My initial assessment pointed to vgui::Dar<vgui::InputSignal*>::getCount, but it turns out the backtraces in the Windows build are misleading due to stripped symbols, my bad. According to @SamVanheer's analysis, it would seem the crash occurs when the spk command input is processed and the sentence parser either reads past the end of the fixed array or passes a null sound pointer.
spk "a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a"
#00 0058e898 584028d3 0000164c 0058ed80 00000020 hw!vgui::Dar<vgui::InputSignal *>::getCount+0x2340c
#01 0058e8c4 58402618 0000164c 0058ed6c 00000000 hw!vgui::Dar<vgui::InputSignal *>::getCount+0x6aad3
#02 0058efd0 583fe944 593c3840 0058f004 0058f004 hw!vgui::Dar<vgui::InputSignal *>::getCount+0x6a818
#03 0058f048 583fe1a7 00000000 00000006 2d652108 hw!vgui::Dar<vgui::InputSignal *>::getCount+0x66b44
#04 0058f174 583b6065 5944cc40 0000009a 00000099 hw!vgui::Dar<vgui::InputSignal *>::getCount+0x663a7
#05 0058f1ac 583b4e0d 0058f1d4 00000001 00000001 hw!vgui::Dar<vgui::InputSignal *>::getCount+0x1e265
#06 0058f5d8 583b4cfe 5944cc40 00000001 ffffffff hw!vgui::Dar<vgui::InputSignal *>::getCount+0x1d00d
#07 0058f624 583d291e 35bbe7a2 00000000 5851db98 hw!vgui::Dar<vgui::InputSignal *>::getCount+0x1cefe
#08 0058f650 584211d9 35bbe7a2 00000001 0058f66c hw!vgui::Dar<vgui::InputSignal *>::getCount+0x3ab1e
#09 0058f670 584208fb 584c8028 0058f6c4 584202d8 hw!F+0x299
#0a 0058f67c 584202d8 00420000 00426348 00b7ab38 hw!vgui::Dar<vgui::InputSignal *>::getCount+0x88afb
#0b 0058f6c4 0042159c 00420000 00426348 00b7ab38 hw!vgui::Dar<vgui::InputSignal *>::getCount+0x884d8
#0c 0058fba8 00422e48 00420000 00000000 00a9ce29 hl+0x159c
#0d 0058fbf4 756d5d49 00710000 756d5d30 0058fc5c hl!CreateInterface+0x1458
#0e 0058fc04 7761d6db 00710000 6ded18fa 00000000 KERNEL32!BaseThreadInitThunk+0x19
#0f 0058fc5c 7761d661 ffffffff 776643b3 00000000 ntdll!__RtlUserThreadStart+0x2b
#10 0058fc6c 00000000 00422ecc 00710000 00000000 ntdll!_RtlUserThreadStart+0x1b
spk "()"
#00 00cfe460 58402711 00000000 00000000 2dd1e108 hw!vgui::Dar<vgui::InputSignal *>::getCount+0x6782a
#01 00cfeb68 583fe944 593c37c0 00cfeb9c 00cfeb9c hw!vgui::Dar<vgui::InputSignal *>::getCount+0x6a911
#02 00cfebe0 583fe1a7 00000000 00000006 2dd1e108 hw!vgui::Dar<vgui::InputSignal *>::getCount+0x66b44
#03 00cfed0c 583b6065 5944cc40 00000009 00000008 hw!vgui::Dar<vgui::InputSignal *>::getCount+0x663a7
#04 00cfed44 583b4e0d 00cfed6c 00000001 00000001 hw!vgui::Dar<vgui::InputSignal *>::getCount+0x1e265
#05 00cff170 583b4cfe 5944cc40 00000001 ffffffff hw!vgui::Dar<vgui::InputSignal *>::getCount+0x1d00d
#06 00cff1bc 583d291e 3627c5ac 00000000 5851db98 hw!vgui::Dar<vgui::InputSignal *>::getCount+0x1cefe
#07 00cff1e8 584211d9 3627c5ac 00000001 00cff204 hw!vgui::Dar<vgui::InputSignal *>::getCount+0x3ab1e
#08 00cff208 584208fb 584c8028 00cff25c 584202d8 hw!F+0x299
#09 00cff214 584202d8 00420000 00426348 00fa56b0 hw!vgui::Dar<vgui::InputSignal *>::getCount+0x88afb
#0a 00cff25c 0042159c 00420000 00426348 00fa56b0 hw!vgui::Dar<vgui::InputSignal *>::getCount+0x884d8
#0b 00cff740 00422e48 00420000 00000000 00f9ce29 hl+0x159c
#0c 00cff78c 756d5d49 00b94000 756d5d30 00cff7f4 hl!CreateInterface+0x1458
#0d 00cff79c 7761d6db 00b94000 1fc3cc30 00000000 KERNEL32!BaseThreadInitThunk+0x19
#0e 00cff7f4 7761d661 ffffffff 77664398 00000000 ntdll!__RtlUserThreadStart+0x2b
#0f 00cff804 00000000 00422ecc 00b94000 00000000 ntdll!_RtlUserThreadStart+0x1b
@0Ky your assessment is based on the idea that the backtrace is pointing you at the functions responsible but that's incorrect. You're using the 25th anniversary version which strips symbol names for anything not explicitly exported so it's picking the closest exported function and adding an offset to indicate its location (getCount+0x1cefe for example). This code doesn't use VGUI at all.
If you use the steam_legacy branch you get an accurate backtrace:
#0 Q_strlen (str=0xff <error: Cannot access memory at address 0xff>)
at ../engine/common.c:219
#1 0xf63ba449 in VOX_ParseWordParams (
psz=0xff <error: Cannot access memory at address 0xff>,
pvoxword=0xffffc140, fFirst=0) at ../engine/SND_MIX.C:3286
#2 0xf63bab11 in VOX_LoadSound (pchan=0xf751d4a0 <channels+768>,
pszin=0xffffc3e0 "xxtestxx") at ../engine/SND_MIX.C:3487
#3 0xf63b32d1 in S_StartStaticSound (entnum=0, sfxin=<optimized out>,
pitch=100, flags=0, attenuation=1, fvol=1, origin=<optimized out>,
entchannel=<optimized out>) at ../engine/SND_DMA.C:1541
#4 0xf63b355e in S_Say_Reliable () at ../engine/SND_DMA.C:2439
#5 S_Say_Reliable () at ../engine/SND_DMA.C:2397
#6 0xf62c38de in Cmd_ExecuteStringWithPrivilegeCheck (
text=0xffffc5d0 "spk \"a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a\"", bIsPrivileged=<optimized out>, src=<optimized out>)
at ../engine/cmd.c:1257
#7 0xf62c3b38 in Cmd_ExecuteStringWithPrivilegeCheck (bIsPrivileged=true,
src=src_command,
text=0xffffc5d0 "spk \"a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a\"") at ../engine/cmd.c:1216
#8 Cbuf_ExecuteCommandsFromBuffer (buf=0xf6b2cb50 <cmd_text>,
bIsPrivileged=true, nCmdsToExecute=-1) at ../engine/cmd.c:245
#9 0xf62c3b71 in Cbuf_ExecuteFromBuffer (bIsPrivileged=true,
d_text>) at ../engine/cmd.c:259
#10 Cbuf_Execute () at ../engine/cmd.c:269
#11 0xf62db7a3 in _Host_Frame (time=0.0144101987) at ../engine/host.c:1410
#12 0xf62dbc52 in Host_Frame (time=0.0144101987, iState=1, stateInfo=0xffffcb0c) at ../engine/host.c:1548
#13 0xf6308b04 in CEngine::Frame (this=0xf6522aa0 <g_Engine>) at ../engine/sys_engine.cpp:245
#14 0xf630658b in RunListenServer (instance=0x0,
basedir=0x804b220 <szBaseDir> "/home/<username>/.local/share/Steam/steamapps/common/Half-Life",
cmdline=0x8051e30 "/home/<username>/.local/share/Steam/steamapps/common/Half-Life/hl_linux",
postRestartCmdLineArgs=0x804d360 <main::szNewCommandParams> "",
launcherFactory=0x8049350 <CreateInterfaceLocal(char const*, int*)>, filesystemFactory=0xf7543d40 <CreateInterface(char const*, int*)>)
at ../engine/sys_dll2.cpp:955
#15 0x08048d67 in main (argc=1, argv=0xffffcd64) at ../launcher/launcher.cpp:439
VOX_LoadSound breaks up the sentence into words using VOX_ParseString and stores it in the char* rgpparseword[32] array. Your test string has 33 words in it.
When VOX_LoadSound reads from it it will keep reading as long as it finds a non-null pointer in the array. In this case it's reading one past the end and reading 0xFF as the pointer (probably from sxamodrt which is located right behind the array in memory and is set to 255 (0xFF) on startup). The loop needs an extra check to stop at reading 32 words.
Simplified version of the code where the bad pointer gets passed around:
int i = 0;
int j = 0;
for ( char* k = rgpparseword[i]; k; k = rgpparseword[i] )
{
if ( VOX_ParseWordParams(k, &rgvoxword[j], i == 0) )
{
snprintf(pathbuffer, sizeof(pathbuffer), "%s%s.wav", directory, rgpparseword[v11]);
pathbuffer[sizeof(pathbuffer) - 1] = 0;
if ( Q_strlen(pathbuffer) > (sizeof(pathbuffer) - 1) )
continue;
++j;
rgvoxword[j].sfx = S_FindName(pathbuffer, &rgvoxword[j].fKeepCached);
}
++i;
}
This loop needs an extra condition to stop: i < ARRAYSIZE(rgpparseword).
spk "()" backtrace:
#0 S_LoadSound (s=0x0, ch=0x0) at ../engine/SND_MEM.C:147
#1 0xf63badbe in VOX_LoadSound (pchan=0xf751d4a0 <channels+768>,
pszin=0xffffc3e0 "xxtestxx") at ../engine/SND_MIX.C:3532
#2 0xf63b32d1 in S_StartStaticSound (entnum=0, sfxin=<optimized out>,
pitch=100, flags=0, attenuation=1, fvol=1, origin=<optimized out>,
entchannel=<optimized out>) at ../engine/SND_DMA.C:1541
#3 0xf63b355e in S_Say_Reliable () at ../engine/SND_DMA.C:2439
#4 S_Say_Reliable () at ../engine/SND_DMA.C:2397
#5 0xf62c38de in Cmd_ExecuteStringWithPrivilegeCheck (
text=0xffffc5d0 "spk \"()\"", bIsPrivileged=<optimized out>,
src=<optimized out>) at ../engine/cmd.c:1257
#6 0xf62c3b38 in Cmd_ExecuteStringWithPrivilegeCheck (bIsPrivileged=true,
src=src_command, text=0xffffc5d0 "spk \"()\"") at ../engine/cmd.c:1216
#7 Cbuf_ExecuteCommandsFromBuffer (buf=0xf6b2cb50 <cmd_text>,
bIsPrivileged=true, nCmdsToExecute=-1) at ../engine/cmd.c:245
#8 0xf62c3b71 in Cbuf_ExecuteFromBuffer (bIsPrivileged=true,
buf=0xf6b2cb50 <cmd_text>) at ../engine/cmd.c:259
#9 Cbuf_Execute () at ../engine/cmd.c:269
#10 0xf62db7a3 in _Host_Frame (time=0.00587570202) at ../engine/host.c:1410
#11 0xf62dbc52 in Host_Frame (time=0.00587570202, iState=1,
stateInfo=0xffffcb0c) at ../engine/host.c:1548
#12 0xf6308b04 in CEngine::Frame (this=0xf6522aa0 <g_Engine>)
at ../engine/sys_engine.cpp:245
#13 0xf630658b in RunListenServer (instance=0x0,
basedir=0x804b220 <szBaseDir> "/home/<username>/.local/share/Steam/steamapps/common/Half-Life",
cmdline=0x8051e30 "/home/<username>/.local/share/Steam/steamapps/common/Half-Life/hl_linux",
postRestartCmdLineArgs=0x804d360 <main::szNewCommandParams> "",
launcherFactory=0x8049350 <CreateInterfaceLocal(char const*, int*)>,
filesystemFactory=0xf7543d40 <CreateInterface(char const*, int*)>)
at ../engine/sys_dll2.cpp:955
#14 0x08048d67 in main (argc=1, argv=0xffffcd64)
at ../launcher/launcher.cpp:439
It's passing a null sfx pointer to s_LoadSound. There's probably a logic error in the sentence parser that causes it to treat "()" as valid without checking that it actually specifies a word.
A simple null check can fix that.
@SamVanheer You're absolutely correct, thanks for pointing that out. I've made corrections to my comment. Thanks for the detailed explanation.