Search code examples
cfopenfstat

stat(), fstat(), lstat(), and fopen(); how to write TOCTOU protected system independent code


I've been dealing with a problem for a few weeks now updating 20 year code that needs to be system independent (work on both Linux and Windows). It involves Time-of-Check, Time-of-Use (TOCTOU) issues. I made a thread here, but it didn't go very far, and after ruminating on it for a while and searching deeper into the problem, I think I understand my question a bit better. Maybe I can ask it a bit better too...

From what I've read, the code needs to check if the file exists, if it is accessible, open the file, do some operations and finally close the file. It seems the best way to do this is a call to lstat(), a call to fopen(), a call to fstat() (to rule out the TOCTOU), and then the operations and closing the file.

However, I've been lead to believe that lstat() and fstat() are POSIX defined, not C Standard defined, ruling out their use for a system agnostic program, much in the same way open() shouldn't be used for cross-compatibility. How would you implement this?

If you look at my first post, you can see the developer from 20 years ago used the C preprocessor to cut the code into cross-compatible parts, but even if I did that, I wouldn't know what to replace lstat() or fstat() with (their windows counterparts).

Edit: Added abreviated code to this post; if something is unclear please go to the original post

#ifdef WIN32
    struct _stat buf;
#else
    struct stat buf;
#endif //WIN32

    FILE *fp;
    char data[2560];

    // Make sure file exists and is readable
#ifdef WIN32
    if (_access(file.c_str(), R_OK) == -1) {
#else
    if (access(file.c_str(), R_OK) == -1) {
#endif //WIN32

        char message[2560];
        sprintf(message, "File '%s' Not Found or Not Readable", file.c_str());
        throw message;
        }

    // Get the file status information
#ifdef WIN32
    if (_stat(file.c_str(), &buf) != 0) {
#else
    if (stat(file.c_str(), &buf) != 0) {
#endif //WIN32

        char message[2560];
        sprintf(message, "File '%s' No Status Available", file.c_str());
        throw message;
        }

    // Open the file for reading
    fp = fopen(file.c_str(), "r");
    if (fp == NULL) {
        char message[2560];
        sprintf(message, "File '%s' Cound Not be Opened", file.c_str());
        throw message;
    }

    // Read the file
    MvString s, ss;
    while (fgets(data, sizeof(data), fp) != (char *)0) {
        s = data;
        s.trimBoth();
        if (s.compare( 0, 5, "GROUP" ) == 0) {
            //size_t t = s.find_last_of( ":" );
            size_t t = s.find( ":" );
            if (t != string::npos) {
                ss = s.substr( t+1 ).c_str();
                ss.trimBoth();
                ss = ss.substr( 1, ss.length() - 3 ).c_str();
                group_list.push_back( ss );
            }
        }
    }
    // Close the file
    fclose(fp);
}

Solution

  • The reliable way to check whether the file exists and can be opened is to try opening it. If it was opened, all was OK. If it was not opened, you can think about spending time to analyze what went wrong.

    The access() function formally asks a different question from what you think; it asks 'can the real user ID or the real group ID access the file', but the program will use the effective user ID or the effective group ID to access the file. If your program is not running SUID or SGID, and was not launched from a program running SUID or SGID — and that's the normal case — then there's no difference. But the question is different.

    The use of stat() or lstat() doesn't seem helpful. In particular, lstat() only tells you whether you start at a symlink, but the code doesn't care about that.

    Both the access() and the stat() calls provide you with TOCTOU windows of vulnerability; the file could be removed after they reported it was present, or created after they reported it was absent.

    You should simply call fopen() and see whether it works; the code will be simpler and more resistant to TOCTOU problems. You might need to consider whether to use open() with all its extra controls (O_EXCL, etc), and then convert the file descriptor to a file pointer (fdopen()).

    All of this applies to the Unix side.

    The details will be different, but on the Windows side, you will still be best off trying to open the file and reacting appropriately to failure.

    In both systems, make sure the options provided to the open function are appropriate.