67700f0c574a5b49df7e49eff54e16221ff7e6ea
braney
  Tue Sep 8 15:38:12 2026 -0700
cart: accept a session file path spelled through a symlinked config directory

The file-name check added in 9ad04e0a0b0 compares a path against trashDir(),
sessionDataDir, sessionDataDirOld and myVariantsDataDir as plain strings.  Those
directories are often reached through a symlink, and sessionData.c stores the
resolved spelling whenever the trash file it is saving is already a relative
symlink, so a saved session can hold either spelling.  On the RR /userdata is a
symlink to /shared/userdata and 583 saved multi-region sessions hold the
resolved form, so hgTracks dropped multiRegionsBedUrl and said "No BED or BED
URL specified" rather than drawing the saved view.

Accept a path under a trusted directory or under whatever that directory
resolves to.  Only the configured directory is resolved.  Resolving the value
itself is not an option, because a trash file is deliberately a symlink into
session storage.

Use a stack buffer rather than realpath(dir, NULL): that allocates with the
system malloc, and freeMem() goes through the kent handler stack, which under
pushCarefulMemHandler() reads a block header that is not there.  hgc, hgTables,
hgVai, hgLogin and hgLinkIn all install that handler before loading a cart.

refs #38303, refs #37623

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

diff --git src/hg/lib/trashDir.c src/hg/lib/trashDir.c
index 9a95956fbbb..8b1d9967939 100644
--- src/hg/lib/trashDir.c
+++ src/hg/lib/trashDir.c
@@ -1,21 +1,22 @@
 /* trashDir.c - temporary file creation and directory creation in /trash */
 
 /* Copyright (C) 2014 The Regents of the University of California 
  * See kent/LICENSE or http://genome.ucsc.edu/license/ for licensing information. */
 
 
+#include <limits.h>
 #include "common.h"
 #include "hash.h"
 #include "hgConfig.h"
 #include "portable.h"
 #include "trashDir.h"
 
 static boolean hasDotDotComponent(char *path)
 /* Return TRUE if any '/'-separated component of path is exactly "..", which is the only
  * way a path can climb back above a directory it appears to be inside of. */
 {
 char *s = path;
 while (s != NULL && s[0] != '\0')
     {
     if (s[0] == '.' && s[1] == '.' && (s[2] == '/' || s[2] == '\0'))
         return TRUE;
@@ -31,52 +32,69 @@
  * boundary, so a sibling directory whose name merely starts the same way (trashBackup next
  * to trash) does not match.  ".." below the boundary is refused. */
 {
 if (isEmpty(dir) || isEmpty(path))
     return FALSE;
 int dirLen = strlen(dir);
 while (dirLen > 0 && dir[dirLen-1] == '/')
     dirLen -= 1;
 if (dirLen == 0)
     return FALSE;
 if (strncmp(path, dir, dirLen) != 0 || path[dirLen] != '/' || path[dirLen+1] == '\0')
     return FALSE;
 return !hasDotDotComponent(path + dirLen + 1);
 }
 
+static boolean pathIsUnderDirOrItsTarget(char *dir, char *path)
+/* pathIsUnderDir(), but also accept a path under the directory that dir resolves to.  A
+ * configured directory is often reached through a symlink, and sessionData.c stores the
+ * resolved spelling of a path whose file is already a symlink, so both spellings turn up in
+ * saved sessions.  Only dir is resolved.  Resolving path would defeat the check, because a
+ * trash file is often a symlink into session storage on purpose. */
+{
+if (pathIsUnderDir(dir, path))
+    return TRUE;
+if (isEmpty(dir))
+    return FALSE;
+char resolved[PATH_MAX];
+if (realpath(dir, resolved) == NULL)
+    return FALSE;
+return pathIsUnderDir(resolved, path);
+}
+
 boolean isTrashPath(char *path)
 /* Return TRUE if path names a file inside the trash directory. */
 {
-return pathIsUnderDir(trashDir(), path);
+return pathIsUnderDirOrItsTarget(trashDir(), path);
 }
 
 boolean isTrashOrSessionDataPath(char *path)
 /* Return TRUE if path is under the trash directory, or under one of the durable session-data
  * directories that trash files are moved to when a session is saved. */
 {
 return isTrashPath(path) ||
-       pathIsUnderDir(cfgOption("sessionDataDir"), path) ||
-       pathIsUnderDir(cfgOption("sessionDataDirOld"), path);
+       pathIsUnderDirOrItsTarget(cfgOption("sessionDataDir"), path) ||
+       pathIsUnderDirOrItsTarget(cfgOption("sessionDataDirOld"), path);
 }
 
 boolean isServerUserFilePath(char *path)
 /* Return TRUE if path is under one of the directories where the server keeps files it made
  * for a user: the trash directory, the session-data directories, or a per-feature data
  * directory such as myVariantsDataDir. */
 {
 return isTrashOrSessionDataPath(path) ||
-       pathIsUnderDir(cfgOption("myVariantsDataDir"), path);
+       pathIsUnderDirOrItsTarget(cfgOption("myVariantsDataDir"), path);
 }
 
 boolean isRemoteUrl(char *path)
 /* Return TRUE if path is a URL fetched over the network rather than a file name.  Only the
  * three protocols the tree actually fetches count; hasProtocol() in net.c is a test for
  * "://" anywhere in the string, which is too loose to decide anything on. */
 {
 return startsWith("http://", path) ||
        startsWith("https://", path) ||
        startsWith("ftp://", path);
 }
 
 boolean isServerUserFileOrUrl(char *path)
 /* Return TRUE if path is either a remote URL or a file the server made for a user.
  *