11d5b26d9798079ec5adedc103216ce818e2da5e
max
  Thu Sep 10 05:19:27 2026 -0700
Widen the session data directory hash from 8 to 10 hex characters

sessionDirFromNames() named a session's durable data directory with 8 hex
characters of md5(sessionName).  32 bits was fine while a directory only had
to be unique among one user's sessions, but every anonymous "Share a link"
session belongs to the single reserved user "l", which makes it a birthday
problem across all of them: two unrelated sessions land in the same directory
more likely than not at around 77,000 anonymous sessions, and at 500,000 we
would expect about 29 such pairs.  Two sessions sharing a directory means
cleaning up one takes the other's custom track files with it.

sessionDirHashLen is now 10 (40 bits), which moves the even-odds point past a
million sessions.  The two fan-out levels snapshotSessionDir() added for user
"l" do not help here, since they are a prefix of the same hash: they spread
the entries over 65536 directories but leave the number of distinct leaf names
unchanged.

Both directory layouts change name as a result, so snapshotCleaner would have
walked past anything written earlier and orphaned its files.  Both dir-naming
functions grew a hashLen argument, sessionDirHashLenLegacy records the old
value, and snapshotCleanAnon() now tries the old spelling of both layouts as
well as the new one.

Existing sessions keep working either way: the cart stores the absolute path
of each durable file, so nothing looks a session's directory up by name except
the cleaner.

refs #10138

diff --git src/hg/lib/sessionData.c src/hg/lib/sessionData.c
index dbf31ac2ea4..79b51a0b4ba 100644
--- src/hg/lib/sessionData.c
+++ src/hg/lib/sessionData.c
@@ -436,54 +436,67 @@
             if (unlink(oldFile) != 0)
                 errnoAbort("saveTrackFile: unlink(oldFile='%s') failed", oldFile);
             if (symlink(newFile, oldFile) != 0)
                 errnoAbort("saveTrackFile: symlink(newFile='%s', oldFile='%s') failed",
                            newFile, oldFile);
             fprintf(stderr, "symlinked %s to %s\n", oldFile, newFile);
             }
         cartSetString(cart, varName, newFile);
         }
     }
 else
     cartRemove(cart, varName);
 return newFile;
 }
 
-char *sessionDirFromNames(char *sessionDataDir, char *encUserName, char *encSessionName)
+char *sessionDirFromNamesHashLen(char *sessionDataDir, char *encUserName, char *encSessionName,
+                                 int hashLen)
 /* Alloc and return session data directory:
- * sessionDataDir/2ByteHashOfEncUserName/encUserName/8ByteHashOfEncSessionName
+ * sessionDataDir/2ByteHashOfEncUserName/encUserName/hashLenByteHashOfEncSessionName
  * 2ByteHashOfEncUserName spreads userName values across up to 256 subdirectories because
  * we have ~15000 distinct namedSessionDb.userName values in 2019.
- * 8ByteHashOfEncSessionName because session names can be very long.  */
+ * A hash of encSessionName rather than encSessionName itself because session names can be very
+ * long; hashLen is sessionDirHashLen except when naming a directory written before that was
+ * widened.  */
 {
 char *dir = NULL;
 if (isNotEmpty(sessionDataDir))
     {
     if (sessionDataDir[0] != '/')
         errAbort("config setting sessionDataDir must be an absolute path (starting with '/')");
+    if (hashLen < 1 || hashLen > 32)
+        errAbort("sessionDirFromNamesHashLen: hashLen must be in [1,32], got %d", hashLen);
     char *userHash = md5HexForString(encUserName);
     userHash[2] = '\0';
     char *sessionHash = md5HexForString(encSessionName);
-    sessionHash[8] = '\0';
+    sessionHash[hashLen] = '\0';
     struct dyString *dy = dyStringCreate("%s/%s/%s/%s",
                                          sessionDataDir, userHash, encUserName, sessionHash);
     dir = dyStringCannibalize(&dy);
+    freeMem(userHash);
     freeMem(sessionHash);
     }
 return dir;
 }
 
+char *sessionDirFromNames(char *sessionDataDir, char *encUserName, char *encSessionName)
+/* Alloc and return the per-session data directory under sessionDataDir (hashed by user and session
+ * name), or NULL if sessionDataDir is empty.  errAborts if sessionDataDir is not an absolute path. */
+{
+return sessionDirFromNamesHashLen(sessionDataDir, encUserName, encSessionName, sessionDirHashLen);
+}
+
 static char *dayOfMonthString()
 /* Return a two-character string with the current day of the month [01..31].  Do not free.
  * (Yeah, not [0..30]!  See man 3 localtime.) */
 {
 static char dayString[16];
 time_t now = time(NULL);
 struct tm *tm = localtime(&now);
 safef(dayString, sizeof dayString, "%02u", tm->tm_mday);
 return dayString;
 }
 
 void sessionDataSaveSession(struct cart *cart, char *encUserName, char *encSessionName,
                             char *dbSuffix)
 /* If hg.conf specifies safe places to store files and/or tables that belong to user sessions,
  * then scan cart for trashDir files and/or customTrash tables, store them in safe locations,