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. *