halflife icon indicating copy to clipboard operation
halflife copied to clipboard

spk/speak commands crash client game if used unproperly!

Open Natsheh11 opened this issue 4 years ago • 4 comments

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.

Natsheh11 avatar Aug 21 '21 20:08 Natsheh11

I just confirmed this in Day of Defeat w/ Linux Client:

spk "hello(e30)" ➔ No client crash spk "hello(e/30)" ➔ Client crashed

fysiks1 avatar Aug 21 '21 21:08 fysiks1

@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 avatar Sep 12 '25 08:09 0Ky

@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 avatar Sep 12 '25 08:09 SamVanheer

@SamVanheer You're absolutely correct, thanks for pointing that out. I've made corrections to my comment. Thanks for the detailed explanation.

0Ky avatar Sep 12 '25 11:09 0Ky