6

Running strace on the bash shell that is instructed to run mkdir provided this output which shows lots of stats prior to execve the actual mkdir binary:

BASH$> strace -f sh -c "bash -c \"mkdir /tmp\" 2>&1 | nl | grep -e "execve\|stat\|access" 
[.....]
  2766  [pid 17371] stat(".", {st_mode=S_IFDIR|0750, st_size=17262, ...}) = 0
  2767  [pid 17371] stat("/usr/local/sbin/mkdir", 0x7ffd87aad0a0) = -1 ENOENT      2767 (No such file or directory)
  2768  [pid 17371] stat("/usr/local/bin/mkdir", 0x7ffd87aad0a0) = -1 ENOENT (No such file or directory)
  2769  [pid 17371] stat("/usr/bin/mkdir", {st_mode=S_IFREG|0755, st_size=51136, ...}) = 0
  2770  [pid 17371] stat("/usr/bin/mkdir", {st_mode=S_IFREG|0755, st_size=51136, ...}) = 0
  2771  [pid 17371] access("/usr/bin/mkdir", X_OK) = 0
  2772  [pid 17371] stat("/usr/bin/mkdir", {st_mode=S_IFREG|0755, st_size=51136, ...}) = 0
  2773  [pid 17371] access("/usr/bin/mkdir", R_OK) = 0
  2774  [pid 17371] stat("/usr/bin/mkdir", {st_mode=S_IFREG|0755, st_size=51136, ...}) = 0
  2775  [pid 17371] stat("/usr/bin/mkdir", {st_mode=S_IFREG|0755, st_size=51136, ...}) = 0
  2776  [pid 17371] access("/usr/bin/mkdir", X_OK) = 0
  2777  [pid 17371] stat("/usr/bin/mkdir", {st_mode=S_IFREG|0755, st_size=51136, ...}) = 0
  2778  [pid 17371] access("/usr/bin/mkdir", R_OK) = 0
  2779  [pid 17371] execve("/usr/bin/mkdir", ["mkdir", "/tmp"], 0x557ec7e15920 /* 5 vars */) = 0

My question is: Is it normal (and if so for what reason) is /usr/bin/mkdir stat()ed a lot? The output lines are numbered and in particular I wonder what sense will line 2776 make once 2771 was run already. Also I was under the impression bash could have saved all systemcalls from 2770 onward to the final execve as the stat should have provided the information at once? What am I missing?

I have since sought an explanation and checked how an alternative shell the dash shell would behave it also shows some stat()ing :

DASH$> strace -f sh -c "dash -c \"mkdir /tmp\" 2>&1 | nl | grep -e "execve\|stat\|access" 
[....]
  2792  [pid 17372] stat("/usr/local/sbin/mkdir", 0x7ffc66010b50) = -1 ENOENT (No such file or directory)
  2793  [pid 17372] stat("/usr/local/bin/mkdir", 0x7ffc66010b50) = -1 ENOENT (No such file or directory)
  2794  [pid 17372] stat("/usr/sbin/mkdir", {st_mode=S_IFREG|0755, st_size=51136, ...}) = 0
  2795  [pid 17372] execve("/usr/sbin/mkdir", ["mkdir", "/run"], 0x55d8d3453bb8 /* 6 vars */) = 0

I am aware that the lines 2792,2793, similar to the lines 2767, 2768are because of searching the executable in the various directories in the currentPATH`.

If this is discounted then dash only does one single stat and bash does 10. Again raising the question: is this normal?

UPDATE: There was more geteuid(),getguid(),getuid() and getgid() mingled in the bash stats

BASH$>strace -f sh -c "bash -c \"mkdir /tmp\"" 2>&1 | grep -e "execve\|stat\|access\|geteuid\|getegid\|getuid\|getgid" 
[....]
[pid 24534] stat("/usr/local/bin/mkdir", 0x7fffda480f30) = -1 ENOENT (No such file or directory)
[pid 24534] stat("/usr/local/sbin/mkdir", 0x7fffda480f30) = -1 ENOENT (No such file or directory)
[pid 24534] stat("/usr/bin/mkdir", {st_mode=S_IFREG|0755, st_size=51136, ...}) = 0
[pid 24534] stat("/usr/bin/mkdir", {st_mode=S_IFREG|0755, st_size=51136, ...}) = 0
[pid 24534] geteuid()                   = 1000
[pid 24534] getegid()                   = 1000
[pid 24534] getuid()                    = 1000
[pid 24534] getgid()                    = 1000
[pid 24534] access("/usr/bin/mkdir", X_OK) = 0
[pid 24534] stat("/usr/bin/mkdir", {st_mode=S_IFREG|0755, st_size=51136, ...}) = 0
[pid 24534] geteuid()                   = 1000
[pid 24534] getegid()                   = 1000
[pid 24534] getuid()                    = 1000
[pid 24534] getgid()                    = 1000
[pid 24534] access("/usr/bin/mkdir", R_OK) = 0
[pid 24534] stat("/usr/bin/mkdir", {st_mode=S_IFREG|0755, st_size=51136, ...}) = 0
[pid 24534] stat("/usr/bin/mkdir", {st_mode=S_IFREG|0755, st_size=51136, ...}) = 0
[pid 24534] geteuid()                   = 1000
[pid 24534] getegid()                   = 1000
[pid 24534] getuid()                    = 1000
[pid 24534] getgid()                    = 1000
[pid 24534] access("/usr/bin/mkdir", X_OK) = 0
[pid 24534] stat("/usr/bin/mkdir", {st_mode=S_IFREG|0755, st_size=51136, ...}) = 0
[pid 24534] geteuid()                   = 1000
[pid 24534] getegid()                   = 1000
[pid 24534] getuid()                    = 1000
[pid 24534] getgid()                    = 1000
[pid 24534] access("/usr/bin/mkdir", R_OK) = 0
[pid 24534] execve("/usr/bin/mkdir", ["mkdir", "/tmp"], 0x55adcd4dc040 /* 55 vars */) = 0

so maybe this can give a clue to what is "going on here" with bash? Is doing some checking to prevent setuid exploits?

**UPDATE 2: ** The geteuid(),getguid(),getuid(), getgid() and access combo seems to be the hallmark of using glibc's int eaccess(const char *pathname, int mode); library function. Each usage of eaccess incurs the use of all geteuid,getguid,getuid, getgid and access, since the bash's runs findcmd.c's file_status function which in turn runs eaccess twice like this.

#if defined (HAVE_EACCESS)
  /* Use eaccess(2) if we have it to take things like ACLs and other
     file access mechanisms into account.  eaccess uses the effective
     user and group IDs, not the real ones.  We could use sh_eaccess,
     but we don't want any special treatment for /dev/fd. */
  if (eaccess (name, X_OK) == 0)
    r |= FS_EXECABLE;
  if (eaccess (name, R_OK) == 0)
    r |= FS_READABLE;

where each eaccess might be linked to 4 syscalls.

3

You should look at the loop in findcmd.c:find_user_command_in_path().

stat() is called (from file_status()) twice for each element in path: once via find_in_path_element() at line 640 and once via is_directory() at line 645.

As you mentioned, it's also in file_status() that eaccess() is called.

While that could be optimized, keep in mind that it's not big deal, because the path is then hashed, and all this searching and stat'ing happens only the first time a command is used.

  • It seemed to me that the hashing, while an effort to reduce iterating through PATH is not exempting the one file_status call in line findcmd.c:365, if I am not mistaken. I have to look some more to reassure it is not wasteful. However it is to have your findings being somewhat to what I have found out in the meanwhile too. :) thanks! – humanityANDpeace Oct 16 '18 at 20:41
  • Well, running it with gdb shows that a second call to mkdir will proceed directly, without any stat'ing. – mosvy Oct 16 '18 at 20:48
  • 1
    You're right, running it with POSIXLY_CORRECT=1 in the environment will cause it to go through all the file_status() nightmare even when hashed. – mosvy Oct 16 '18 at 20:53
  • What I do not get is, that there is a racing condition in any way. so why checking executability in the first place? why not simply execve early and react to any error encountered? Also any glues as to why glibc eaccess really does all the stat then geteuid, .... access (again) ? Seems like at least the last access could have been saved? – humanityANDpeace Oct 16 '18 at 21:04
  • 1
    the point e.i.a there is quite surprising, though. Other people may be of the same advice, see here. As to glibc()'s eaccess, it just does all those syscalls. The rationale is probably that caching them is nearly impossible to get right in multithreaded programs, as any static/global variable would have to be protected with mutexes and such, which would be more expensive than a dozen light system calls. – mosvy Oct 16 '18 at 22:00
0

Looking at the source code of bash, the answer is:

Yes the calls are normal, they are due to several factors including

  1. bash runs a function file_status that includes a call to stat and in most GNU/Linux setups two seperate calls to eaccess from glibc
  2. glibc's eaccess itself runs again stat and then the whole happy bunch of geteuid, getegid, getuid, getgid and finally access (as it might have not recorded any useful info from its inital stat and maybe glibc does simply not feel like saving on syscalls (context switches do not matter!).
  3. bash wants to make certain that the file it found can iterrating through the directories in PATH are indeed executable and readable for the user attempting to do so. (one test)
  4. bash runs a hash table to reduce searching path on successive invokations (the secont test with file_status)

This all then generates the numerous simingly superflous sys calls. 6/11 syscalls for each cantate in PATH and another 6/11 syscalls once the command was previously found to be in the hash-table and is hence checked for being still valid.

the file_status function in bash's findcmd.c looks in the case of my compilation for my linux box like this (i.e. the ifdefs evaluated)

int
file_status (name)
     const char *name;
{
  struct stat finfo;
  int r;

  /* Determine whether this file exists or not. */
  if (stat (name, &finfo) < 0)
    return (0);

  /* If the file is a directory, then it is not "executable" in the
     sense of the shell. */
  if (S_ISDIR (finfo.st_mode))
    return (FS_EXISTS|FS_DIRECTORY);

  r = FS_EXISTS;

  /* Use eaccess(2) if we have it to take things like ACLs and other
     file access mechanisms into account.  eaccess uses the effective
     user and group IDs, not the real ones.  We could use sh_eaccess,
     but we don't want any special treatment for /dev/fd. */
  if (exec_name_should_ignore (name) == 0 && eaccess (name, X_OK) == 0)
    r |= FS_EXECABLE;
  if (eaccess (name, R_OK) == 0)
    r |= FS_READABLE;

  return r;
}

which will run once stat() initially and the eaccess() twice which in turns (being glibc function runs :

  1. stat
  2. geteuid
  3. getguid
  4. getuid
  5. getgid
  6. access )

and hence is responsible for this part of the original bash output:

[pid 24534] stat("/usr/bin/mkdir", {st_mode=S_IFREG|0755, st_size=51136, ...}) = 0
[pid 24534] geteuid()                   = 1000
[pid 24534] getegid()                   = 1000
[pid 24534] getuid()                    = 1000
[pid 24534] getgid()                    = 1000
[pid 24534] access("/usr/bin/mkdir", X_OK) = 0
[pid 24534] stat("/usr/bin/mkdir", {st_mode=S_IFREG|0755, st_size=51136, ...}) = 0
[pid 24534] geteuid()                   = 1000
[pid 24534] getegid()                   = 1000
[pid 24534] getuid()                    = 1000
[pid 24534] getgid()                    = 1000
[pid 24534] access("/usr/bin/mkdir", R_OK) = 0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.