Skip to content

Filesystem Purity

Victor Roemer edited this page Aug 4, 2022 · 5 revisions

Filesystem Purity

The filesystem purity is an enforcement where the remote server requires clients to only load the same paks. This is both an anti-cheat as it prevents loading hacked cgames and ui modules, and it enforces compatibility with modded servers.

NOTE: A server can theoretically transmit up to 2 x 4096 pak names total. but unlikely as the MAX_GAMESTATE_CHARS is 16000 bytes.

The client setups 2 tables of packs, based on the serverinfo received at connection time, loaded paks and referenced paks.

Loaded Paks:

from the source code

If the string is empty, all data sources will be allowed. If not empty, only pk3 files that match one of the space separated checksums will be checked for files, with the exception of .cfg and .dat files.

In code, this table is

fs_serverPakNames[4096] // pak name
fs_serverPaks[4096]     // pak checksum

NOTE: This appears to be the most important, see FS_ReorderPurePaks below

Referenced Paks:

From the source code:

The checksums and names of the pk3 files referenced at the server are sent to the client and stored here. The client will use these checksums to see if any pk3 files need to be auto-downloaded.

fs_serverReferencedPakNames[4096] // pak name
fs_serverReferencedPaks[4096]     // pak checksum

NOTE: These 2 lists do share some overlap

(char *) $124 = 0x0000000101e00218 "slacker/vms-gpp1-bunker_220207-144309"
p fs_serverPakNames[0]
(char *) $125 = 0x0000000101dfdcb0 "vms-gpp1-bunker_220207-144309"
p fs_serverPakNames[1]
(char *) $126 = 0x0000000101dfdd10 "data-bunker"
p fs_serverReferencedPakNames[1]
(char *) $127 = 0x0000000101e00280 "slacker/data-bunker"
p fs_numServerReferencedPaks

However, it’s not perfect and even possibly problematic

p fs_serverReferencedPakNames[2]
(char *) $132 = 0x0000000101e002d0 "base/map-atcs-1.1.0"
p fs_serverPakNames[2]
(char *) $133 = 0x0000000101dfdd58 "vms-1.1.0"

Connection, Pack Loading

A server will also transmit a "Checksum Feed Key" which is pseudo-random generated. A server may generate multiple of these over the lifetime of a clients connection and it’s algorithm tries to guarentee that any new key is greater than the value of the previous.

Client Connection state:

clientconnection_t:
    int checksumFeed;  // from the server for checksum calculations

After the client parses the pak names and checksums, it will begin the operation of loading, starting with a call to FS_ConditionalRestart, passing the checksum feed with it.

If the checksum feed read from the gamestate does not match what the client previously obtained the whole Filesystem state is restarted. Otherwise, the client is assumed to have the correct state, and will only attempt to re-order the list of packs to match that of the server.

  if (checksumFeed != fs_checksumFeed)
      FS_Restart(checksumFeed);

  else if (fs_numServerPaks && !fs_reordered)
      FS_ReorderPurePaks();

The ReorderingPurePaks function will iteratively search for the matching "pure" paks in the clients loaded fs_searchpaths

  auto p_insert_index = &fs_searchpaths;
  for (int i = 0; i < fs_numServerPaks; i++)
  {
      // track the pointer-to-current-item
      auto p_previous = p_insert_index;
      for (auto s = *p_insert_index; s; s = s->next)
      {
          // the part of the list before p_insert_index has been sorted already
          if (s->pack && fs_serverPaks[i] == s->pack->checksum)

Downloads

After reshuffling the entire filesystem view of the world, call to CL_InitDownloads()

NOTE: Cascading pile of bullshit

A call to FS_ComparePaks will check that all "ReferencedPaks" exist in the client (checksum matches). If they are not found, the paks are converted into "download strings" in the format:

@pakName.pk3@pakName2.pk3@pakName3.pk3

The client will also compare the name of the paks to all pack to any packages in gamedir folders. (FS_SV_FileExists) If a conflicting package name is found, the downloaded pack will get a new name

              {
                  char st[MAX_ZPATH];
                  // We already have one called this, we need to download it to another name
                  // Make something up with the checksum in it
                  Com_sprintf(st, sizeof(st), "%s.%08x.pk3", fs_serverReferencedPakNames[i],
                      fs_serverReferencedPaks[i]);
                  Q_strcat(neededpaks, len, st);
              }

*WARNING: MAX_ZPATH is 256, which will likely cause truncation and allow not so arbitrary file overwrites.

WARNING: The logic to detect overflowing the buffer is super sketchy

                // Find out whether it might have overflowed the buffer and don't add this file to
                // the
                // list if that is the case.
                if (strlen(origpos) + (origpos - neededpaks) >= (len - 1))
                {
                    *origpos = '\0';
                    break;
                }

The resulting output from ComparePaks, of paks that must be downloaded is stored in clc.downloadList

p clc.downloadList
(char[1024]) $137 = "@slacker/vms-gpp1-bunker_220207-144309.pk3@slacker/vms-gpp1-bunker_220207-144309.pk3@slacker/data-bunker.pk3@slacker/data-bunker.pk3\0"

Server state

sv_init.cpp

628:    sv.checksumFeed = (((int)rand() << 16) ^ rand()) ^ Com_Milliseconds();
629:    FS_Restart(sv.checksumFeed);
644:    sv.checksumFeedServerId = sv.serverId;

sv_client.cpp:

659:	MSG_WriteLong( &msg, sv.checksumFeed);
1117:			// we may get incoming cp sequences from a previous checksumFeed, which we need to ignore
1119:			if (atoi(pArg) < sv.checksumFeedServerId)
1206:			nChkSum1 = sv.checksumFeed;

NOTE: The code that implements both of these are duplicated code that should be made DRY.