Categories
buffer-overflow c fgets gets

Why is the gets function so dangerous that it should not be used?

287

When I try to compile C code that uses the gets() function with GCC, I get this warning:

(.text+0x34): warning: the `gets’ function is dangerous and should not be used.

I remember this has something to do with stack protection and security, but I’m not sure exactly why.

How can I remove this warning and why is there such a warning about using gets()?

If gets() is so dangerous then why can’t we remove it?

4

  • 3

    gets() Buffer_overflow_attack

    – EsmaeelE

    Dec 9, 2017 at 12:41

  • 7

    And note that scanf("%s", b) has all the same problems as gets.

    Dec 16, 2020 at 14:56

  • As a measure of how seriously even WG14 (the ISO working group responsible for the C standard) take this, it is, so far, the ONLY feature formally removed from the C standard. WG14 have a policy of never break existing code (even if already fundamentally broken) – a policy they broke to get rid of gets()!

    – Andrew

    Jun 10 at 4:52

218

In order to use gets safely, you have to know exactly how many characters you will be reading, so that you can make your buffer large enough. You will only know that if you know exactly what data you will be reading.

Instead of using gets, you want to use fgets, which has the signature

char* fgets(char *string, int length, FILE * stream);

(fgets, if it reads an entire line, will leave the '\n' in the string; you’ll have to deal with that.)

gets remained an official part of the language up to the 1999 ISO C standard, but it was officially removed in the 2011 standard. Most C implementations still support it, but at least gcc issues a warning for any code that uses it.

2

  • 96

    It’s actually not gcc which warns, it’s the glibc which contains a pragma or attribute on gets() that causes the compiler to emit a warning when used.

    – fuz

    Jan 5, 2015 at 11:47

  • 7

    @fuz actually, it’s not even only the compiler that warns: the warning quoted in the OP was printed by the linker!

    – Ruslan

    Apr 21, 2020 at 14:53

214

Why is gets() dangerous

The first internet worm (the Morris Internet Worm) escaped about 30 years ago (1988-11-02), and it used gets() and a buffer overflow as one of its methods of propagating from system to system. The basic problem is that the function doesn’t know how big the buffer is, so it continues reading until it finds a newline or encounters EOF, and may overflow the bounds of the buffer it was given.

You should forget you ever heard that gets() existed.

The C11 standard ISO/IEC 9899:2011 eliminated gets() as a standard function, which is A Good Thing™ (it was formally marked as ‘obsolescent’ and ‘deprecated’ in ISO/IEC 9899:1999/Cor.3:2007 — Technical Corrigendum 3 for C99, and then removed in C11). Sadly, it will remain in libraries for many years (meaning ‘decades’) for reasons of backwards compatibility. If it were up to me, the implementation of gets() would become:

char *gets(char *buffer)
{
    assert(buffer != 0);
    abort();
    return 0;
}

Given that your code will crash anyway, sooner or later, it is better to head the trouble off sooner rather than later. I’d be prepared to add an error message:

fputs("obsolete and dangerous function gets() called\n", stderr);

Modern versions of the Linux compilation system generates warnings if you link gets() — and also for some other functions that also have security problems (mktemp(), …).

Alternatives to gets()

fgets()

As everyone else said, the canonical alternative to gets() is fgets() specifying stdin as the file stream.

char buffer[BUFSIZ];

while (fgets(buffer, sizeof(buffer), stdin) != 0)
{
    ...process line of data...
}

What no-one else yet mentioned is that gets() does not include the newline but fgets() does. So, you might need to use a wrapper around fgets() that deletes the newline:

char *fgets_wrapper(char *buffer, size_t buflen, FILE *fp)
{
    if (fgets(buffer, buflen, fp) != 0)
    {
        size_t len = strlen(buffer);
        if (len > 0 && buffer[len-1] == '\n')
            buffer[len-1] = '\0';
        return buffer;
    }
    return 0;
}

Or, better:

char *fgets_wrapper(char *buffer, size_t buflen, FILE *fp)
{
    if (fgets(buffer, buflen, fp) != 0)
    {
        buffer[strcspn(buffer, "\n")] = '\0';
        return buffer;
    }
    return 0;
}

Also, as caf points out in a comment and paxdiablo shows in his answer, with fgets() you might have data left over on a line. My wrapper code leaves that data to be read next time; you can readily modify it to gobble the rest of the line of data if you prefer:

        if (len > 0 && buffer[len-1] == '\n')
            buffer[len-1] = '\0';
        else
        {
             int ch;
             while ((ch = getc(fp)) != EOF && ch != '\n')
                 ;
        }

The residual problem is how to report the three different result states — EOF or error, line read and not truncated, and partial line read but data was truncated.

This problem doesn’t arise with gets() because it doesn’t know where your buffer ends and merrily tramples beyond the end, wreaking havoc on your beautifully tended memory layout, often messing up the return stack (a Stack Overflow) if the buffer is allocated on the stack, or trampling over the control information if the buffer is dynamically allocated, or copying data over other precious global (or module) variables if the buffer is statically allocated. None of these is a good idea — they epitomize the phrase ‘undefined behaviour`.


There is also the TR 24731-1 (Technical Report from the C Standard Committee) which provides safer alternatives to a variety of functions, including gets():

§6.5.4.1 The gets_s function

Synopsis

#define __STDC_WANT_LIB_EXT1__ 1
#include <stdio.h>
char *gets_s(char *s, rsize_t n);

Runtime-constraints

s shall not be a null pointer. n shall neither be equal to zero nor be greater than
RSIZE_MAX. A new-line character, end-of-file, or read error shall occur within reading
n-1 characters from stdin.25)

3 If there is a runtime-constraint violation, s[0] is set to the null character, and characters
are read and discarded from stdin until a new-line character is read, or end-of-file or a
read error occurs.

Description

4 The gets_s function reads at most one less than the number of characters specified by n
from the stream pointed to by stdin, into the array pointed to by s. No additional
characters are read after a new-line character (which is discarded) or after end-of-file.
The discarded new-line character does not count towards number of characters read. A
null character is written immediately after the last character read into the array.

5 If end-of-file is encountered and no characters have been read into the array, or if a read
error occurs during the operation, then s[0] is set to the null character, and the other
elements of s take unspecified values.

Recommended practice

6 The fgets function allows properly-written programs to safely process input lines too
long to store in the result array. In general this requires that callers of fgets pay
attention to the presence or absence of a new-line character in the result array. Consider
using fgets (along with any needed processing based on new-line characters) instead of
gets_s.

25) The gets_s function, unlike gets, makes it a runtime-constraint violation for a line of input to
overflow the buffer to store it. Unlike fgets, gets_s maintains a one-to-one relationship between
input lines and successful calls to gets_s. Programs that use gets expect such a relationship.

The Microsoft Visual Studio compilers implement an approximation to the TR 24731-1 standard, but there are differences between the signatures implemented by Microsoft and those in the TR.

The C11 standard, ISO/IEC 9899-2011, includes TR24731 in Annex K as an optional part of the library. Unfortunately, it is seldom implemented on Unix-like systems.


getline() — POSIX

POSIX 2008 also provides a safe alternative to gets() called getline(). It allocates space for the line dynamically, so you end up needing to free it. It removes the limitation on line length, therefore. It also returns the length of the data that was read, or -1 (and not EOF!), which means that null bytes in the input can be handled reliably. There is also a ‘choose your own single-character delimiter’ variation called getdelim(); this can be useful if you are dealing with the output from find -print0 where the ends of the file names are marked with an ASCII NUL '\0' character, for example.

16

  • 9

    It’s also worth pointing out that fgets() and your fgets_wrapper() version will leave the trailing portion of an over-long line in the input buffer, to be read by the next input function. In many cases, you will want to read-and-discard these characters.

    – caf

    Nov 30, 2010 at 2:33

  • 7

    I wonder why they didn’t add an fgets() alternative that allows one to use its functionality without having to make a silly strlen call. For example, an fgets variant which returned the number of bytes read into the string would make it easy for code to see if the last byte read was a newline. If the behavior of passing a null pointer for the buffer was defined as “read and discard up to n-1 bytes until the next newline”, that would allow code to easily discard the tail of over-length lines.

    – supercat

    Mar 27, 2015 at 21:31

  • 3

    @supercat: Yes, I agree — it is a pity. The nearest approach to that is probably POSIX getline() and its relative getdelim(), which do return the length of the ‘line’ read by the commands, allocating space as required to be able to store the whole line. Even that can cause problems if you end up with a single-line JSON file that is multiple gigabytes in size; can you afford all that memory? (And while we’re at it, can we have strcpy() and strcat() variants that return a pointer to the null byte at the end? Etc.)

    Mar 27, 2015 at 21:37

  • 4

    @supercat: the other problem with fgets() is that if the file contains a null byte, you can’t tell how much data there is after the null byte up to the end of line (or EOF). strlen() can only report up to the null byte in the data; after that, it is guesswork and therefore almost certainly wrong.

    Mar 27, 2015 at 22:28

  • 7

    “forget you ever heard that gets() existed.” When I do this I run into it again and come back here. Are you hacking stackoverflow to get upvotes?

    Jan 24, 2016 at 15:17

24

Because gets doesn’t do any kind of check while getting bytes from stdin and putting them somewhere. A simple example:

char array1[] = "12345";
char array2[] = "67890";

gets(array1);

Now, first of all you are allowed to input how many characters you want, gets won’t care about it. Secondly the bytes over the size of the array in which you put them (in this case array1) will overwrite whatever they find in memory because gets will write them. In the previous example this means that if you input "abcdefghijklmnopqrts" maybe, unpredictably, it will overwrite also array2 or whatever.

The function is unsafe because it assumes consistent input. NEVER USE IT!

9

  • 4

    What makes gets outright unusable is that it doesn’t have an array length/count parameter that it takes; had it been there, it’d just be another ordinary C standard function.

    – legends2k

    Sep 30, 2013 at 3:59


  • @legends2k: I’m curious what the intended usage for gets was, and why no standard fgets variant was made as convenient for use cases where the newline is not desired as part of the input?

    – supercat

    Mar 28, 2015 at 18:04

  • 2

    @supercat gets was, as the name suggests, designed to get a string from stdin, however the rationale for not having a size parameter may have been from the spirit of C: Trust the programmer. This function was removed in C11 and the replacement given gets_s takes in the size of the input buffer. I’ve no clue about the fgets part though.

    – legends2k

    Mar 29, 2015 at 1:29


  • @legends2k: The only context I can see in which gets might be excusable would be if one was using a hardware-line-buffered I/O system which was physically incapable of submitting a line over a certain length, and the intended lifetime of the program was shorter than the lifetime of the hardware. In that case, if hardware is incapable of submitting lines over 127 bytes long it might be justifiable to gets into a 128-byte buffer, though I would think the advantages of being able to specify a shorter buffer when expecting smaller input would more than justify the cost.

    – supercat

    Mar 29, 2015 at 16:57

  • @legends2k: Actually, what might have been ideal would have been to have a “string pointer” identify a byte that would select among a few different string/buffer/buffer-info formats, with one value of prefix byte indicating a struct that contained the prefix byte [plus padding], plus the buffer size, used size, and address of the actual text. Such a pattern would make it possible for code to pass an arbitrary substring (not just the tail) of another string without having to copy anything, and would allow methods like gets and strcat to safely accept as much as will fit.

    – supercat

    Mar 29, 2015 at 17:02