2

I'm trying to get the size in bytes of a /proc/pid/exe file with lstat. Here's my code:

int     main(int argc, char *argv[]) 
{   
 struct stat    sb;
 char       *linkname;
 ssize_t    r;

  if (argc != 2) 
  {
    fprintf(stderr, "Usage: %s <pathname>\n", argv[0]);
    exit(EXIT_FAILURE);
  }

  if (lstat(argv[1], &sb) == -1) 
  {
    perror("lstat");
    exit(EXIT_FAILURE);
  }

  printf("sb.st_size %d\n", sb.st_size);   

  exit(EXIT_SUCCESS);
}

It seems like sb.st_size is ALWAYS equal to 0, and I don't understand why. Plus, this sample is extracted from readlink(2) man page.

Edit: I'm trying to get it working on openSUSE.

Thanks in advance for ur help people.

Amina
  • 404
  • 4
  • 14
  • `lstat` returns the size of a symbolic link. Since `/proc/pid/exe` is a link, not a file, its size is not defined, and zero is a reasonable answer. If you want the size of the executable, use `stat`. – Gene Jul 05 '14 at 00:34
  • Here's what I read in the man: `The st_size field gives the size of the file (if it is a regular file or a symbolic link) in bytes.The size of a symbolic link is the length of the pathname it contains, without a terminating null byte.` That is exactly what I'm looking for. The length of the pathname it is pointing to. – Amina Jul 05 '14 at 00:38
  • That's totally OS dependent. You didn't specify an OS. – Gene Jul 05 '14 at 00:54
  • Sorry, now it's specified. – Amina Jul 05 '14 at 01:01
  • Ah! Here's something to try. `st_size` is likely to be a 64-bit quantity. Use a `%llu` specifier in `printf`. – Gene Jul 05 '14 at 01:08

3 Answers3

7

The files in /proc are not ordinary files. For most of them, stat() et al. return .st_size == 0.

In particular, /proc/PID/exe is not really a symlink or a hardlink, but a special pseudofile, which behaves mostly like a symlink.

(If you need to, you can detect procfs files checking the .st_dev field. Compare to .st_dev obtained from lstat("/proc/self/exe",..), for example.)

To obtain the path to a specific execubtable based on its PID, I recommend an approach relying on the return value of readlink() instead:

#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <stdio.h>

/* Creative Commons CC0: Public Domain dedication
 * (In jurisdictions without public domain, this example program
 *  is licensed under the Creative Commons CC0 license.)
 *
 * To the extent possible under law, Nominal Animal has waived all
 * copyright and related or neighboring rights to this example program.
 *
 * In other words, you are free to use it in any way you wish,
 * but if it breaks something, you get to keep all the pieces.
*/

/** exe_of() - Obtain the executable path a process is running
 * @pid: Process ID
 * @sizeptr: If specified, the allocated size is saved here
 * @lenptr: If specified, the path length is saved here
 * Returns the dynamically allocated pointer to the path,
 * or NULL with errno set if an error occurs.
*/
char *exe_of(const pid_t pid, size_t *const sizeptr, size_t *const lenptr)
{
    char   *exe_path = NULL;
    size_t  exe_size = 1024;
    ssize_t exe_used;
    char    path_buf[64];
    int     path_len;

    path_len = snprintf(path_buf, sizeof path_buf, "/proc/%ld/exe", (long)pid);
    if (path_len < 1 || path_len >= sizeof path_buf) {
        errno = ENOMEM;
        return NULL;
    }

    while (1) {

        exe_path = malloc(exe_size);
        if (!exe_path) {
            errno = ENOMEM;
            return NULL;
        }

        exe_used = readlink(path_buf, exe_path, exe_size - 1);
        if (exe_used == (ssize_t)-1)
            return NULL;

        if (exe_used < (ssize_t)1) {
            /* Race condition? */
            errno = ENOENT;
            return NULL;
        }

        if (exe_used < (ssize_t)(exe_size - 1))
            break;

        free(exe_path);
        exe_size += 1024;
    }

    /* Try reallocating the exe_path to minimum size.
     * This is optional, and can even fail without
     * any bad effects. */
    {
        char *temp;

        temp = realloc(exe_path, exe_used + 1);
        if (temp) {
            exe_path = temp;
            exe_size = exe_used + 1;
        }
    }

    if (sizeptr)
        *sizeptr = exe_size;

    if (lenptr)
        *lenptr = exe_used;

    exe_path[exe_used] = '\0';
    return exe_path;
}

int main(int argc, char *argv[])
{
    int   arg;
    char *exe;
    long  pid;
    char  dummy;

    if (argc < 2 || !strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) {
        printf("\n");
        printf("Usage: %s [ -h | --help ]\n", argv[0]);
        printf("       %s PID [ PID ... ]\n", argv[0]);
        printf("\n");
        return 0;
    }

    for (arg = 1; arg < argc; arg++)
        if (sscanf(argv[arg], " %ld %c", &pid, &dummy) == 1 && pid > 0L) {
            exe = exe_of((pid_t)pid, NULL, NULL);
            if (exe) {
                printf("Process %ld runs '%s'.\n", pid, exe);
                free(exe);
            } else
                printf("Process %ld: %s.\n", pid, strerror(errno));
        } else {
            printf("%s: Invalid PID.\n", argv[arg]);
            return 1;
        }

    return 0;
}

Above, the exe_of() function returns a dynamically allocated copy of where the pseudo-symlink /proc/PID/exe points to, optionally storing the allocated size and/or the path length too. (The example program above doesn't need them, so they're NULL.)

The idea is very simple: Allocate an initial dynamic pointer that is large enough for most cases, but not ridiculously large. Reserve the last byte for the end-of-string NUL byte. If the size returned by readlink() is the same as the buffer length given to it -- it does not add a terminating end-of-string NUL byte itself --, then the buffer might have been too short; discard it, allocate a larger buffer, and retry.

Similarly, if you wish to read the full contents of a pseudo-file under /proc/, you cannot use lstat()/stat() first to find out how large a buffer you might need; you need to allocate a buffer, read as much as you can, and when necessary, just reallocate a larger buffer. (I could show example code for that, too.)

Questions?

Nominal Animal
  • 38,216
  • 5
  • 59
  • 86
  • Okay that's almost perfect. I pretty much understand now, thank you sooo much. However, there are some parts of ur example code that I don't get. I think u made a mistake right after the malloc, u might not be checking the right variable. And what comes after the "while" loop? I see curly braces but nothing before. – Amina Jul 05 '14 at 01:36
  • 1
    @Amina: You're right, good catch! The check is for the malloc() return value; fixed now. The block following the `while (1) { .. }` is just that, a local scope; you're allowed to do that. It is only executed once, and you cannot `break` out of it as it is not a loop, just a code block. I used a separate block for it because it is a bit special: it reallocates the string to optimal size, but even if it fails, nothing bad happens, the string just takes more memory than necessary. Normally, I leave a comment before it. Actually, I'll add it there now. – Nominal Animal Jul 05 '14 at 03:27
  • Thank u. U've been of great help! – Amina Jul 05 '14 at 08:54
  • Also note: this will still fail if the path of the link > PATH_MAX on linux, even if the file is valid from the link – Rahly May 29 '16 at 01:47
  • @Rahly: I do believe you are mistaken. The implementation here is similar to GNU `readlink_malloc()`, and is similarly not limited in any way by PATH_MAX. And, as far as I am aware, the kernel does not internally limit the `/proc/PID/exe` symlink lengths to PATH_MAX. – Nominal Animal May 29 '16 at 11:34
  • @NominalAnimal: except that it is, even with the current kernel.org stable. This is because of 3 factors, ls -l on a proc/pid directory that had a long path name results in ls: cannot read symbolic link 'exe': File name too long, and that the fact that GNU doesn't do anything but make a syscall(SYS_readlink/at), at which, even with a 100Meg buffer causes a ENAMETOOLONG. – Rahly May 29 '16 at 11:41
  • and #3, according to the standard, http://pubs.opengroup.org/onlinepubs/9699919799/functions/readlink.html , readlink is SUPPOSE to do this. ENAMETOOLONG: The length of a pathname exceeds {PATH_MAX}, or pathname resolution of a symbolic link produced an intermediate result with a length that exceeds {PATH_MAX} – Rahly May 29 '16 at 11:42
  • @Rahly: Ah, I think what you are getting at: the kernel limits `/proc/PID/exe` symlinks to `PAGE_SIZE` length (see [fs/proc/base.c:do_proc_readlink()](https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/tree/fs/proc/base.c#n1600)). That is an internal implementation limitation that depends on the hardware architecture, and has nothing to do with `PATH_MAX` per se. – Nominal Animal May 29 '16 at 12:16
  • @NominalAnimal: close but not it, thats a limit to the call. So its limiting "/proc/self/exe" string to PAGE_SIZE, also note, this function does not return -ENAMETOOLONG. The problem is the resolution of the link to a name > PATH_MAX. Look at https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/tree/fs/namei.c#n216 , getname_kernel, where it COPIES the link name from the DRIVER into kernel memory. Note: return ERR_PTR(-ENAMETOOLONG); This is the kernel after handling to the driver, so it happens for everything. – Rahly May 29 '16 at 12:30
  • @Rahly: No. `readlink()` is fs-specific: see [fs/stat.c:readlink()](https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/tree/fs/stat.c#n315). For `/proc` that is `do_proc_readlink()`, and usually `d_path()`. `-ENAMETOOLONG` may occur in [fs/dcache.c:prepend_name()](https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/tree/fs/dcache.c#n3043). A temporary buffer (of one page) is used, because `d_path()` constructs the path from leaf towards root, and it won't start at the beginning of the buffer. This issue can be fixed by adding higher-order temporary buffer allocation .. – Nominal Animal May 29 '16 at 14:11
  • 1
    .. to [`fs/proc/base.c:do_proc_readlink()`](https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/tree/fs/proc/base.c#n1600) in case of `-ENAMETOOLONG` with a single-page buffer, and user provided buffer being longer. This is currently seen as unnecessary by the kernel devs, as higher order allocations are problematic. The path walk itself is race-prone, and cannot be done by writing the components directly to the userspace buffer. If you are willing to test, I can provide you with a kernel patch you can verify this with. – Nominal Animal May 29 '16 at 14:15
  • Sorry, you are right, I was looking at symbolic link creation where .readlink is set to generic_readlink, which follows the kernels defined behavior. But since also PAGE_SIZE by default is the same as PATH_MAX, the end result is the same. To even request a change will probably fall on deaf ears because its is the defined behavior of the function. The only problem I have with it is that allows a program to "hide" from others in the system, malicious or not. – Rahly May 29 '16 at 18:47
  • @Rahly: No worries -- it is a valid remark, that `/proc/PID/exe` symlinks are not always available. If I recall correctly, the main counterargument for adding multi-page support for /proc readlink() boils down to there being no valid use cases for executable paths being over a page long. So, when checking e.g. Unix domain socket peer credentials (via `SCM_CREDENTIALS`), not being able to determine the binary the peer is executing is as suspicious as that binary being completely user-controllable. In other words, that such "hiding" binaries are easily detected as nefarious. – Nominal Animal May 29 '16 at 20:09
  • Although true, easy to detect, but not easily found. Note: this applies to /proc/PID/cwd as well. ENOENT seems to be system processes, which makes sense. So I can find the offending process, and thereby kill it. Nothing is preventing the system to possibly starting it back up because I cannot remove the offending executable, not knowing where it is located. – Rahly May 29 '16 at 20:17
1

Can't comment on the post I want to reply to so sorry!

Valgrind will report exe_path = malloc(exe_size); in Nominal Animal's post as 'blocks are definitely lost'. So, if you're using that function then your memory will rise very quickly.

Freeing the memory even when you return NULL will fix this. Explicit casting of (char*) from the malloc calls should also be added to make gcc stop complaining when you use -Wall

char* exe_of(const pid_t pid, size_t *const sizeptr, size_t *const lenptr)
{
    char   *exe_path = NULL;
    size_t  exe_size = 1024;
    ssize_t exe_used;
    char    path_buf[64];
    unsigned int path_len;

    path_len = snprintf(path_buf, sizeof path_buf, "/proc/%ld/exe", (long)pid);
    if (path_len < 1 || path_len >= sizeof path_buf) {
        errno = ENOMEM;
        return NULL;
    }

    while (1) {

        exe_path = (char*)malloc(exe_size);
        if (!exe_path) {
            errno = ENOMEM;
            return NULL;
        }

        exe_used = readlink(path_buf, exe_path, exe_size - 1);
        if (exe_used == (ssize_t)-1) {
            free(exe_path);
            return NULL;
        }

        if (exe_used < (ssize_t)1) {
            /* Race condition? */
            errno = ENOENT;
            free(exe_path);
            return NULL;
        }

        if (exe_used < (ssize_t)(exe_size - 1))
            break;

        free(exe_path);
        exe_size += 1024;
    }

    /* Try reallocating the exe_path to minimum size.
     * This is optional, and can even fail without
     * any bad effects. */
    {
        char *temp;

        temp = (char*)realloc(exe_path, exe_used + 1);
        if (temp) {
            exe_path = temp;
            exe_size = exe_used + 1;
        }
    }

    if (sizeptr)
        *sizeptr = exe_size;

    if (lenptr)
        *lenptr = exe_used;

    exe_path[exe_used] = '\0';
    return exe_path;
}
0

Generally you don't have to worry about this, but after commenting with Nominal Animal, it seems that linux limits filepaths from /proc/PID/exe by PAGE_SIZE. So even if the file system supports paths longer than that, there is no way to get readlink() to give you that path because this is implemented as a hard limit. I have found another way so IF readlink fails with ENAMETOOLONG, you can read /proc/PID/maps, although this could be hit or miss, i've found that it hits every process in 4 different distros EXCEPT system processes which do not have actual filenames (ENOENT)

int getExecutableFromMaps(char *buf, size_t bufsize) {
  FILE *fp;
  char *mylinebuf = NULL;
  size_t mylinebufsize = 0;
  size_t counter = 0;
  size_t start = 0;
  size_t column = 0;
  int result = -1;
  fp = fopen("/proc/self/maps", "r");
  if( fp != NULL ) {
    if( getline(&mylinebuf, &mylinebufsize, fp) >= 0 ) {
      if( mylinebuf != NULL ) {
        while( column < 5 && counter < mylinebufsize ) {
          while( counter < mylinebufsize && mylinebuf[counter] != ' ') {
            counter++;
          }
          while( counter < mylinebufsize && mylinebuf[counter] == ' ') {
            counter++;
          }
          column++;
        }
        int start = counter;
        while( counter < mylinebufsize && (mylinebuf[counter] != '\n' && mylinebuf[counter] != '\r') ) {
          counter++;
        }
        if( counter <= mylinebufsize && start < counter && (counter-start+1)<=bufsize ) {
          memcpy(buf, &mylinebuf[start], counter-start);
          buf[counter-start+1] = 0;
          result = counter-start+1;
        }
      }
    }
    if( mylinebuf != NULL ) {
      free(mylinebuf);
    }
    fclose(fp);
  }
  return result;
}

Note: this is supplemental code that should only be called AFTER readlink fails, as this is expensive.

Rahly
  • 1,462
  • 13
  • 16
  • This is a very good point. Personally, I'd prefer to combine both of ours into a function that returns the executable path as a dynamically allocated pointer, given a process PID. Do you mind if I steal the approach from you, and add the combined new version to my answer? (With proper attribution, of course. I am a bit ashamed that I didn't think of this approach myself -- I've posted a few answers about [parsin /proc/PID/maps](http://stackoverflow.com/a/36524010/1475978) etc. -- so I do know about it, and I'd like to rectify it.) – Nominal Animal May 30 '16 at 08:02
  • Yeah go ahead im game – Rahly May 30 '16 at 09:10
  • Note: I created a directory that was over PAGE_SIZE and put an executable in it to test. Only the *maps(maps,numa_maps, and smaps) files had the correct paths without errors. Used EXT4, but I tested that XFS also has no constraints on path length, will try BTRFS if needed. Also good to note that path is resolved so realpath() doesn't need to be called. – Rahly May 30 '16 at 09:13
  • Oops. I uncovered a bug in fs/proc/task_mmu.c which causes files named `"some\nname"` and `"some\\012name"` both show up as `some\012name` in the proc map files. That is *nasty* - really allows hiding executables. I'll push a fix upstream for this first, and come back to this later. I think I shall also ask for opinions on "fixing" the page length limit on the proc readlink() -- it might be possible to convert it too to seq interface, avoiding the preallocated fixed buffer.. Feel free to ping me, if nothing happens in a week or so. :) – Nominal Animal May 30 '16 at 16:53
  • Good luck trying to get readlink fixed. Since this is the way the standard is defined. But maybe you can get them to see security aspects trump the using the standard. Probably would be easier to suggest a new api call that does whats needed. I also would love that the max individual filename size was bigger than 63 characters :(. Think I should submit a bug to the kernel bugzilla? – Rahly May 30 '16 at 19:06
  • It's only the readlink() for the /proc pseudofilesystem, which already works in weird ways. For example, you can still `stat()` it successfully, even when `readlink()` fails with `ENAMETOOLONG`. I'm still looking into whether the kernel seq_file() interface can be used for proc readlink() or not.. when posting on LKML, one better have done the groundwork, or be ignored. Re 63 chars, upping the max length from current 255 on ext4/btrfs/tmpfs has been discussed, and shot down; you'd need *really* good reasoning for it. – Nominal Animal May 30 '16 at 20:19
  • was looking at this further, yes this is REALLY nasty considering they do NOT escape the backspace. "test\012 \this" is really bad. Strangely enough bash's auto-complete fails as well as displaying a prompt (keeps calling the bell character). – Rahly Aug 06 '16 at 23:46
  • i created a 'test\n\t\\ this' and it does not escape a tab as well. very ugly. – Rahly Aug 06 '16 at 23:53
  • I did post two simple patches to LKML ([here](http://marc.info/?l=linux-kernel&m=146463784122248&w=4) and [here](http://marc.info/?l=linux-kernel&m=146463168620574&w=4)) that would fix the backslash issues, but there was zero interest or responses, and being Finnish ("Pitäkää tunkkinne" -mentality), I'm not that interested in insisting they fix an obvious bug. If they don't want to, it's their goof. – Nominal Animal Aug 07 '16 at 13:36