Bug 128933 - [libc] realpath(3) does not follow SUS specification for ENOENT / ENOTDIR conditions
Summary: [libc] realpath(3) does not follow SUS specification for ENOENT / ENOTDIR con...
Status: Closed FIXED
Alias: None
Product: Base System
Classification: Unclassified
Component: kern (show other bugs)
Version: 1.0-CURRENT
Hardware: Any Any
: Normal Affects Only Me
Assignee: Konstantin Belousov
URL:
Keywords:
Depends on:
Blocks:
 
Reported: 2008-11-17 02:10 UTC by James Vega
Modified: 2012-09-12 16:50 UTC (History)
0 users

See Also:


Attachments

Note You need to log in before you can comment on or make changes to this bug.
Description James Vega 2008-11-17 02:10:02 UTC
According to the Single Unix Specification[0], the realpath stdlib function should return NULL (to indicate an error) when the first argument to realpath is either NULL or an empty string and set errno to EINVAL/ENOENT respectively.

For the empty string case, FreeBSD is currently populating resolved with the current working directory and returning the pointer to that.

For the NULL case, I see no check whether path is NULL or not.  Instead, the first use of it is dereferencing the pointer

  if (path[0] == '/'

[0] - http://www.opengroup.org/onlinepubs/009695399/functions/realpath.html
Comment 1 Андрей Чернов 2008-11-18 09:06:12 UTC
On Mon, Nov 17, 2008 at 02:04:59AM +0000, James Vega wrote:
> According to the Single Unix Specification[0], the realpath stdlib function should return NULL (to indicate an error) when the first argument to realpath is either NULL or an empty string and set errno to EINVAL/ENOENT respectively.
> 
> For the empty string case, FreeBSD is currently populating resolved with the current working directory and returning the pointer to that.
> 
> For the NULL case, I see no check whether path is NULL or not.  Instead, the first use of it is dereferencing the pointer
> 
>   if (path[0] == '/'
> 
> [0] - http://www.opengroup.org/onlinepubs/009695399/functions/realpath.html

The situation is even worse. realpath(3) breaks those SUS rules 
("shall fail if") too:

[ENOENT] A component of file_name does not name an existing file 
[ENOTDIR] A component of the path prefix is not a directory

i.e. realpath(3) does not detect fictional file name (last component) 
with ENOENT and does not detect intermediate non-directories components 
with ENOTDIR.

See following examples with realpath(1):

# realpath /non_existent
/non_existent
(should be "No such file or directory")


# realpath /bin/non_existent/cp
realpath: /bin/non_existent/cp: No such file or directory
(should be: "Not a directory", as early as non_existent checked)

-- 
http://ache.pp.ru/
Comment 2 Arjan van Leeuwen 2010-10-27 15:13:33 UTC
We ran into this recently. This is a real problem in portable code, since  
it's not possible to assume that realpath() will notify the caller if a  
file doesn't exist, even though it should according to the specification.

Arjan

-- 
Using Opera's revolutionary e-mail client: http://www.opera.com/mail/
Comment 3 Alexander Best freebsd_committer freebsd_triage 2010-10-27 18:51:58 UTC
Responsible Changed
From-To: freebsd-bugs->kib

Please note that the NULL issue has already been fixed by r206893 and PR 121897 
was closed. The issue Andrey Chernov described in his followup to this PR 
however remains to be fixed. 
Kib, could you take a look at this?
Comment 4 Kostik Belousov 2012-05-07 12:49:26 UTC
Quick look makes me think that the following change is enough.

diff --git a/lib/libc/stdlib/realpath.c b/lib/libc/stdlib/realpath.c
index 2c9562e..f038759 100644
--- a/lib/libc/stdlib/realpath.c
+++ b/lib/libc/stdlib/realpath.c
@@ -163,10 +163,8 @@ realpath(const char * __restrict path, char *
__restrict resolved)
 			return (NULL);
 		}
 		if (lstat(resolved, &sb) != 0) {
-			if (errno == ENOENT && p == NULL) {
-				errno = serrno;
-				return (resolved);
-			}
+			if (errno != ENOENT || p != NULL)
+				errno = ENOTDIR;
 			if (m)
 				free(resolved);
 			return (NULL);
Comment 5 dfilter service freebsd_committer freebsd_triage 2012-06-01 15:44:43 UTC
Author: kib
Date: Fri Jun  1 14:40:16 2012
New Revision: 236400
URL: http://svn.freebsd.org/changeset/base/236400

Log:
  MFC r235266:
  According to SUSv4, realpath(3) must fail if
    [ENOENT]  A component of file_name does not name an existing file or
        file_name points to an empty string.
    [ENOTDIR] A component of the path prefix is not a directory, or the
        file_name argument contains at least one non- <slash> character
        and ends with one or more trailing <slash> characters and the last
        pathname component names an existing file that is neither a
        directory nor a symbolic link to a directory.
  Add checks for the listed conditions, and set errno accordingly.
  
  Update the realpath(3) manpage to mention SUS behaviour. Remove the
  requirement to include sys/param.h before stdlib.h.
  
  PR:	128933

Modified:
  stable/9/lib/libc/stdlib/realpath.3
  stable/9/lib/libc/stdlib/realpath.c
Directory Properties:
  stable/9/lib/libc/   (props changed)

Modified: stable/9/lib/libc/stdlib/realpath.3
==============================================================================
--- stable/9/lib/libc/stdlib/realpath.3	Fri Jun  1 14:29:59 2012	(r236399)
+++ stable/9/lib/libc/stdlib/realpath.3	Fri Jun  1 14:40:16 2012	(r236400)
@@ -31,7 +31,7 @@
 .\"     @(#)realpath.3	8.2 (Berkeley) 2/16/94
 .\" $FreeBSD$
 .\"
-.Dd April 19, 2010
+.Dd May 11, 2012
 .Dt REALPATH 3
 .Os
 .Sh NAME
@@ -40,7 +40,6 @@
 .Sh LIBRARY
 .Lb libc
 .Sh SYNOPSIS
-.In sys/param.h
 .In stdlib.h
 .Ft "char *"
 .Fn realpath "const char *pathname" "char *resolved_path"
@@ -72,11 +71,12 @@ The
 function will resolve both absolute and relative paths
 and return the absolute pathname corresponding to
 .Fa pathname .
-All but the last component of
+All components of
 .Fa pathname
 must exist when
 .Fn realpath
-is called.
+is called, and all but the last component must name either directories or
+symlinks pointing to the directories.
 .Sh "RETURN VALUES"
 The
 .Fn realpath

Modified: stable/9/lib/libc/stdlib/realpath.c
==============================================================================
--- stable/9/lib/libc/stdlib/realpath.c	Fri Jun  1 14:29:59 2012	(r236399)
+++ stable/9/lib/libc/stdlib/realpath.c	Fri Jun  1 14:40:16 2012	(r236400)
@@ -132,8 +132,29 @@ realpath(const char * __restrict path, c
 			resolved[resolved_len++] = '/';
 			resolved[resolved_len] = '\0';
 		}
-		if (next_token[0] == '\0')
+		if (next_token[0] == '\0') {
+			/*
+			 * Handle consequential slashes.  The path
+			 * before slash shall point to a directory.
+			 *
+			 * Only the trailing slashes are not covered
+			 * by other checks in the loop, but we verify
+			 * the prefix for any (rare) "//" or "/\0"
+			 * occurence to not implement lookahead.
+			 */
+			if (lstat(resolved, &sb) != 0) {
+				if (m)
+					free(resolved);
+				return (NULL);
+			}
+			if (!S_ISDIR(sb.st_mode)) {
+				if (m)
+					free(resolved);
+				errno = ENOTDIR;
+				return (NULL);
+			}
 			continue;
+		}
 		else if (strcmp(next_token, ".") == 0)
 			continue;
 		else if (strcmp(next_token, "..") == 0) {
@@ -151,9 +172,7 @@ realpath(const char * __restrict path, c
 		}
 
 		/*
-		 * Append the next path component and lstat() it. If
-		 * lstat() fails we still can return successfully if
-		 * there are no more path components left.
+		 * Append the next path component and lstat() it.
 		 */
 		resolved_len = strlcat(resolved, next_token, PATH_MAX);
 		if (resolved_len >= PATH_MAX) {
@@ -163,10 +182,8 @@ realpath(const char * __restrict path, c
 			return (NULL);
 		}
 		if (lstat(resolved, &sb) != 0) {
-			if (errno == ENOENT && p == NULL) {
-				errno = serrno;
-				return (resolved);
-			}
+			if (errno != ENOENT || p != NULL)
+				errno = ENOTDIR;
 			if (m)
 				free(resolved);
 			return (NULL);
_______________________________________________
svn-src-all@freebsd.org mailing list
http://lists.freebsd.org/mailman/listinfo/svn-src-all
To unsubscribe, send any mail to "svn-src-all-unsubscribe@freebsd.org"
Comment 6 Konstantin Belousov freebsd_committer freebsd_triage 2012-06-01 17:27:27 UTC
State Changed
From-To: open->closed

Committed to HEAD and RELENG_9.
Comment 7 eblake 2012-09-12 16:42:41 UTC
The fix applied for this bug appears to have mis-interpreted SUS, in a
manner incompatible with glibc and Solaris implementations, and was
detected as a failure in the gnulib testsuite, as mentioned in this
thread[1].

[1]https://lists.gnu.org/archive/html/bug-gnulib/2012-09/msg00079.html

As already mentioned, SUS requires realpath to fail with ENOENT if a
component does not exist, and with ENOTDIR if a component is not a
directory.  But the SUS (POSIX) wording is intended that a file can be
detected only as not being a directory if it already exists as some
other file type, since ENOENT was already reserved for the case of not
existing.  Therefore, I disagree with your argument of:

> # realpath /bin/non_existent/cp
> realpath: /bin/non_existent/cp: No such file or directory
> (should be: "Not a directory", as early as non_existent checked)


and instead, argue that it should be:

# realpath /bin/non_existent/cp
realpath: /bin/non_existent/cp: No such file or directory
(correct - since /bin/non_existent does not exist, ENOENT trumps ENOTDIR)

# realpath /bin/cp/cp
realpath: /bin/cp/cp: Not a directory
(since /bin/cp exists, but is not a directory, /bin/cp/cp cannot be checked)

-- 
Eric Blake   eblake@redhat.com    +1-919-301-3266
Libvirt virtualization library http://libvirt.org