clinuxgetcwd

C - is calling getcwd causing a bug here?


I'm learning syscalls like stat and readlink. I try to stat the root directory in two different ways:

  1. Just stat the path to root. Simple.
  2. Then, a roundabout way, fopen "/", make a file descriptor path from the fd I get and readlink it to get "/". Then stat.

What I don't understand is this works the way I expect and the inode numbers are the same, except when I introduce more code AFTERWARDS, namely getcwd, the inodes are strangely not the same and test 2 fails. If you run both versions, with and without the part it says to cut out, the printf of p2 also changes and you can see there is garbage after /. What am I doing wrong here or what's going on here? How is code after those initial statements causing a change earlier in the code?

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
#include <limits.h>
#include <stdlib.h>
#include <string.h>

int main()
{
    /* Stat root this way */
    struct stat st1;
    char *p1 = "/";
    if (!stat(p1, &st1))
        printf("PASS\n");
    else
        printf("FAIL - %m\n");

    /* Stat root another way */
    struct stat st2;
    char fdpath[100], p2[100];
    FILE *fp = fopen("/", "r");
    sprintf(fdpath, "/proc/self/fd/%d", fileno(fp));
    readlink(fdpath, p2, sizeof(p2));
    if (!stat(p2, &st2))
        printf("PASS\n");
    else
        printf("FAIL - %m\n");

    printf("p2 = %s\n", p2);

    /* Check inodes are the same */
    printf("    st1.st_ino = %ld\n", st1.st_ino);
    printf("    st2.st_ino = %ld\n", st2.st_ino);

    /* TRY WITHOUT THIS - Adding this makes the inodes different! Take it out and they're the same */
    char cwd_buf[100];
    char *cwd_ret = getcwd(cwd_buf, sizeof(cwd_buf));
    if (cwd_ret == NULL)
        printf("getcwd failed - %m\n");
    printf("cwd_ret = %s\n", cwd_ret);

    return 0;
}

Solution

  • readlink does not null-terminate the string it returns, so p2 may contain arbitrary garbage following /.

    You should do

    ssize_t len = readlink(fdpath, p2, sizeof(p2));
    if (len == -1) { /* error */ }
    if (len == sizeof(p2)) { /* truncated */ }
    p2[len] = '\0';
    if (!stat(p2, &st2)) // ...
    

    You should do error checking in many other places, too.

    Adding and removing the unrelated code at the end probably changed the stack layout (because of the additional local variables), meaning that p2 contained different garbage with and without it. It could even be that in some cases, the garbage was null bytes, so that the string happened to be properly terminated after all. This sort of thing happens all the time with bugs in C code and isn't particularly meaningful; it's undefined behavior in the language of the C standard. Don't think too hard about why it happens; just try to find and fix the bug and don't write the same bug again :-)