b50a995e326f37cb2248e9a04628c15fb1b58215
max
  Mon Sep 14 05:40:04 2026 -0700
My Sessions no longer hides a user's own "__" sessions, refs #38313

Both My Sessions listings decided a row was a share token by testing the
session name for the "__" prefix, so they also hid sessions a user had named
that way themselves - eleven of them on the RR, across four accounts, and
their owners could no longer rename, describe, reshare or delete them.

The prefix is a naming convention we follow, not a namespace we own.  The
authoritative mark is the "snapshotType <type>" line saveSnapshotSession()
already writes into the settings column, so test that instead.  Both queries
already select settings, so no query changes.

snapshotTypeFromSettings() walks the settings lines rather than calling
raFromString(): it runs once per listed row, and a hash there costs ~600ns and
three allocations for every session that has a description, against ~30ns and
none for the walk.  Rows with empty settings, the common case, short-circuit
in both.

diff --git src/hg/lib/snapshotSession.c src/hg/lib/snapshotSession.c
index d7cec8668dd..c2a2aa728c8 100644
--- src/hg/lib/snapshotSession.c
+++ src/hg/lib/snapshotSession.c
@@ -47,30 +47,71 @@
 
 boolean snapshotHasRequired(struct snapshotType *type, struct cart *cart)
 /* Return FALSE when type declares a requiredVar that is missing/empty in cart. */
 {
 if (type == NULL || isEmpty(type->requiredVar))
     return TRUE;
 return isNotEmpty(cartOptionalString(cart, type->requiredVar));
 }
 
 boolean snapshotIsSnapshotName(char *sessionName)
 /* Return TRUE if sessionName is a snapshot name (starts with the "__" prefix). */
 {
 return sessionName != NULL && startsWith(snapshotNamePrefix, sessionName);
 }
 
+char *snapshotTypeFromSettings(char *settings)
+/* See snapshotSession.h.  Reads the one tag by walking the lines rather than calling
+ * raFromString(): this runs once per row in the My Sessions listings, and building a hash there
+ * costs ~600ns and three allocations per session that has any settings at all, against ~30ns and
+ * none for the walk.  Same line semantics as raFromString - leading blanks skipped, tag is the
+ * first word, value is the rest of the line. */
+{
+if (isEmpty(settings))
+    return NULL;
+int tagLen = strlen(snapshotTypeSetting);
+char *s = settings;
+while (s != NULL && s[0] != '\0')
+    {
+    s = skipLeadingSpaces(s);
+    if (s == NULL || s[0] == '\0')
+        break;
+    if (startsWith(snapshotTypeSetting, s) && (s[tagLen] == ' ' || s[tagLen] == '\t'))
+        {
+        char *val = skipLeadingSpaces(s + tagLen);
+        char *lineEnd = strchr(val, '\n');
+        char *type = (lineEnd != NULL) ? cloneStringZ(val, lineEnd - val) : cloneString(val);
+        if (isEmpty(type))
+            freez(&type);
+        return type;
+        }
+    s = strchr(s, '\n');
+    if (s != NULL)
+        s++;
+    }
+return NULL;
+}
+
+boolean snapshotIsSnapshotSettings(char *settings)
+/* See snapshotSession.h. */
+{
+char *type = snapshotTypeFromSettings(settings);
+boolean isSnapshot = (type != NULL);
+freez(&type);
+return isSnapshot;
+}
+
 static char *snapshotSessionDirHashLen(char *sessionDataDir, char *encUserName,
                                        char *encSessionName, int hashLen)
 /* Alloc and return the durable data directory for one snapshot, or NULL if sessionDataDir is empty.
  * Like sessionData's sessionDirFromNames but with two extra hash levels drawn from the session name,
  * so a single high-volume user - the anonymous "l", which owns most snapshots - never accumulates
  * millions of entries in one directory.  Layout:
  *   sessionDataDir / <2hex md5(user)> / <encUser> / <2hex sess> / <2hex sess> / <hashLen hex sess>
  * The two fan-out levels are a prefix of the same hash, so they spread the entries out without
  * adding uniqueness: it is hashLen alone that decides how likely two snapshots are to land in the
  * same directory, which is why it is sessionDirHashLen and not 8.  hashLen is passed in only so
  * that the cleaner can also name a directory written before it was widened.
  * Snapshots use their own layout (not sessionDirFromNames), so this never affects normal sessions. */
 {
 if (isEmpty(sessionDataDir))
     return NULL;
@@ -194,33 +235,36 @@
                   namedSessionTable, encUserName, encSessionName);
 struct sqlResult *sr = sqlGetResult(conn, dy->string);
 char **row;
 if ((row = sqlNextRow(sr)) != NULL)
     {
     firstUse = cloneString(row[0]);
     useCount = atoi(row[1]) + 1;
     }
 sqlFreeResult(&sr);
 
 dyStringClear(dy);
 sqlDyStringPrintf(dy, "DELETE FROM %s WHERE userName='%s' AND sessionName='%s'",
                   namedSessionTable, encUserName, encSessionName);
 sqlUpdate(conn, dy->string);
 
-/* settings records the snapshot type, so the row is self-describing for reconstruction/debugging. */
+/* settings records the snapshot type, so the row is self-describing for reconstruction/debugging.
+ * This is also what marks the row as a share token rather than a session the user saved: the
+ * "__" name prefix cannot say that, because a user may have named a session of their own that way
+ * (refs #38313). */
 char settings[256];
-safef(settings, sizeof settings, "snapshotType %s\n", type->name);
+safef(settings, sizeof settings, "%s %s\n", snapshotTypeSetting, type->name);
 
 dyStringClear(dy);
 sqlDyStringPrintf(dy,
     "INSERT INTO %s (userName, sessionName, contents, shared, firstUse, lastUse, useCount",
     namedSessionTable);
 if (gotSettings)
     sqlDyStringPrintf(dy, ", settings");
 sqlDyStringPrintf(dy, ") VALUES ('%s', '%s', '", encUserName, encSessionName);
 sqlDyAppendEscaped(dy, contents);
 sqlDyStringPrintf(dy, "', 1, ");                 /* shared = 1 (shareable by link) */
 if (firstUse)
     sqlDyStringPrintf(dy, "'%s', ", firstUse);
 else
     sqlDyStringPrintf(dy, "now(), ");
 sqlDyStringPrintf(dy, "now(), %d", useCount);