Subject: standards/19209: test(1)'s -r, -w, and -x don't match POSIX for root (or 4.4BSD, or even V7)
To: None <gnats-bugs@gnats.netbsd.org>
From: Greg A. Woods <woods@weird.com>
List: netbsd-bugs
Date: 11/29/2002 20:16:52
>Number:         19209
>Category:       standards
>Synopsis:       test(1)'s -r, -w, and -x don't match POSIX for root (or 4.4BSD, or even V7)
>Confidential:   no
>Severity:       serious
>Priority:       medium
>Responsible:    standards-manager
>State:          open
>Class:          sw-bug
>Submitter-Id:   net
>Arrival-Date:   Fri Nov 29 17:17:00 PST 2002
>Closed-Date:
>Last-Modified:
>Originator:     Greg A. Woods
>Release:        netbsd-1.6
>Organization:
Planix, Inc.; Toronto, Ontario; Canada
>Environment:
System: NetBSD 1.6
>Description:

	I've wanted to fix this for a while, but finally got perturbed
	into doing it due to frustrations with a small script I was
	porting -- one that has to run as root.

	For the nitty gritty details see the comment included in the
	patch below....

>How-To-Repeat:

	try the following as root:

	# chmod a-w /bin/sh	# just in case....
	# if [ -w /bin/sh ] ; then echo "OOPS!  /bin/sh is writable!" ; fi

>Fix:

Index: test.c
===================================================================
RCS file: /cvs/master/m-NetBSD/main/basesrc/bin/test/test.c,v
retrieving revision 1.24
diff -c -c -r1.24 test.c
*** test.c	16 Sep 2001 19:03:26 -0000	1.24
--- test.c	30 Nov 2002 01:02:09 -0000
***************
*** 353,371 ****
  filstat(char *nm, enum token mode)
  {
  	struct stat s;
  
  	if (mode == FILSYM ? lstat(nm, &s) : stat(nm, &s))
  		return 0;
  
  	switch (mode) {
  	case FILRD:
! 		return access(nm, R_OK) == 0;
  	case FILWR:
! 		return access(nm, W_OK) == 0;
  	case FILEX:
! 		return access(nm, X_OK) == 0;
  	case FILEXIST:
! 		return access(nm, F_OK) == 0;
  	case FILREG:
  		return S_ISREG(s.st_mode);
  	case FILDIR:
--- 353,433 ----
  filstat(char *nm, enum token mode)
  {
  	struct stat s;
+ 	u_int16_t i;
  
  	if (mode == FILSYM ? lstat(nm, &s) : stat(nm, &s))
  		return 0;
  
+ 	/*
+ 	 * The manual, and IEEE POSIX 1003.2, suggests these should check the
+ 	 * mode bits, not use access().  For example for '-w':
+ 	 *
+ 	 *	True shall indicate only that the write flag is on.  The file
+ 	 *	is not writable on a read-only file system even if this test
+ 	 *	indicates true.
+ 	 *
+ 	 * On the other hand IEEE POSIX 1003.1-2001, as quoted in SuSv3, says:
+ 	 *
+ 	 *	True shall indicate that permission to write from[sic] file
+ 	 *	will be granted, as defined in File Read, Write, and Creation.
+ 	 *
+ 	 * However that section says only:
+ 	 *
+ 	 *	When a file is to be read or written, the file shall be opened
+ 	 *	with an access mode corresponding to the operation to be
+ 	 *	performed.  If file access permissions deny access, the
+ 	 *	requested operation shall fail.
+ 	 *
+ 	 * and when I first read this I though surely we can't go about using
+ 	 * open(O_WRITE) to try this test!  However the POSIX 1003.1-2001
+ 	 * Rationale section for test does in fact say:
+ 	 *
+ 	 *	On historical BSD systems, test -w directory always returned
+ 	 *	false because test tried to open the directory for writing,
+ 	 *	which always fails.
+ 	 *
+ 	 * and indeed this is in fact true for Seventh Edition UNIX, UNIX 32V,
+ 	 * and UNIX System III, and thus presumably also for BSD up to and
+ 	 * including 4.3.
+ 	 *
+ 	 * Oddly the SysV implementation (at least in the 'test' builtin in
+ 	 * /bin/sh) also uses access(name, 2) even though it goes to much
+ 	 * greater lengths to test for execute permissions (like pdksh does).
+ 	 *
+ 	 * Interestingly the 4.4BSD was correct and it was implemented
+ 	 * "intelligently" with stat() instead of open().
+ 	 *
+ 	 * This was apparently broken in NetBSD around about 1994/06/30 when
+ 	 * the old 4.4BSD implementation was replaced with a (arguably much
+ 	 * better coded) implementation derived from pdksh.
+ 	 *
+ 	 * Note that modern pdksh is yet different again, but still not
+ 	 * correct, at least not w.r.t. POSIX as I interpret it.
+ 	 */
  	switch (mode) {
  	case FILRD:
! 		i = S_IROTH;
! 		if (s.st_uid == geteuid())
! 			i = S_IRUSR;
! 		else if (s.st_gid == getegid())
! 			i = S_IRGRP;
! 		return (s.st_mode & i);
  	case FILWR:
! 		i = S_IWOTH;
! 		if (s.st_uid == geteuid())
! 			i = S_IWUSR;
! 		else if (s.st_gid == getegid())
! 			i = S_IWGRP;
! 		return (s.st_mode & i);
  	case FILEX:
! 		i = S_IXOTH;
! 		if (s.st_uid == geteuid())
! 			i = S_IXUSR;
! 		else if (s.st_gid == getegid())
! 			i = S_IXGRP;
! 		return (s.st_mode & i);
  	case FILEXIST:
! 		return 1;	/* the successful lstat()/stat() is good enough */
  	case FILREG:
  		return S_ISREG(s.st_mode);
  	case FILDIR:
>Release-Note:
>Audit-Trail:
>Unformatted: