924c63fc1be1909f8ab2de587a968dfe1955f499
braney
  Tue Sep 8 07:19:24 2026 -0700
lib: replace eatExcessDotDotInPath with eatExcessDotsInPath, and resolve a root ".."

simplifyPathToDir now canonicalizes with eatExcessDotsInPath instead of the
older eatExcessDotDotInPath, which is removed. simplifyPathToDir was its only
caller. The old routine scanned for the literal string "/../" and so gave
several answers that do not match realpath(3):

../../a                 became  a          (the second .. ate the first)
x/./../y                became  x/y        (.. ate the ".", not the "x")
../..                   became  ""         (an empty path, not the parent)
a/./b                   stayed  a/./b      (single dots were never removed)
h/../../../etc/passwd   became  etc/passwd (the escaping .. were eaten)

The last one is the reason to bother. A caller that wants to know whether a
path stays inside a directory cannot tell from the old result, because a path
that climbs out comes back looking like it stayed in.

Two changes to eatExcessDotsInPath came out of this.

It now drops a ".." at the root of an absolute path, so /../a gives /a and
/.. gives /, as realpath(3) does. Because an absolute path can then never
hold a "..", the guard is exactly "nothing consumed and not absolute".

It also returns "." rather than "" for a non-empty relative path that reduces
to nothing. Callers join the result with "%s/%s", where an empty string names
the file system root instead of the current directory. Without this,
"tdbQuery -root=." looked for /tagTypes.tab and died. The in-place write is
safe because a non-empty input always leaves room for one byte.

The DEBUG selftest is rewritten. Two of its assertions asserted the old wrong
answers for /.. and /../a, a third followed from them, and three asserted the
empty-string result now replaced by ".". Added cases for each item above.

Verified: all 38 selftest cases pass against the built library, and 48089
exhaustively enumerated paths over the alphabets "/.a" and "/.ab" match an
independent model with no ASan or UBSan report. tdbQuery and raSqlQuery give
byte-identical output to the master build across every root form tried,
except two that the master build got wrong.

The other caller of eatExcessDotsInPath is resolveDotDots, which hgTrackUi
uses to canonicalize a fileUrl before checking it against a hub's base
directory. Neither change loosens that check: a path that used to
canonicalize to /../secret now gives /secret, and neither is under a hub base
directory.

refs #37263

diff --git src/lib/osunix.c src/lib/osunix.c
index acb4045456b..3403abd8b38 100644
--- src/lib/osunix.c
+++ src/lib/osunix.c
@@ -1,929 +1,912 @@
 /* Some wrappers around operating-system specific stuff. 
  *
  * This file is copyright 2002 Jim Kent, but license is hereby
  * granted for all use - public, private or commercial. */
 
 #include "common.h"
 #include <dirent.h>
 #include <sys/utsname.h>
 #include <sys/time.h>
 #include <sys/statvfs.h>
 #include <pwd.h>
 #include <termios.h>
 #include "portable.h"
 #include "portimpl.h"
 #include <sys/wait.h>
 #include <regex.h>
 #include <utime.h>
 #include <sys/resource.h>
 
 
 
 
 off_t fileSize(char *pathname)
 /* get file size for pathname. return -1 if not found */
 {
 struct stat mystat;
 ZeroVar(&mystat);
 if (stat(pathname,&mystat)==-1)
     {
     return -1;
     }
 return mystat.st_size;
 }
 
 long long freeSpaceOnFileSystem(char *path)
 /* Given a path to a file or directory on a file system,  return free space
  * in bytes. */
 {
 struct statvfs fi;
 int err = statvfs(path,&fi);
 if (err < 0)
     errnoAbort("freeSpaceOnFileSystem could not statvfs");
 return (long long)fi.f_bsize * fi.f_bavail;
 }
 
 long clock1000()
 /* A millisecond clock. */
 {
 struct timeval tv;
 static long origSec;
 gettimeofday(&tv, NULL);
 if (origSec == 0)
     origSec = tv.tv_sec;
 return (tv.tv_sec-origSec)*1000 + tv.tv_usec / 1000;
 }
 
 void sleep1000(int milli)
 /* Sleep for given number of 1000ths of second */
 {
 if (milli > 0)
     {
     struct timeval tv;
     tv.tv_sec = milli/1000;
     tv.tv_usec = (milli%1000)*1000;
     select(0, NULL, NULL, NULL, &tv);
     }
 }
 
 long clock1()
 /* A seconds clock. */
 {
 struct timeval tv;
 gettimeofday(&tv, NULL);
 return tv.tv_sec;
 }
 
 void uglyfBreak()
 /* Go into debugger. */
 {
 static char *nullPt = NULL;
 nullPt[0] = 0;
 }
 
 char *getCurrentDir()
 /* Return current directory.  Abort if it fails. */
 {
 static char dir[PATH_LEN];
 
 if (getcwd( dir, sizeof(dir) ) == NULL )
     errnoAbort("getCurrentDir: can't get current directory");
 return dir;
 }
 
 void setCurrentDir(char *newDir)
 /* Set current directory.  Abort if it fails. */
 {
 if (chdir(newDir) != 0)
     errnoAbort("setCurrentDir: can't to set current directory: %s", newDir);
 }
 
 boolean maybeSetCurrentDir(char *newDir)
 /* Change directory, return FALSE (and set errno) if fail. */
 {
 return chdir(newDir) == 0;
 }
 
 struct slName *listDir(char *dir, char *pattern)
 /* Return an alphabetized list of all files that match 
  * the wildcard pattern in directory. */
 {
 struct slName *list = NULL, *name;
 struct dirent *de;
 DIR *d;
 
 if ((d = opendir(dir)) == NULL)
     return NULL;
 while ((de = readdir(d)) != NULL)
     {
     char *fileName = de->d_name;
     if (differentString(fileName, ".") && differentString(fileName, ".."))
 	{
 	if (pattern == NULL || wildMatch(pattern, fileName))
 	    {
 	    name = newSlName(fileName);
 	    slAddHead(&list, name);
 	    }
 	}
     }
 closedir(d);
 slNameSort(&list);
 return list;
 }
 
 struct slName *listDirRegEx(char *dir, char *regEx, int flags)
 /* Return an alphabetized list of all files that match 
  * the regular expression pattern in directory.
  * See REGCOMP(3) for flags (e.g. REG_ICASE)  */
 {
 struct slName *list = NULL, *name;
 struct dirent *de;
 DIR *d;
 regex_t re;
 int err = regcomp(&re, regEx, flags | REG_NOSUB);
 if(err)
     errAbort("regcomp failed; err: %d", err);
 
 if ((d = opendir(dir)) == NULL)
     return NULL;
 while ((de = readdir(d)) != NULL)
     {
     char *fileName = de->d_name;
     if (differentString(fileName, ".") && differentString(fileName, ".."))
 	{
 	if (!regexec(&re, fileName, 0, NULL, 0))
 	    {
 	    name = newSlName(fileName);
 	    slAddHead(&list, name);
 	    }
 	}
     }
 closedir(d);
 regfree(&re);
 slNameSort(&list);
 return list;
 }
 
 struct fileInfo *newFileInfo(char *name, off_t size, bool isDir, int statErrno, 
 	time_t lastAccess, time_t creationTime)
 /* Return a new fileInfo. */
 {
 int len = strlen(name);
 struct fileInfo *fi = needMem(sizeof(*fi) + len);
 fi->size = size;
 fi->isDir = isDir;
 fi->statErrno = statErrno;
 fi->lastAccess = lastAccess;
 fi->creationTime = creationTime;
 strcpy(fi->name, name);
 return fi;
 }
 
 int cmpFileInfo(const void *va, const void *vb)
 /* Compare two fileInfo. */
 {
 const struct fileInfo *a = *((struct fileInfo **)va);
 const struct fileInfo *b = *((struct fileInfo **)vb);
 return strcmp(a->name, b->name);
 }
 
 boolean makeDir(char *dirName)
 /* Make dir.  Returns TRUE on success.  Returns FALSE
  * if failed because directory exists.  Prints error
  * message and aborts on other error. */
 {
 int err;
 if ((err = mkdir(dirName, 0777)) < 0)
     {
     if (errno != EEXIST)
 	{
 	perror("");
 	errAbort("Couldn't make directory %s", dirName);
 	}
     return FALSE;
     }
 return TRUE;
 }
 
 
 struct fileInfo *listDirXExt(char *dir, char *pattern, boolean fullPath, boolean ignoreStatFailures)
 /* Return list of files matching wildcard pattern with
  * extra info. If full path is true then the path will be
  * included in the name of each file. */
 {
 struct fileInfo *list = NULL, *el;
 struct dirent *de;
 DIR *d;
 int dirNameSize = strlen(dir);
 int fileNameOffset = dirNameSize+1;
 char pathName[512];
 
 if ((d = opendir(dir)) == NULL)
     return NULL;
 memcpy(pathName, dir, dirNameSize);
 pathName[dirNameSize] = '/';
 
 while ((de = readdir(d)) != NULL)
     {
     char *fileName = de->d_name;
     if (differentString(fileName, ".") && differentString(fileName, ".."))
 	{
 	if (pattern == NULL || wildMatch(pattern, fileName))
 	    {
 	    struct stat st;
 	    bool isDir = FALSE;
 	    int statErrno = 0;
 	    strcpy(pathName+fileNameOffset, fileName);
 	    if (stat(pathName, &st) < 0)
 		{
 		if (ignoreStatFailures)
 		    statErrno = errno;
 		else
     		    errnoAbort("stat failed in listDirX: %s", pathName);
 		}
 	    if (S_ISDIR(st.st_mode))
 		isDir = TRUE;
 	    if (fullPath)
 		fileName = pathName;
 	    el = newFileInfo(fileName, st.st_size, isDir, statErrno, st.st_atime, st.st_ctime);
 	    slAddHead(&list, el);
 	    }
 	}
     }
 closedir(d);
 slSort(&list, cmpFileInfo);
 return list;
 }
 
 struct fileInfo *listDirX(char *dir, char *pattern, boolean fullPath)
 /* Return list of files matching wildcard pattern with
  * extra info. If full path is true then the path will be
  * included in the name of each file. */
 {
 return listDirXExt(dir, pattern, fullPath, FALSE);
 }
 
 time_t fileModTime(char *pathName)
 /* Return file last modification time.  The units of
  * these may vary from OS to OS, but you can depend on
  * later files having a larger time. */
 {
 struct stat st;
 if (stat(pathName, &st) < 0)
     errnoAbort("stat failed in fileModTime: %s", pathName);
 return st.st_mtime;
 }
 
 
 char *getHost()
 /* Return host name. */
 {
 static char *hostName = NULL;
 static char buf[128];
 if (hostName == NULL)
     {
     hostName = getenv("HTTP_HOST");
     if (hostName == NULL)
         {
 	hostName = getenv("HOST");
 	if (hostName == NULL)
 	    {
 	    if (hostName == NULL)
 		{
 		static struct utsname unamebuf;
 		if (uname(&unamebuf) >= 0)
 		    hostName = unamebuf.nodename;
 		else
 		    hostName = "unknown";
 		}
 	    }
         }
     strncpy(buf, hostName, sizeof(buf));
     chopSuffix(buf);
     hostName = buf;
     }
 return hostName;
 }
 
 char *mysqlHost()
 /* Return host computer on network for mySQL database. */
 {
 boolean gotIt = FALSE;
 static char *host = NULL;
 if (!gotIt)
     {
     static char hostBuf[128];
     gotIt = TRUE;
     if (fileExists("mysqlHost"))
 	{
 	return (host = firstWordInFile("mysqlHost", hostBuf, sizeof(hostBuf)));
 	}
     else
 	return (host = getenv("MYSQLHOST"));
     }
 return host;
 }
 
 char *semiUniqName(char *base)
 /* Figure out a name likely to be unique.
  * Name will have no periods.  Returns a static
  * buffer, so best to clone result unless using
  * immediately. */
 {
 int pid = getpid();
 int num = time(NULL)&0xFFFFF;
 char host[512];
 strcpy(host, getHost());
 char *s = strchr(host, '.');
 if (s != NULL)
      *s = 0;
 subChar(host, '-', '_');
 subChar(host, ':', '_');
 static char name[PATH_LEN];
 safef(name, sizeof(name), "%s_%s_%x_%x",
 	base, host, pid, num);
 return name;
 }
 
 char *getTempDir(void)
 /* get temporary directory to use for programs.  This first checks TMPDIR environment
  * variable, then /data/tmp, /scratch/tmp, /var/tmp, /tmp.  Return is static and
  * only set of first call */
 {
 static char *checkTmpDirs[] = {"/data/tmp", "/scratch/tmp", "/var/tmp", "/tmp", NULL};
 
 static char* tmpDir = NULL;
 if (tmpDir == NULL)
     {
     tmpDir = getenv("TMPDIR");
     if (tmpDir != NULL)
         tmpDir = cloneString(tmpDir);  // make sure it's stable
     else
         {
         int i;
         for (i = 0; (checkTmpDirs[i] != NULL) && (tmpDir == NULL); i++)
             {
             if (fileSize(checkTmpDirs[i]) >= 0)
                 tmpDir = checkTmpDirs[i];
             }
         }
     }
 if (tmpDir == NULL)
     errAbort("BUG: can't find a tmp directory");
 return tmpDir;
 }
 
 char *rTempName(char *dir, char *base, char *suffix)
 /* Make a temp name that's almost certainly unique. */
 {
 char *x;
 static char fileName[PATH_LEN];
 int i;
 char *lastSlash = (lastChar(dir) == '/' ? "" : "/");
 for (i=0;;++i)
     {
     x = semiUniqName(base);
     safef(fileName, sizeof(fileName), "%s%s%s%d%s",
     	dir, lastSlash, x, i, suffix);
     if (!fileExists(fileName))
         break;
     }
 return fileName;
 }
 
 void mustRename(char *oldName, char *newName)
 /* Rename file or die trying. */
 {
 int err = rename(oldName, newName);
 if (err < 0)
     errnoAbort("Couldn't rename %s to %s", oldName, newName);
 }
 
 void mustRemove(char *path)
 /* Remove file or die trying */
 {
 int err = remove(path);
 if (err < 0)
     errnoAbort("Couldn't remove %s", path);
 }
 
 void eatSlashSlashInPath(char *path)
 /* Convert multiple // to single // */
 {
 char *s, *d;
 s = d = path;
 char c, lastC = 0;
 while ((c = *s++) != 0)
     {
     if (c == '/' && lastC == c)
         continue;
     *d++ = c;
     lastC = c;
     }
 *d = 0;
 }
 
 void eatExcessDotsInPath(char *path)
 /* Remove . and .. components from path in place using two pointers.
  * Single dots are removed, double dots consume the preceding component
- * unless it is also ".." or doesn't exist (relative path).
+ * unless it is also ".." or doesn't exist (relative path).  A ".." at the
+ * root of an absolute path is dropped, so /../a becomes /a, matching
+ * realpath(3).  Any trailing slash is removed.  A non-empty relative path
+ * that reduces to nothing becomes ".", not the empty string, since callers
+ * join the result with "%s/%s" and an empty string there would name the
+ * file system root.  An empty input stays empty.
  * Assumes no // in input (call eatSlashSlashInPath first). */
 {
 char *src = path;
 char *dst = path;
 boolean absolute = (*src == '/');
+boolean wasEmpty = (*src == 0);
 
 if (absolute)
     *dst++ = *src++;
 
 while (*src)
     {
     /* Find end of this component */
     char *compEnd = strchr(src, '/');
     int compLen;
     if (compEnd)
         compLen = compEnd - src;
     else
         compLen = strlen(src);
 
     if (compLen == 1 && src[0] == '.')
         {
         /* Single dot: skip entirely */
         src += compLen;
         if (*src == '/')
             src++;
         }
     else if (compLen == 2 && src[0] == '.' && src[1] == '.')
         {
         /* Double dot: try to consume previous component */
         boolean consumed = FALSE;
         if (dst > path + (absolute ? 1 : 0))
             {
             /* There is a previous component. Find its start. */
             char *prevEnd = dst - 1;  /* points at trailing '/' (or last char) */
             if (prevEnd > path && *(prevEnd) == '/')
                 prevEnd--;  /* back past trailing slash */
             char *prevSlash = matchingCharBeforeInLimits(path, prevEnd + 1, '/');
             char *prevStart;
             if (prevSlash == NULL)
                 prevStart = path;
             else
                 prevStart = prevSlash + 1;
             int prevLen = (dst - prevStart);
             if (prevStart < dst && *(dst-1) == '/')
                 prevLen--;  /* exclude trailing slash from comparison */
             /* Only consume if previous component is not ".." */
             if (!(prevLen == 2 && prevStart[0] == '.' && prevStart[1] == '.'))
                 {
                 dst = prevStart;
                 /* Also remove the preceding separator if present */
                 if (dst > path + (absolute ? 1 : 0) && *(dst-1) == '/')
                     dst--;
                 consumed = TRUE;
                 }
             }
-        if (!consumed)
+        if (!consumed && !absolute)
             {
-            /* Write ".." forward */
-            if (dst > path + (absolute ? 1 : 0))
+            /* Nothing left to consume in a relative path, so keep the "..".
+             * An absolute path instead drops it, since /.. is / */
+            if (dst > path)
                 *dst++ = '/';
             *dst++ = '.';
             *dst++ = '.';
             }
         src += compLen;
         if (*src == '/')
             src++;
         }
     else
         {
         /* Normal component: copy with leading slash separator if needed */
         if (dst > path + (absolute ? 1 : 0))
             *dst++ = '/';
         memmove(dst, src, compLen);
         dst += compLen;
         src += compLen;
         if (*src == '/')
             src++;
         }
     }
 
-*dst = 0;
-}
-
-static void eatExcessDotDotInPath(char *path)
-/* If there's a /.. in path take it out.  Turns 
- *      'this/long/../dir/file' to 'this/dir/file
- * and
- *      'this/../file' to 'file'  
- *
- * and
- *      'this/long/..' to 'this'
- * and
- *      'this/..' to  ''   
- * and
- *       /this/..' to '/' */
-{
-/* Take out each /../ individually */
-for (;;)
-    {
-    /* Find first bit that needs to be taken out. */
-    char *excess= strstr(path, "/../");
-    char *excessEnd = excess+4;
-    if (excess == NULL || excess == path)
-        break;
-
-    /* Look for a '/' before this */
-    char *excessStart = matchingCharBeforeInLimits(path, excess, '/');
-    if (excessStart == NULL) /* Preceding '/' not found */
-         excessStart = path;
-    else 
-         excessStart += 1;
-    strcpy(excessStart, excessEnd);
-    }
+/* "." rather than "" for something like "a/..", so the result stays a
+ * relative path.  There is always room: the input was at least one byte. */
+if (!absolute && dst == path && !wasEmpty)
+    *dst++ = '.';
 
-/* Take out final /.. if any */
-if (endsWith(path, "/.."))
-    {
-    if (!sameString(path, "/.."))  /* We don't want to turn this to blank. */
-	{
-	int len = strlen(path);
-	char *excessStart = matchingCharBeforeInLimits(path, path+len-3, '/');
-	if (excessStart == NULL) /* Preceding '/' not found */
-	     excessStart = path;
-	else 
-	     excessStart += 1;
-	*excessStart = 0;
-	}
-    }
+*dst = 0;
 }
 
 char *simplifyPathToDir(char *path)
-/* Return path with ~ and .. taken out.  Also any // or trailing /.   
+/* Return path with ~, . and .. taken out.  Also any // or trailing /.
  * freeMem result when done. */
 {
 /* Expand ~ if any with result in newPath */
 char newPath[PATH_LEN];
 int newLen = 0;
 char *s = path;
 if (*s == '~')
     {
     char *homeDir = getenv("HOME");
     if (homeDir == NULL)
         errAbort("No HOME environment var defined after ~ in simplifyPathToDir");
     ++s;
     if (*s == '/')  /*    ~/something      */
         {
 	++s;
 	safef(newPath, sizeof(newPath), "%s/", homeDir);
 	}
     else            /*   ~something        */
 	{
 	safef(newPath, sizeof(newPath), "%s/../", homeDir);
 	}
     newLen = strlen(newPath);
     }
 int remainingLen  = strlen(s);
 if (newLen + remainingLen >= sizeof(newPath))
     errAbort("path too big in simplifyPathToDir");
 strcpy(newPath+newLen, s);
 
-/* Remove //, .. and trailing / */
+/* Remove //, . , .. and trailing / */
 eatSlashSlashInPath(newPath);
-eatExcessDotDotInPath(newPath);
-int lastPos = strlen(newPath)-1;
-if (lastPos > 0 && newPath[lastPos] == '/')
-    newPath[lastPos] = 0;
+eatExcessDotsInPath(newPath);
 
 return cloneString(newPath);
 }
 
 #ifdef DEBUG
 void simplifyPathToDirSelfTest()
 {
 /* First test some cases which should remain the same. */
 assert(sameString(simplifyPathToDir(""),""));
 assert(sameString(simplifyPathToDir("a"),"a"));
 assert(sameString(simplifyPathToDir("a/b"),"a/b"));
 assert(sameString(simplifyPathToDir("/"),"/"));
-assert(sameString(simplifyPathToDir("/.."),"/.."));
-assert(sameString(simplifyPathToDir("/../a"),"/../a"));
 
 /* Now test removing trailing slash. */
 assert(sameString(simplifyPathToDir("a/"),"a"));
 assert(sameString(simplifyPathToDir("a/b/"),"a/b"));
 
+/* Test . removal. */
+assert(sameString(simplifyPathToDir("."),"."));
+assert(sameString(simplifyPathToDir("./"),"."));
+assert(sameString(simplifyPathToDir("./a"),"a"));
+assert(sameString(simplifyPathToDir("a/."),"a"));
+assert(sameString(simplifyPathToDir("a/./b"),"a/b"));
+assert(sameString(simplifyPathToDir("/a/./b"),"/a/b"));
+
 /* Test .. removal. */
-assert(sameString(simplifyPathToDir("a/.."),""));
-assert(sameString(simplifyPathToDir("a/../"),""));
+assert(sameString(simplifyPathToDir("a/.."),"."));
+assert(sameString(simplifyPathToDir("a/../"),"."));
 assert(sameString(simplifyPathToDir("a/../b"),"b"));
 assert(sameString(simplifyPathToDir("/a/.."),"/"));
 assert(sameString(simplifyPathToDir("/a/../"),"/"));
 assert(sameString(simplifyPathToDir("/a/../b"),"/b"));
 assert(sameString(simplifyPathToDir("a/b/.."),"a"));
 assert(sameString(simplifyPathToDir("a/b/../"),"a"));
 assert(sameString(simplifyPathToDir("a/b/../c"),"a/c"));
 assert(sameString(simplifyPathToDir("a/../b/../c"),"c"));
-assert(sameString(simplifyPathToDir("a/../b/../c/.."),""));
+assert(sameString(simplifyPathToDir("a/../b/../c/.."),"."));
 assert(sameString(simplifyPathToDir("/a/../b/../c/.."),"/"));
+assert(sameString(simplifyPathToDir("x/./../y"),"y"));
+
+/* A .. that climbs out of a relative path has to survive, so that a caller
+ * can tell "still inside" from "escaped". */
+assert(sameString(simplifyPathToDir(".."),".."));
+assert(sameString(simplifyPathToDir("../.."),"../.."));
+assert(sameString(simplifyPathToDir("../a"),"../a"));
+assert(sameString(simplifyPathToDir("../../a"),"../../a"));
+assert(sameString(simplifyPathToDir("a/../../b"),"../b"));
+assert(sameString(simplifyPathToDir("h/../../../etc/passwd"),"../../etc/passwd"));
+
+/* A .. at the root of an absolute path is dropped, as in realpath(3). */
+assert(sameString(simplifyPathToDir("/.."),"/"));
+assert(sameString(simplifyPathToDir("/../a"),"/a"));
+assert(sameString(simplifyPathToDir("/a/../../b"),"/b"));
 
 /* Test // removal */
 assert(sameString(simplifyPathToDir("//"),"/"));
-assert(sameString(simplifyPathToDir("//../"),"/.."));
+assert(sameString(simplifyPathToDir("//../"),"/"));
 assert(sameString(simplifyPathToDir("a//b///c"),"a/b/c"));
 assert(sameString(simplifyPathToDir("a/b///"),"a/b"));
 }
 #endif /* DEBUG */
 
 char *getUser()
 /* Get user name */
 {
 uid_t uid = geteuid();
 struct passwd *pw = getpwuid(uid);
 if (pw == NULL)
     errnoAbort("getUser: can't get user name for uid %d", (int)uid);
 return pw->pw_name;
 }
 
 int mustFork()
 /* Fork or abort. */
 {
 int childId = fork();
 if (childId == -1)
     errnoAbort("mustFork: Unable to fork");
 return childId;
 }
 
 int rawKeyIn()
 /* Read in an unbuffered, unechoed character from keyboard. */
 {
 struct termios attr;
 tcflag_t old;
 char c;
 
 /* Set terminal to non-echoing non-buffered state. */
 if (tcgetattr(STDIN_FILENO, &attr) != 0)
     errAbort("Couldn't do tcgetattr");
 old = attr.c_lflag;
 attr.c_lflag &= ~ICANON;
 attr.c_lflag &= ~ECHO;
 if (tcsetattr(STDIN_FILENO, TCSANOW, &attr) == -1)
     errAbort("Couldn't do tcsetattr");
 
 /* Read one byte */
 if (read(STDIN_FILENO,&c,1) != 1)
    errnoAbort("rawKeyIn: I/O error");
 
 /* Put back terminal to how it was. */
 attr.c_lflag = old;
 if (tcsetattr(STDIN_FILENO, TCSANOW, &attr) == -1)
     errAbort("Couldn't do tcsetattr2");
 return c;
 }
 
 boolean isPipe(int fd)
 /* determine in an open file is a pipe  */
 {
 struct stat buf;
 if (fstat(fd, &buf) < 0)
     errnoAbort("isPipe: fstat failed");
 return S_ISFIFO(buf.st_mode);
 }
 
 void childExecFailedExit(char *msg)
 /* Child exec failed, so quit without atexit cleanup */
 {
 fprintf(stderr, "child exec failed: %s\n", msg);
 fflush(stderr);
 _exit(1);  // Let the parent know that the child failed by returning 1.
 
 /* Explanation:
 _exit() is not the normal exit().  
 _exit() avoids the usual atexit() cleanup.
 The MySQL library that we link to uses atexit() cleanup to close any open MySql connections.
 However, because the child's mysql connections are shared by the parent,
 this causes the parent MySQL connections to become invalid,
 and causes the puzzling "MySQL has gone away" error in the parent
 when it tries to use its now invalid MySQL connections.
 */
 
 }
 
 static void execPStack(pid_t ppid)
 /* exec pstack on the specified pid */
 {
 char *cmd[3], pidStr[32];
 safef(pidStr, sizeof(pidStr), "%ld", (long)ppid);
 cmd[0] = "pstack";
 cmd[1] = pidStr;
 cmd[2] = NULL;
 
 // redirect stdout to stderr
 if (dup2(2, 1) < 0)
     errAbort("dup2 failed");
 
 execvp(cmd[0], cmd);
 
 childExecFailedExit(cmd[0]); // cannot use the normal errAbort.
 
 }
 
 void vaDumpStack(char *format, va_list args)
 /* debugging function to run the pstack program on the current process. In
  * prints a message, following by a new line, and then the stack track.  Just
  * prints errors to stderr rather than aborts. For debugging purposes
  * only.  */
 {
 static boolean inDumpStack = FALSE;  // don't allow re-entry if called from error handler
 if (inDumpStack)
     return;
 inDumpStack = TRUE;
 
 fflush(stdout);  // clear buffer before forking
 vfprintf(stderr, format, args);
 fputc('\n', stderr);
 fflush(stderr);
 pid_t ppid = getpid();
 pid_t pid = fork();
 if (pid < 0)
     {
     perror("can't fork pstack");
     return;
     }
 if (pid == 0)
     execPStack(ppid);
 int wstat;
 if (waitpid(pid, &wstat, 0) < 0)
     perror("waitpid on pstack failed");
 else
     {
     if (WIFEXITED(wstat))
         {
         if (WEXITSTATUS(wstat) != 0)
             fprintf(stderr, "pstack failed\n");
         }
     else if (WIFSIGNALED(wstat))
         fprintf(stderr, "pstack signaled %d\n", WTERMSIG(wstat));
     }
 inDumpStack = FALSE;
 }
 
 void dumpStack(char *format, ...)
 /* debugging function to run the pstack program on the current process. In
  * prints a message, following by a new line, and then the stack track.  Just
  * prints errors to stderr rather than aborts. For debugging purposes
  * only.  */
 {
 va_list args;
 va_start(args, format);
 vaDumpStack(format, args);
 va_end(args);
 }
 
 boolean maybeTouchFile(char *fileName)
 /* If file exists, set its access and mod times to now.  If it doesn't exist, create it.
  * Return FALSE if we have a problem doing so (e.g. when qateam is gdb'ing and code tries 
  * to touch some file owned by www). */
 {
 if (fileExists(fileName))
     {
     int ret = utime(fileName, NULL);
     if (ret != 0)
 	{
 	warn("utime(%s) failed (ownership?)", fileName);
 	return FALSE;
 	}
     }
 else
     {
     FILE *f = fopen(fileName, "w");
     if (f == NULL)
 	return FALSE;
     else
 	carefulClose(&f);
     }
 return TRUE;
 }
 
 void touchFileFromFile(const char *oldFile, const char *newFile)
 /* Set access and mod time of newFile from oldFile. */
 {
     struct stat buf;
     if (stat(oldFile, &buf) != 0)
         errnoAbort("stat failed on %s", oldFile);
     struct utimbuf puttime;
     puttime.modtime = buf.st_mtime;
     puttime.actime = buf.st_atime;
     if (utime(newFile, &puttime) != 0)
 	errnoAbort("utime failed on %s", newFile);
 }
 
 boolean isDirectory(char *pathName)
 /* Return TRUE if pathName is a directory. */
 {
 struct stat st;
 
 if (stat(pathName, &st) < 0)
     return FALSE;
 if (S_ISDIR(st.st_mode))
     return TRUE;
 return FALSE;
 }
 
 boolean isRegularFile(char *fileName)
 /* Return TRUE if fileName is a regular file. */
 {
 struct stat st;
 
 if (stat(fileName, &st) < 0)
     return FALSE;
 if (S_ISREG(st.st_mode))
     return TRUE;
 return FALSE;
 }
 
 void mustBeReadableAndRegularFile(char *fileName)
 /* errAbort if fileName is a regular file and readable. */
 {
 // check if file exists and is readable, including the
 // magic "stdin" name.
 FILE *fh = mustOpen(fileName, "r");
 struct stat st;
 if (fstat(fileno(fh), &st) < 0)
     errnoAbort("stat failed on: %s", fileName);  // should never happen
 carefulClose(&fh);
 
 if (!S_ISREG(st.st_mode))
     errAbort("input file (%s) must be a regular file.  Pipes or other special devices can't be used here.", fileName);
 }
 
 char *mustReadSymlinkExt(char *path, struct stat *sb)
 /* Read symlink or abort. FreeMem the returned value. */
 {
 ssize_t nbytes, bufsiz;
 // determine whether the buffer returned was truncated.
 bufsiz = sb->st_size + 1;
 char *symPath = needMem(bufsiz);
 nbytes = readlink(path, symPath, bufsiz);
 if (nbytes == -1) 
     errnoAbort("readlink failure on symlink %s", path);
 if (nbytes == bufsiz)
     errAbort("readlink returned buffer truncated\n");
 return symPath;
 }
 
 char *mustReadSymlink(char *path)
 /* Read symlink or abort. Checks that path is a symlink. 
 FreeMem the returned value. */
 {
 struct stat sb;
 if (lstat(path, &sb) == -1)
     errnoAbort("lstat failure on %s", path);
 if ((sb.st_mode & S_IFMT) != S_IFLNK)
     errnoAbort("path %s not a symlink.", path);
 return mustReadSymlinkExt(path, &sb);
 }
 
 
 void makeSymLink(char *oldName, char *newName)
 /* Return a symbolic link from newName to oldName or die trying */
 {
 int err = symlink(oldName, newName);
 if (err < 0)
      errnoAbort("Couldn't make symbolic link from %s to %s\n", oldName, newName);
 }
 static double timevalToSeconds(struct timeval tv)
 /* convert a timeval structure to seconds */
 {
 return ((double)tv.tv_sec)  + (1.0e-6 * (double)tv.tv_usec);
 }
 
 struct runTimes getTimesInSeconds(void)
 /* get the current clock time since epoch, process user CPU, and system CPU times, all in
  * seconds. */
 {
 struct runTimes rts;
 
 struct timeval tv;
 gettimeofday(&tv, NULL);
 rts.clockSecs = timevalToSeconds(tv);
 
 struct rusage usage;
 getrusage(RUSAGE_SELF, &usage);
 rts.userSecs = timevalToSeconds(usage.ru_utime);
 rts.sysSecs = timevalToSeconds(usage.ru_stime);
 
 return rts;
 }
 
 void setMemLimit(unsigned long maxMem)
 /* Set the maximum amount of memory that the application can use. */
 {
 struct rlimit rlimit;
 
 rlimit.rlim_cur = maxMem;
 rlimit.rlim_max = rlimit.rlim_cur ;
 int val = setrlimit(RLIMIT_AS, &rlimit);
 
 if (val != 0)
     errnoAbort("Couldn't set maxMem to %ld\n", maxMem);
 }