[06/11] nss: Add __nss_fgetent_r

Message ID 7448b77dd7a68200489ac39e8c643ca8db04a3f2.1594974444.git.fweimer@redhat.com
State Committed
Headers
Series Fix fgetsgent_r data corruption bug (20338) |

Commit Message

Florian Weimer July 17, 2020, 8:30 a.m. UTC
  And helper functions __nss_readline, __nss_readline_seek,
 __nss_parse_line_result.

This consolidates common code for handling overlong lines and
parse files.  Use the new functionality in internal_getent
in nss/nss_files/files-XXX.c.
---
 include/nss_files.h         | 29 +++++++++++
 nss/Makefile                |  4 +-
 nss/Versions                |  2 +-
 nss/nss_fgetent_r.c         | 55 +++++++++++++++++++++
 nss/nss_files/files-XXX.c   | 79 ++++++++++-------------------
 nss/nss_parse_line_result.c | 46 +++++++++++++++++
 nss/nss_readline.c          | 99 +++++++++++++++++++++++++++++++++++++
 7 files changed, 260 insertions(+), 54 deletions(-)
 create mode 100644 nss/nss_fgetent_r.c
 create mode 100644 nss/nss_parse_line_result.c
 create mode 100644 nss/nss_readline.c
  

Comments

Carlos O'Donell July 21, 2020, 3:27 a.m. UTC | #1
On 7/17/20 4:30 AM, Florian Weimer via Libc-alpha wrote:
> And helper functions __nss_readline, __nss_readline_seek,
>  __nss_parse_line_result.
> 
> This consolidates common code for handling overlong lines and
> parse files.  Use the new functionality in internal_getent
> in nss/nss_files/files-XXX.c.

OK for 2.32. Very clean! 

Tested-by: Carlos O'Donell <carlos@redhat.com>
Reviewed-by: Carlos O'Donell <carlos@redhat.com>

> ---
>  include/nss_files.h         | 29 +++++++++++
>  nss/Makefile                |  4 +-
>  nss/Versions                |  2 +-
>  nss/nss_fgetent_r.c         | 55 +++++++++++++++++++++
>  nss/nss_files/files-XXX.c   | 79 ++++++++++-------------------
>  nss/nss_parse_line_result.c | 46 +++++++++++++++++
>  nss/nss_readline.c          | 99 +++++++++++++++++++++++++++++++++++++
>  7 files changed, 260 insertions(+), 54 deletions(-)
>  create mode 100644 nss/nss_fgetent_r.c
>  create mode 100644 nss/nss_parse_line_result.c
>  create mode 100644 nss/nss_readline.c
> 
> diff --git a/include/nss_files.h b/include/nss_files.h
> index d0f26815b5..f45ea02dc0 100644
> --- a/include/nss_files.h
> +++ b/include/nss_files.h
> @@ -25,6 +25,28 @@
>  FILE *__nss_files_fopen (const char *path);
>  libc_hidden_proto (__nss_files_fopen)
>  
> +/* Read a line from FP, storing it BUF.  Strip leading blanks and skip
> +   comments.  Sets errno and returns error code on failure.  Special
> +   failure: ERANGE means the buffer is too small.  The function writes
> +   the original offset to *POFFSET (which can be negative in the case
> +   of non-seekable input).  */
> +int __nss_readline (FILE *fp, char *buf, size_t len, off64_t *poffset);
> +libc_hidden_proto (__nss_readline)
> +
> +/* Seek FP to OFFSET.  Sets errno and returns error code on failure.
> +   On success, sets errno to ERANGE and returns ERANGE (to indicate
> +   re-reading of the same input line to the caller).  If OFFSET is
> +   negative, fail with ESPIPE without seeking.  Intended to be used
> +   after parsing data read by __nss_readline failed with ERANGE.  */
> +int __nss_readline_seek (FILE *fp, off64_t offset) attribute_hidden;
> +
> +/* Handles the result of a parse_line call (as defined by
> +   nss/nss_files/files-parse.c).  Adjusts the file offset of FP as
> +   necessary.  Returns 0 on success, and updates errno on failure (and
> +   returns that error code).  */
> +int __nss_parse_line_result (FILE *fp, off64_t offset, int parse_line_result);
> +libc_hidden_proto (__nss_parse_line_result)
> +
>  struct parser_data;
>  
>  /* Instances of the parse_line function from
> @@ -52,4 +74,11 @@ libnss_files_hidden_proto (_nss_files_parse_servent)
>  libc_hidden_proto (_nss_files_parse_sgent)
>  libc_hidden_proto (_nss_files_parse_spent)
>  
> +/* Generic implementation of fget*ent_r.  Reads lines from FP until
> +   EOF or a successful parse into *RESULT using PARSER.  Returns 0 on
> +   success, ENOENT on EOF, ERANGE on too-small buffer.  */
> +int __nss_fgetent_r (FILE *fp, void *result,
> +                     char *buffer, size_t buffer_length,
> +                     nss_files_parse_line parser) attribute_hidden;
> +
>  #endif /* _NSS_FILES_H */
> diff --git a/nss/Makefile b/nss/Makefile
> index 00f4d89310..20c412c3e1 100644
> --- a/nss/Makefile
> +++ b/nss/Makefile
> @@ -28,7 +28,9 @@ headers			:= nss.h
>  routines		= nsswitch getnssent getnssent_r digits_dots \
>  			  valid_field valid_list_field rewrite_field \
>  			  $(addsuffix -lookup,$(databases)) \
> -			  compat-lookup nss_hash nss_files_fopen
> +			  compat-lookup nss_hash nss_files_fopen \
> +			  nss_readline nss_parse_line_result \
> +			  nss_fgetent_r
>  
>  # These are the databases that go through nss dispatch.
>  # Caution: if you add a database here, you must add its real name
> diff --git a/nss/Versions b/nss/Versions
> index f489cb6eb0..71703750bf 100644
> --- a/nss/Versions
> +++ b/nss/Versions
> @@ -18,7 +18,7 @@ libc {
>      __nss_passwd_lookup2; __nss_group_lookup2; __nss_hosts_lookup2;
>      __nss_services_lookup2; __nss_next2; __nss_lookup;
>      __nss_hash; __nss_database_lookup2;
> -    __nss_files_fopen;
> +    __nss_files_fopen; __nss_readline; __nss_parse_line_result;
>    }
>  }
>  
> diff --git a/nss/nss_fgetent_r.c b/nss/nss_fgetent_r.c
> new file mode 100644
> index 0000000000..8f7c5b5cc7
> --- /dev/null
> +++ b/nss/nss_fgetent_r.c
> @@ -0,0 +1,55 @@
> +/* Generic implementation of fget*ent_r.
> +   Copyright (C) 2020 Free Software Foundation, Inc.
> +   This file is part of the GNU C Library.
> +
> +   The GNU C Library is free software; you can redistribute it and/or
> +   modify it under the terms of the GNU Lesser General Public
> +   License as published by the Free Software Foundation; either
> +   version 2.1 of the License, or (at your option) any later version.
> +
> +   The GNU C Library is distributed in the hope that it will be useful,
> +   but WITHOUT ANY WARRANTY; without even the implied warranty of
> +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
> +   Lesser General Public License for more details.
> +
> +   You should have received a copy of the GNU Lesser General Public
> +   License along with the GNU C Library; if not, see
> +   <https://www.gnu.org/licenses/>.  */
> +
> +#include <errno.h>
> +#include <nss_files.h>
> +
> +int
> +__nss_fgetent_r (FILE *fp, void *result, char *buffer, size_t buffer_length,
> +                 nss_files_parse_line parser)
> +{
> +  int ret;
> +
> +  _IO_flockfile (fp);
> +
> +  while (true)
> +    {
> +      off64_t original_offset;
> +      ret = __nss_readline (fp, buffer, buffer_length, &original_offset);
> +      if (ret == 0)
> +        {
> +          /* Parse the line into *RESULT.  */
> +          ret = parser (buffer, result,
> +                        (struct parser_data *) buffer, buffer_length, &errno);
> +
> +          /* Translate the result code from the parser into an errno
> +             value.  Also seeks back to the start of the line if
> +             necessary.  */
> +          ret = __nss_parse_line_result (fp, original_offset, ret);
> +
> +          if (ret == EINVAL)
> +            /* Skip over malformed lines.  */
> +            continue;
> +        }
> +      break;
> +    }
> +
> +  _IO_funlockfile (fp);
> +
> +  return ret;
> +}
> diff --git a/nss/nss_files/files-XXX.c b/nss/nss_files/files-XXX.c
> index 9cc5137953..1db9e46127 100644
> --- a/nss/nss_files/files-XXX.c
> +++ b/nss/nss_files/files-XXX.c
> @@ -135,10 +135,9 @@ internal_getent (FILE *stream, struct STRUCTURE *result,
>  		 char *buffer, size_t buflen, int *errnop H_ERRNO_PROTO
>  		 EXTRA_ARGS_DECL)
>  {
> -  char *p;
>    struct parser_data *data = (void *) buffer;
>    size_t linebuflen = buffer + buflen - data->linebuffer;
> -  int parse_result;
> +  int saved_errno = errno;	/* Do not clobber errno on success.  */
>  
>    if (buflen < sizeof *data + 2)
>      {
> @@ -149,66 +148,42 @@ internal_getent (FILE *stream, struct STRUCTURE *result,
>  
>    while (true)
>      {
> -      ssize_t r = __libc_readline_unlocked
> -	(stream, data->linebuffer, linebuflen);
> -      if (r < 0)
> -	{
> -	  *errnop = errno;
> -	  H_ERRNO_SET (NETDB_INTERNAL);
> -	  if (*errnop == ERANGE)
> -	    /* Request larger buffer.  */
> -	    return NSS_STATUS_TRYAGAIN;
> -	  else
> -	    /* Other read failure.  */
> -	    return NSS_STATUS_UNAVAIL;
> -	}
> -      else if (r == 0)
> +      off64_t original_offset;
> +      int ret = __nss_readline (stream, data->linebuffer, linebuflen,
> +				&original_offset);

OK.

> +      if (ret == ENOENT)
>  	{
>  	  /* End of file.  */
>  	  H_ERRNO_SET (HOST_NOT_FOUND);
> +	  __set_errno (saved_errno);
>  	  return NSS_STATUS_NOTFOUND;
>  	}
> -
> -      /* Everything OK.  Now skip leading blanks.  */
> -      p = data->linebuffer;
> -      while (isspace (*p))
> -	++p;
> -
> -      /* Ignore empty and comment lines.  */
> -      if (*p == '\0' || *p == '#')
> -	continue;
> -
> -      /* Parse the line.   */
> -      *errnop = EINVAL;
> -      parse_result = parse_line (p, result, data, buflen, errnop EXTRA_ARGS);
> -
> -      if (parse_result == -1)
> +      else if (ret == 0)
>  	{
> -	  if (*errnop == ERANGE)
> +	  ret = __nss_parse_line_result (stream, original_offset,
> +					 parse_line (data->linebuffer,
> +						     result, data, buflen,
> +						     errnop EXTRA_ARGS));

OK.

> +	  if (ret == 0)
>  	    {
> -	      /* Return to the original file position at the beginning
> -		 of the line, so that the next call can read it again
> -		 if necessary.  */
> -	      if (__fseeko64 (stream, -r, SEEK_CUR) != 0)
> -		{
> -		  if (errno == ERANGE)
> -		    *errnop = EINVAL;
> -		  else
> -		    *errnop = errno;
> -		  H_ERRNO_SET (NETDB_INTERNAL);
> -		  return NSS_STATUS_UNAVAIL;
> -		}
> +	      /* Line has been parsed successfully.  */
> +	      __set_errno (saved_errno);
> +	      return NSS_STATUS_SUCCESS;
>  	    }
> -	  H_ERRNO_SET (NETDB_INTERNAL);
> -	  return NSS_STATUS_TRYAGAIN;
> +	  else if (ret == EINVAL)
> +	    /* If it is invalid, loop to get the next line of the file
> +	       to parse.  */
> +	    continue;
>  	}
>  
> -      /* Return the data if parsed successfully.  */
> -      if (parse_result != 0)
> -	return NSS_STATUS_SUCCESS;
> -
> -      /* If it is invalid, loop to get the next line of the file to
> -	 parse.  */
> +      *errnop = ret;
> +      H_ERRNO_SET (NETDB_INTERNAL);
> +      if (ret == ERANGE)
> +	/* Request larger buffer.  */
> +	return NSS_STATUS_TRYAGAIN;
> +      else
> +	/* Other read failure.  */
> +	return NSS_STATUS_UNAVAIL;
>      }
>  }
>  
> diff --git a/nss/nss_parse_line_result.c b/nss/nss_parse_line_result.c
> new file mode 100644
> index 0000000000..cd008e3c36
> --- /dev/null
> +++ b/nss/nss_parse_line_result.c
> @@ -0,0 +1,46 @@
> +/* Implementation of __nss_parse_line_result.
> +   Copyright (C) 2020 Free Software Foundation, Inc.
> +   This file is part of the GNU C Library.
> +
> +   The GNU C Library is free software; you can redistribute it and/or
> +   modify it under the terms of the GNU Lesser General Public
> +   License as published by the Free Software Foundation; either
> +   version 2.1 of the License, or (at your option) any later version.
> +
> +   The GNU C Library is distributed in the hope that it will be useful,
> +   but WITHOUT ANY WARRANTY; without even the implied warranty of
> +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
> +   Lesser General Public License for more details.
> +
> +   You should have received a copy of the GNU Lesser General Public
> +   License along with the GNU C Library; if not, see
> +   <https://www.gnu.org/licenses/>.  */
> +
> +#include <nss_files.h>
> +
> +#include <assert.h>
> +#include <errno.h>
> +
> +int
> +__nss_parse_line_result (FILE *fp, off64_t offset, int parse_line_result)
> +{
> +  assert (parse_line_result >= -1 && parse_line_result <= 1);
> +
> +  switch (__builtin_expect (parse_line_result, 1))
> +    {
> +    case 1:
> +      /* Sucess.  */
> +      return 0;
> +    case 0:
> +      /* Parse error.  */
> +      __set_errno (EINVAL);
> +      return EINVAL;
> +    case -1:
> +      /* Out of buffer space.  */
> +      return __nss_readline_seek (fp, offset);
> +
> +      default:
> +        __builtin_unreachable ();
> +    }
> +}
> +libc_hidden_def (__nss_parse_line_result)
> diff --git a/nss/nss_readline.c b/nss/nss_readline.c
> new file mode 100644
> index 0000000000..44e0dd9319
> --- /dev/null
> +++ b/nss/nss_readline.c
> @@ -0,0 +1,99 @@
> +/* Read a line from an nss_files database file.
> +   Copyright (C) 2020 Free Software Foundation, Inc.
> +   This file is part of the GNU C Library.
> +
> +   The GNU C Library is free software; you can redistribute it and/or
> +   modify it under the terms of the GNU Lesser General Public
> +   License as published by the Free Software Foundation; either
> +   version 2.1 of the License, or (at your option) any later version.
> +
> +   The GNU C Library is distributed in the hope that it will be useful,
> +   but WITHOUT ANY WARRANTY; without even the implied warranty of
> +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
> +   Lesser General Public License for more details.
> +
> +   You should have received a copy of the GNU Lesser General Public
> +   License along with the GNU C Library; if not, see
> +   <https://www.gnu.org/licenses/>.  */
> +
> +#include <nss_files.h>
> +
> +#include <ctype.h>
> +#include <errno.h>
> +#include <string.h>
> +
> +int
> +__nss_readline (FILE *fp, char *buf, size_t len, off64_t *poffset)
> +{

OK. Like this clean implementation.

> +  /* We need space for at least one character, the line terminator,
> +     and the NUL byte.  */
> +  if (len < 3)
> +    {
> +      *poffset = -1;
> +      __set_errno (ERANGE);
> +      return ERANGE;
> +    }
> +
> +  while (true)
> +    {
> +      /* Keep original offset for retries.  */
> +      *poffset = __ftello64 (fp);
> +
> +      buf[len - 1] = '\xff';        /* Marker to recognize truncation.  */
> +      if (fgets_unlocked (buf, len, fp) == NULL)
> +        {
> +          if (feof_unlocked (fp))
> +            {
> +              __set_errno (ENOENT);
> +              return ENOENT;
> +            }
> +          else
> +            {
> +              /* Any other error.  Do not return ERANGE in this case
> +                 because the caller would retry.  */
> +              if (errno == ERANGE)
> +                __set_errno (EINVAL);
> +              return errno;
> +            }
> +        }
> +      else if (buf[len - 1] != '\xff')
> +        /* The buffer is too small.  Arrange for re-reading the same
> +           line on the next call.  */
> +        return __nss_readline_seek (fp, *poffset);
> +
> +      /* fgets_unlocked succeeded.  */
> +
> +      /* Remove leading whitespace.  */
> +      char *p = buf;
> +      while (isspace (*p))
> +        ++p;
> +      if (*p == '\0' || *p == '#')
> +        /* Skip empty lines and comments.  */
> +        continue;
> +      if (p != buf)
> +        memmove (buf, p, strlen (p));
> +
> +      /* Return line to the caller.  */
> +      return 0;
> +    }
> +}
> +libc_hidden_def (__nss_readline)
> +
> +int
> +__nss_readline_seek (FILE *fp, off64_t offset)
> +{
> +  if (offset < 0 /* __ftello64 failed.  */
> +      || __fseeko64 (fp, offset, SEEK_SET) < 0)
> +    {
> +      /* Without seeking support, it is not possible to
> +         re-read the same line, so this is a hard failure.  */

OK.

> +      fseterr_unlocked (fp);
> +      __set_errno (ESPIPE);
> +      return ESPIPE;
> +    }
> +  else
> +    {
> +      __set_errno (ERANGE);
> +      return ERANGE;
> +    }
> +}
>
  

Patch

diff --git a/include/nss_files.h b/include/nss_files.h
index d0f26815b5..f45ea02dc0 100644
--- a/include/nss_files.h
+++ b/include/nss_files.h
@@ -25,6 +25,28 @@ 
 FILE *__nss_files_fopen (const char *path);
 libc_hidden_proto (__nss_files_fopen)
 
+/* Read a line from FP, storing it BUF.  Strip leading blanks and skip
+   comments.  Sets errno and returns error code on failure.  Special
+   failure: ERANGE means the buffer is too small.  The function writes
+   the original offset to *POFFSET (which can be negative in the case
+   of non-seekable input).  */
+int __nss_readline (FILE *fp, char *buf, size_t len, off64_t *poffset);
+libc_hidden_proto (__nss_readline)
+
+/* Seek FP to OFFSET.  Sets errno and returns error code on failure.
+   On success, sets errno to ERANGE and returns ERANGE (to indicate
+   re-reading of the same input line to the caller).  If OFFSET is
+   negative, fail with ESPIPE without seeking.  Intended to be used
+   after parsing data read by __nss_readline failed with ERANGE.  */
+int __nss_readline_seek (FILE *fp, off64_t offset) attribute_hidden;
+
+/* Handles the result of a parse_line call (as defined by
+   nss/nss_files/files-parse.c).  Adjusts the file offset of FP as
+   necessary.  Returns 0 on success, and updates errno on failure (and
+   returns that error code).  */
+int __nss_parse_line_result (FILE *fp, off64_t offset, int parse_line_result);
+libc_hidden_proto (__nss_parse_line_result)
+
 struct parser_data;
 
 /* Instances of the parse_line function from
@@ -52,4 +74,11 @@  libnss_files_hidden_proto (_nss_files_parse_servent)
 libc_hidden_proto (_nss_files_parse_sgent)
 libc_hidden_proto (_nss_files_parse_spent)
 
+/* Generic implementation of fget*ent_r.  Reads lines from FP until
+   EOF or a successful parse into *RESULT using PARSER.  Returns 0 on
+   success, ENOENT on EOF, ERANGE on too-small buffer.  */
+int __nss_fgetent_r (FILE *fp, void *result,
+                     char *buffer, size_t buffer_length,
+                     nss_files_parse_line parser) attribute_hidden;
+
 #endif /* _NSS_FILES_H */
diff --git a/nss/Makefile b/nss/Makefile
index 00f4d89310..20c412c3e1 100644
--- a/nss/Makefile
+++ b/nss/Makefile
@@ -28,7 +28,9 @@  headers			:= nss.h
 routines		= nsswitch getnssent getnssent_r digits_dots \
 			  valid_field valid_list_field rewrite_field \
 			  $(addsuffix -lookup,$(databases)) \
-			  compat-lookup nss_hash nss_files_fopen
+			  compat-lookup nss_hash nss_files_fopen \
+			  nss_readline nss_parse_line_result \
+			  nss_fgetent_r
 
 # These are the databases that go through nss dispatch.
 # Caution: if you add a database here, you must add its real name
diff --git a/nss/Versions b/nss/Versions
index f489cb6eb0..71703750bf 100644
--- a/nss/Versions
+++ b/nss/Versions
@@ -18,7 +18,7 @@  libc {
     __nss_passwd_lookup2; __nss_group_lookup2; __nss_hosts_lookup2;
     __nss_services_lookup2; __nss_next2; __nss_lookup;
     __nss_hash; __nss_database_lookup2;
-    __nss_files_fopen;
+    __nss_files_fopen; __nss_readline; __nss_parse_line_result;
   }
 }
 
diff --git a/nss/nss_fgetent_r.c b/nss/nss_fgetent_r.c
new file mode 100644
index 0000000000..8f7c5b5cc7
--- /dev/null
+++ b/nss/nss_fgetent_r.c
@@ -0,0 +1,55 @@ 
+/* Generic implementation of fget*ent_r.
+   Copyright (C) 2020 Free Software Foundation, Inc.
+   This file is part of the GNU C Library.
+
+   The GNU C Library is free software; you can redistribute it and/or
+   modify it under the terms of the GNU Lesser General Public
+   License as published by the Free Software Foundation; either
+   version 2.1 of the License, or (at your option) any later version.
+
+   The GNU C Library is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   Lesser General Public License for more details.
+
+   You should have received a copy of the GNU Lesser General Public
+   License along with the GNU C Library; if not, see
+   <https://www.gnu.org/licenses/>.  */
+
+#include <errno.h>
+#include <nss_files.h>
+
+int
+__nss_fgetent_r (FILE *fp, void *result, char *buffer, size_t buffer_length,
+                 nss_files_parse_line parser)
+{
+  int ret;
+
+  _IO_flockfile (fp);
+
+  while (true)
+    {
+      off64_t original_offset;
+      ret = __nss_readline (fp, buffer, buffer_length, &original_offset);
+      if (ret == 0)
+        {
+          /* Parse the line into *RESULT.  */
+          ret = parser (buffer, result,
+                        (struct parser_data *) buffer, buffer_length, &errno);
+
+          /* Translate the result code from the parser into an errno
+             value.  Also seeks back to the start of the line if
+             necessary.  */
+          ret = __nss_parse_line_result (fp, original_offset, ret);
+
+          if (ret == EINVAL)
+            /* Skip over malformed lines.  */
+            continue;
+        }
+      break;
+    }
+
+  _IO_funlockfile (fp);
+
+  return ret;
+}
diff --git a/nss/nss_files/files-XXX.c b/nss/nss_files/files-XXX.c
index 9cc5137953..1db9e46127 100644
--- a/nss/nss_files/files-XXX.c
+++ b/nss/nss_files/files-XXX.c
@@ -135,10 +135,9 @@  internal_getent (FILE *stream, struct STRUCTURE *result,
 		 char *buffer, size_t buflen, int *errnop H_ERRNO_PROTO
 		 EXTRA_ARGS_DECL)
 {
-  char *p;
   struct parser_data *data = (void *) buffer;
   size_t linebuflen = buffer + buflen - data->linebuffer;
-  int parse_result;
+  int saved_errno = errno;	/* Do not clobber errno on success.  */
 
   if (buflen < sizeof *data + 2)
     {
@@ -149,66 +148,42 @@  internal_getent (FILE *stream, struct STRUCTURE *result,
 
   while (true)
     {
-      ssize_t r = __libc_readline_unlocked
-	(stream, data->linebuffer, linebuflen);
-      if (r < 0)
-	{
-	  *errnop = errno;
-	  H_ERRNO_SET (NETDB_INTERNAL);
-	  if (*errnop == ERANGE)
-	    /* Request larger buffer.  */
-	    return NSS_STATUS_TRYAGAIN;
-	  else
-	    /* Other read failure.  */
-	    return NSS_STATUS_UNAVAIL;
-	}
-      else if (r == 0)
+      off64_t original_offset;
+      int ret = __nss_readline (stream, data->linebuffer, linebuflen,
+				&original_offset);
+      if (ret == ENOENT)
 	{
 	  /* End of file.  */
 	  H_ERRNO_SET (HOST_NOT_FOUND);
+	  __set_errno (saved_errno);
 	  return NSS_STATUS_NOTFOUND;
 	}
-
-      /* Everything OK.  Now skip leading blanks.  */
-      p = data->linebuffer;
-      while (isspace (*p))
-	++p;
-
-      /* Ignore empty and comment lines.  */
-      if (*p == '\0' || *p == '#')
-	continue;
-
-      /* Parse the line.   */
-      *errnop = EINVAL;
-      parse_result = parse_line (p, result, data, buflen, errnop EXTRA_ARGS);
-
-      if (parse_result == -1)
+      else if (ret == 0)
 	{
-	  if (*errnop == ERANGE)
+	  ret = __nss_parse_line_result (stream, original_offset,
+					 parse_line (data->linebuffer,
+						     result, data, buflen,
+						     errnop EXTRA_ARGS));
+	  if (ret == 0)
 	    {
-	      /* Return to the original file position at the beginning
-		 of the line, so that the next call can read it again
-		 if necessary.  */
-	      if (__fseeko64 (stream, -r, SEEK_CUR) != 0)
-		{
-		  if (errno == ERANGE)
-		    *errnop = EINVAL;
-		  else
-		    *errnop = errno;
-		  H_ERRNO_SET (NETDB_INTERNAL);
-		  return NSS_STATUS_UNAVAIL;
-		}
+	      /* Line has been parsed successfully.  */
+	      __set_errno (saved_errno);
+	      return NSS_STATUS_SUCCESS;
 	    }
-	  H_ERRNO_SET (NETDB_INTERNAL);
-	  return NSS_STATUS_TRYAGAIN;
+	  else if (ret == EINVAL)
+	    /* If it is invalid, loop to get the next line of the file
+	       to parse.  */
+	    continue;
 	}
 
-      /* Return the data if parsed successfully.  */
-      if (parse_result != 0)
-	return NSS_STATUS_SUCCESS;
-
-      /* If it is invalid, loop to get the next line of the file to
-	 parse.  */
+      *errnop = ret;
+      H_ERRNO_SET (NETDB_INTERNAL);
+      if (ret == ERANGE)
+	/* Request larger buffer.  */
+	return NSS_STATUS_TRYAGAIN;
+      else
+	/* Other read failure.  */
+	return NSS_STATUS_UNAVAIL;
     }
 }
 
diff --git a/nss/nss_parse_line_result.c b/nss/nss_parse_line_result.c
new file mode 100644
index 0000000000..cd008e3c36
--- /dev/null
+++ b/nss/nss_parse_line_result.c
@@ -0,0 +1,46 @@ 
+/* Implementation of __nss_parse_line_result.
+   Copyright (C) 2020 Free Software Foundation, Inc.
+   This file is part of the GNU C Library.
+
+   The GNU C Library is free software; you can redistribute it and/or
+   modify it under the terms of the GNU Lesser General Public
+   License as published by the Free Software Foundation; either
+   version 2.1 of the License, or (at your option) any later version.
+
+   The GNU C Library is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   Lesser General Public License for more details.
+
+   You should have received a copy of the GNU Lesser General Public
+   License along with the GNU C Library; if not, see
+   <https://www.gnu.org/licenses/>.  */
+
+#include <nss_files.h>
+
+#include <assert.h>
+#include <errno.h>
+
+int
+__nss_parse_line_result (FILE *fp, off64_t offset, int parse_line_result)
+{
+  assert (parse_line_result >= -1 && parse_line_result <= 1);
+
+  switch (__builtin_expect (parse_line_result, 1))
+    {
+    case 1:
+      /* Sucess.  */
+      return 0;
+    case 0:
+      /* Parse error.  */
+      __set_errno (EINVAL);
+      return EINVAL;
+    case -1:
+      /* Out of buffer space.  */
+      return __nss_readline_seek (fp, offset);
+
+      default:
+        __builtin_unreachable ();
+    }
+}
+libc_hidden_def (__nss_parse_line_result)
diff --git a/nss/nss_readline.c b/nss/nss_readline.c
new file mode 100644
index 0000000000..44e0dd9319
--- /dev/null
+++ b/nss/nss_readline.c
@@ -0,0 +1,99 @@ 
+/* Read a line from an nss_files database file.
+   Copyright (C) 2020 Free Software Foundation, Inc.
+   This file is part of the GNU C Library.
+
+   The GNU C Library is free software; you can redistribute it and/or
+   modify it under the terms of the GNU Lesser General Public
+   License as published by the Free Software Foundation; either
+   version 2.1 of the License, or (at your option) any later version.
+
+   The GNU C Library is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   Lesser General Public License for more details.
+
+   You should have received a copy of the GNU Lesser General Public
+   License along with the GNU C Library; if not, see
+   <https://www.gnu.org/licenses/>.  */
+
+#include <nss_files.h>
+
+#include <ctype.h>
+#include <errno.h>
+#include <string.h>
+
+int
+__nss_readline (FILE *fp, char *buf, size_t len, off64_t *poffset)
+{
+  /* We need space for at least one character, the line terminator,
+     and the NUL byte.  */
+  if (len < 3)
+    {
+      *poffset = -1;
+      __set_errno (ERANGE);
+      return ERANGE;
+    }
+
+  while (true)
+    {
+      /* Keep original offset for retries.  */
+      *poffset = __ftello64 (fp);
+
+      buf[len - 1] = '\xff';        /* Marker to recognize truncation.  */
+      if (fgets_unlocked (buf, len, fp) == NULL)
+        {
+          if (feof_unlocked (fp))
+            {
+              __set_errno (ENOENT);
+              return ENOENT;
+            }
+          else
+            {
+              /* Any other error.  Do not return ERANGE in this case
+                 because the caller would retry.  */
+              if (errno == ERANGE)
+                __set_errno (EINVAL);
+              return errno;
+            }
+        }
+      else if (buf[len - 1] != '\xff')
+        /* The buffer is too small.  Arrange for re-reading the same
+           line on the next call.  */
+        return __nss_readline_seek (fp, *poffset);
+
+      /* fgets_unlocked succeeded.  */
+
+      /* Remove leading whitespace.  */
+      char *p = buf;
+      while (isspace (*p))
+        ++p;
+      if (*p == '\0' || *p == '#')
+        /* Skip empty lines and comments.  */
+        continue;
+      if (p != buf)
+        memmove (buf, p, strlen (p));
+
+      /* Return line to the caller.  */
+      return 0;
+    }
+}
+libc_hidden_def (__nss_readline)
+
+int
+__nss_readline_seek (FILE *fp, off64_t offset)
+{
+  if (offset < 0 /* __ftello64 failed.  */
+      || __fseeko64 (fp, offset, SEEK_SET) < 0)
+    {
+      /* Without seeking support, it is not possible to
+         re-read the same line, so this is a hard failure.  */
+      fseterr_unlocked (fp);
+      __set_errno (ESPIPE);
+      return ESPIPE;
+    }
+  else
+    {
+      __set_errno (ERANGE);
+      return ERANGE;
+    }
+}