Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is fgets() returning NULL with a short buffer compliant?

In unit testing a function containing fgets(), came across an unexpected result when the buffer size n < 2. Obviously such a buffer size is foolish, but the test is exploring corner cases.

Simplified code:

#include <error.h>
#include <stdio.h>

void test_fgets(char * restrict s, int n) {
  FILE *stream = stdin;
  s[0] = 42;
  printf("< s:%p n:%d stream:%p\n", s, n, stream);
  char *retval = fgets(s, n, stream);
  printf("> errno:%d feof:%d ferror:%d retval:%p s[0]:%d\n\n",
    errno, feof(stream), ferror(stream), retval, s[0]);
}

int main(void) {
  char s[100];
  test_fgets(s, sizeof s);  // Entered "123\n" and works as expected
  test_fgets(s, 1);         // fgets() --> NULL, feof() --> 0, ferror() --> 0
  test_fgets(s, 0);         // Same as above
  return 0;
}

What is surprising is that fgets() returns NULL and neither feof() nor ferror() are 1.

The C spec, below, seems silent on this rare case.

Questions:

  • Is returning NULL without setting feof() nor ferror() compliant behavior?
  • Could a different result be compliant behavior?
  • Does it make a difference if n is 1 or less than 1?

Platform: gcc version 4.5.3 Target: i686-pc-cygwin

Here is an abstract from the C11 Standard, some emphasis mine:

7.21.7.2 The fgets function

The fgets function reads at most one less than the number of characters specified by n [...]

The fgets function returns s if successful. If end-of-file is encountered and no characters have been read into the array, the contents of the array remain unchanged and a null pointer is returned. If a read error occurs during the operation, the array contents are indeterminate and a null pointer is returned.

Related postings
How to use feof and ferror for fgets (minishell in C)
Trouble creating a shell in C (Seg-Fault and ferror)
fputs(), fgets(), ferror() questions and C++ equivalents
Return value of fgets()


[Edit] Comments on answers

@Shafik Yaghmour well presented the overall issue: since the C spec does not mention what to do when it does not read any data nor write any data to s when (n <= 0), it is Undefined Behavior. So any reasonable response should be acceptable such as return NULL, set no flags, leave buffer alone.

As to what should happen when n==1, @Oliver Matthews answer and @Matt McNabb comment indicate a C spec's lack of clarity considering a buffer of n == 1. The C spec seems to favor a buffer of n == 1 should return the buffer pointer with s[0] == '\0', but is not explicit enough.

like image 305
chux - Reinstate Monica Avatar asked Apr 30 '14 12:04

chux - Reinstate Monica


People also ask

Does fgets return null?

Upon successful completion, fgets() returns s. If the stream is at end-of-file, the end-of-file indicator for the stream is set and fgets() returns a null pointer. If a read error occurs, the error indicator for the stream is set, fgets() returns a null pointer and sets errno to indicate the error.

Is fgets always null terminated?

So, yes, when fgets() does not return NULL the destination array always has a null character.

What is buffer in fgets?

buffer is a char array or chunk of memory where the characters fetched are stored. size is the size of the buffer , the same value. The fgets() function reads one less than the size value to ensure that the input string is capped with a null character, \0 .

Is fgets safe in C?

fgets is safe to use in comparison to gets since it checks for character array str bounds. gets keeps on reading characters from the users, until a newline character is encountered.


2 Answers

The behavior is different in newer releases of glibc, for n == 1, it returns s which indicates success, this is not an unreasonable reading of 7.19.7.2 The fgets function paragraph 2 which says (it is the same in both C99 and C11, emphasis mine):

char *fgets(char * restrict s, int n, FILE * restrict stream);

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

Not terribly useful but does not violate anything said in the standard, it will read at most 0 characters and null-terminate. So the results you are seeing looks like a bug that was fixed in later releases of glibc. It also clearly not an end of file nor a read error as covered in paragraph 3:

[...]If end-of-file is encountered and no characters have been read into the array, the contents of the array remain unchanged and a null pointer is returned. If a read error occurs during the operation, the array contents are indeterminate and a null pointer is returned.

As far as the final case where n == 0 this looks like it is simply undefined behavior. The draft C99 standard section 4. Conformance paragraph 2 says (emphasis mine):

If a ‘‘shall’’ or ‘‘shall not’’ requirement that appears outside of a constraint is violated, the behavior is undefined. Undefined behavior is otherwise indicated in this International Standard by the words ‘‘undefined behavior’’ or by the omission of any explicit definition of behavior. There is no difference in emphasis among these three; they all describe ‘‘behavior that is undefined’’.

The wording is the same in C11. It is impossible to read at most -1 characters and it is neither an end of file nor a read error. So we have no explicit definition of the behavior in this case. Looks like a defect but I cannot find any defect reports that cover this.

like image 198
Shafik Yaghmour Avatar answered Nov 15 '22 18:11

Shafik Yaghmour


tl;dr: that version of glibc has a bug for n=1, the spec has (arguably) an ambiguity for n<1; but I think newer glibc's take the most sensible option.

So, the c99 spec is basically the same.

The behavior for test_fgets(s, 1) is wrong. glibc 2.19 gives the correct output (retval!=null, s[0]==null.

The behavior for test_fgets(s,0) is undefined, really. It wasn't successful (you can't read at most -1 characters), but it doesn't hit either of the two 'return null' criteria (EOF& 0 read; read error).

However, GCC's behavior is arguably correct (returning the pointer to the unchanged s would also be OK) - feof isn't set, because it hasn't hit eof; ferror isn't set because there wasn't a read error.

I suspect the logic in gcc (not got the source to hand) has an 'if n<=0 return null' near the top.

[edit:]

On reflection, I actually think that glibc's behavior for n=0 is the most correct response it could give:

  • No eof read, so feof()==0
  • No reads, so no read error could have happened, so ferror=0

Now as for the return value - fgets cannot have read -1 characters (it's impossible). If fgets returned the passed in pointer back, it would look like a successful call. - Ignoring this corner case, fgets commits to returning a null-terminated string. If it didn't in this case, you couldn't rely on it. But fgets will set the character after after the last character read into the array to null. given we read in -1 characters (apparantly) on this call, that would make it setting the 0th character to null?

So, the sanest choice is to return null (in my opinion).

like image 41
Oliver Matthews Avatar answered Nov 15 '22 17:11

Oliver Matthews