From roam@orbitel.bg  Wed Aug 16 08:29:17 2000
Return-Path: <roam@orbitel.bg>
Received: from sentinel.office1.bg (sentinel.office1.bg [195.24.48.182])
	by hub.freebsd.org (Postfix) with SMTP id 6848A37BFE6
	for <FreeBSD-gnats-submit@freebsd.org>; Wed, 16 Aug 2000 08:29:06 -0700 (PDT)
	(envelope-from roam@orbitel.bg)
Received: (qmail 2654 invoked by uid 1001); 16 Aug 2000 15:21:08 -0000
Message-Id: <20000816152108.2653.qmail@ringwraith.office1>
Date: 16 Aug 2000 15:21:08 -0000
From: Peter Pentchev <roam@orbitel.bg>
Reply-To: Peter Pentchev <roam@orbitel.bg>
To: FreeBSD-gnats-submit@freebsd.org
Subject: [PATCH] /bin/cp -p whines on set[ug]id immutable files
X-Send-Pr-Version: 3.2

>Number:         20646
>Category:       bin
>Synopsis:       [PATCH] /bin/cp -p whines on set[ug]id immutable files
>Confidential:   no
>Severity:       serious
>Priority:       medium
>Responsible:    dwmalone
>State:          closed
>Quarter:        
>Keywords:       
>Date-Required:  
>Class:          sw-bug
>Submitter-Id:   current-users
>Arrival-Date:   Wed Aug 16 08:30:00 PDT 2000
>Closed-Date:    Thu Jul 12 05:10:13 PDT 2001
>Last-Modified:  Thu Jul 12 05:12:47 PDT 2001
>Originator:     Peter Pentchev <roam@orbitel.bg>
>Release:        FreeBSD 4.1-STABLE i386
>Organization:
Orbitel JSCo
>Environment:

RELENG_4 as of today

>Description:

cp -p preserves, among others, the file flags. Unfortunately, this
clashes with its attempt just a little bit later to retain a file's
setuid/setgid/sticky bits if the user doing the copying is the file
owner.

(There was a similar PR about /sbin/restore's handling of utimes
 a while back.)

>How-To-Repeat:

[root@ringwraith /usr/home/roam]# ls -lo /usr/bin/passwd
-r-sr-xr-x  2 root  wheel  schg 26260 Aug 16 16:13 /usr/bin/passwd
[root@ringwraith /usr/home/roam]# cp -p /usr/bin/passwd .
cp: ./passwd: Operation not permitted
[root@ringwraith /usr/home/roam]# ls -lo passwd
-r-sr-xr-x  1 root  wheel  schg 26260 Aug 16 16:13 passwd
[root@ringwraith /usr/home/roam]#

>Fix:

Alright, so this might not be the best solution; arguably the best
way is to move the whole setuid/setgid/sticky bits fixup into
setfile() itself, where it belongs.  But this works for me ;)

diff -u -urN src/bin/cp/cp.c mysrc/bin/cp/cp.c
--- src/bin/cp/cp.c	Sun Nov 28 11:34:21 1999
+++ mysrc/bin/cp/cp.c	Wed Aug 16 18:03:29 2000
@@ -388,7 +388,7 @@
                          * umask; arguably wrong, but it's been that way
                          * forever.
 			 */
-			if (pflag && setfile(curr->fts_statp, 0))
+			if (pflag && setfile(curr->fts_statp, 0, 1))
 				badcp = rval = 1;
 			else if (dne)
 				(void)chmod(to.p_path,
diff -u -urN src/bin/cp/extern.h mysrc/bin/cp/extern.h
--- src/bin/cp/extern.h	Wed Aug 16 18:15:40 2000
+++ mysrc/bin/cp/extern.h	Wed Aug 16 17:46:20 2000
@@ -51,6 +51,6 @@
 int	copy_file __P((FTSENT *, int));
 int	copy_link __P((FTSENT *, int));
 int	copy_special __P((struct stat *, int));
-int	setfile __P((struct stat *, int));
+int	setfile __P((struct stat *, int, int));
 void	usage __P((void));
 __END_DECLS
diff -u -urN src/bin/cp/utils.c mysrc/bin/cp/utils.c
--- src/bin/cp/utils.c	Wed Aug 16 18:15:40 2000
+++ mysrc/bin/cp/utils.c	Wed Aug 16 18:16:37 2000
@@ -176,7 +176,8 @@
 	 * to remove it if we created it and its length is 0.
 	 */
 
-	if (pflag && setfile(fs, to_fd))
+	/* do not copy flags at this time, or the next (f)chmod() fails */
+	if (pflag && setfile(fs, to_fd, 0))
 		rval = 1;
 	/*
 	 * If the source was setuid or setgid, lose the bits unless the
@@ -194,6 +195,11 @@
 			rval = 1;
 		}
 	}
+
+	/* setfile() again, just for the file flags */
+	if (pflag && setfile(fs, to_fd, 2))
+		rval = 1;
+
 	(void)close(from_fd);
 	if (close(to_fd)) {
 		warn("%s", to.p_path);
@@ -239,7 +245,7 @@
 		warn("mkfifo: %s", to.p_path);
 		return (1);
 	}
-	return (pflag ? setfile(from_stat, 0) : 0);
+	return (pflag ? setfile(from_stat, 0, 1) : 0);
 }
 
 int
@@ -255,14 +261,23 @@
 		warn("mknod: %s", to.p_path);
 		return (1);
 	}
-	return (pflag ? setfile(from_stat, 0) : 0);
+	return (pflag ? setfile(from_stat, 0, 1) : 0);
 }
 
 
 int
-setfile(fs, fd)
+setfile(fs, fd, setflags)
 	register struct stat *fs;
-	int fd;
+	int fd, setflags;
+	/* values for setflags:
+	   0 - do not touch fd's flags;
+	   1 - set fd's flags to match fs's flags;
+	   2 - ONLY set fd's flags to fs's flags, do nothing more
+
+	   the only reason for setflags to be 2 is in copy_file()
+	   after the set[ug]id fixup; this would be better if
+	   the fixup itself were moved here.
+	*/
 {
 	static struct timeval tv[2];
 	struct stat ts;
@@ -292,28 +307,34 @@
 	 * the mode; current BSD behavior is to remove all setuid bits on
 	 * chown.  If chown fails, lose setuid/setgid bits.
 	 */
-	if (!gotstat || fs->st_uid != ts.st_uid || fs->st_gid != ts.st_gid)
-		if (fd ? fchown(fd, fs->st_uid, fs->st_gid) :
-		    chown(to.p_path, fs->st_uid, fs->st_gid)) {
-			if (errno != EPERM) {
+
+	/* Oops. Are we called *after* the copy_file() set[ug]id fixup? */
+	if (setflags != 2) {
+		if (!gotstat || fs->st_uid != ts.st_uid || fs->st_gid != ts.st_gid)
+			if (fd ? fchown(fd, fs->st_uid, fs->st_gid) :
+					chown(to.p_path, fs->st_uid, fs->st_gid)) {
+				if (errno != EPERM) {
+					warn("chown: %s", to.p_path);
+					rval = 1;
+				}
+				fs->st_mode &= ~(S_ISUID | S_ISGID);
+			}
+		
+		if (!gotstat || fs->st_mode != ts.st_mode)
+			if (fd ? fchmod(fd, fs->st_mode) : chmod(to.p_path, fs->st_mode)) {
 				warn("chown: %s", to.p_path);
 				rval = 1;
 			}
-			fs->st_mode &= ~(S_ISUID | S_ISGID);
-		}
-
-	if (!gotstat || fs->st_mode != ts.st_mode)
-		if (fd ? fchmod(fd, fs->st_mode) : chmod(to.p_path, fs->st_mode)) {
-			warn("chown: %s", to.p_path);
-			rval = 1;
-		}
+	}
 
-	if (!gotstat || fs->st_flags != ts.st_flags)
-		if (fd ?
-		    fchflags(fd, fs->st_flags) : chflags(to.p_path, fs->st_flags)) {
-			warn("chflags: %s", to.p_path);
-			rval = 1;
-		}
+	if (setflags) {
+		if (!gotstat || fs->st_flags != ts.st_flags)
+			if (fd ?
+					fchflags(fd, fs->st_flags) : chflags(to.p_path, fs->st_flags)) {
+				warn("chflags: %s", to.p_path);
+				rval = 1;
+			}
+	}
 
 	return (rval);
 }

>Release-Note:
>Audit-Trail:
Responsible-Changed-From-To: freebsd-bugs->dwmalone 
Responsible-Changed-By: dwmalone 
Responsible-Changed-When: Wed Aug 16 11:26:13 PDT 2000 
Responsible-Changed-Why:  
I looked at a similar PR for restore - I'll try to have a look at this one. 

http://www.freebsd.org/cgi/query-pr.cgi?pr=20646 

From: Peter Pentchev <roam@orbitel.bg>
To: freebsd-gnats-submit@freebsd.org
Cc:  
Subject: Re: bin/20646: [PATCH] /bin/cp -p whines on set[ug]id immutable files
Date: Wed, 16 Aug 2000 23:27:50 +0300

 On Wed, Aug 16, 2000 at 11:26:58AM -0700, dwmalone@FreeBSD.org wrote:
 > 
 > Responsible-Changed-From-To: freebsd-bugs->dwmalone
 > Responsible-Changed-By: dwmalone
 > Responsible-Changed-When: Wed Aug 16 11:26:13 PDT 2000
 > Responsible-Changed-Why: 
 > I looked at a similar PR for restore - I'll try to have a look at this one.
 
 Actually, Sheldon Hearn has been discussing this with me via e-mail
 for the past hour;  Sheldon, care to summarize? :)
 
 G'luck,
 Peter
 

From: Peter Pentchev <roam@orbitel.bg>
To: Sheldon Hearn <sheldonh@uunet.co.za>
Cc:  
Subject: Re: bin/20646: [PATCH] /bin/cp -p whines on set[ug]id immutable files
Date: Wed, 16 Aug 2000 23:16:16 +0300

 On Wed, Aug 16, 2000 at 09:16:21PM +0200, Sheldon Hearn wrote:
 > 
 > That's what I'm getting at.  I just wanted to be sure that I understand.
 > The problem here is not with the end result on the filesystem, but
 > rather with the manner in which cp(1) interacts with the caller.
 > 
 > I'll take a look and see if we can't come up with something simpler.
 > Don't take this badly; you yourself said that your patch may not embody
 > the best solution.  I'd just like to explore possible alternatives,
 > especially if we can find a more elegant solution.
 > 
 > Ciao,
 > Sheldon.
 > 
 
 OK, I see your point.  I'll sleep on it and think again tomorrow;
 I admin that today's patch was made somewhat in haste, because I
 *needed* a cp that would return success.  I'm too sleepy right now
 to figure out under what conditions this breaks anyway :(
 
 [ 15 minutes later ;-]
 
 Actually, instead of hiding behind sleepiness, I sat down and figured
 out just when this setuid/setgid fixup occured.  The result?
 A somewhat better patch, with a wholly different idea :)
 Look at it and tell me what you think.
 
 G'luck,
 Peter
 
 -- 
 I've heard that this sentence is a rumor.
 
 diff -u -urN src/bin/cp/utils.c mysrc/bin/cp/utils.c
 --- src/bin/cp/utils.c	Wed Aug 16 18:15:40 2000
 +++ mysrc/bin/cp/utils.c	Wed Aug 16 23:12:17 2000
 @@ -178,22 +178,11 @@
  
  	if (pflag && setfile(fs, to_fd))
  		rval = 1;
 -	/*
 -	 * If the source was setuid or setgid, lose the bits unless the
 -	 * copy is owned by the same user and group.
 -	 */
 -#define	RETAINBITS \
 -	(S_ISUID | S_ISGID | S_ISVTX | S_IRWXU | S_IRWXG | S_IRWXO)
 -	else if (fs->st_mode & (S_ISUID | S_ISGID) && fs->st_uid == myuid) {
 -		if (fstat(to_fd, &to_stat)) {
 -			warn("%s", to.p_path);
 -			rval = 1;
 -		} else if (fs->st_gid == to_stat.st_gid &&
 -		    fchmod(to_fd, fs->st_mode & RETAINBITS & ~myumask)) {
 -			warn("%s", to.p_path);
 -			rval = 1;
 -		}
 -	}
 +
 +	/* We no longer need to reinstate the setuid/setgid bits - see
 +	   the 'chown failed' part of setfile() - they are no longer reset,
 +	   and we couldn't reinstate them on immutable files anyway. */
 +
  	(void)close(from_fd);
  	if (close(to_fd)) {
  		warn("%s", to.p_path);
 @@ -299,7 +288,20 @@
  				warn("chown: %s", to.p_path);
  				rval = 1;
  			}
 -			fs->st_mode &= ~(S_ISUID | S_ISGID);
 +
 +			/* the chown failed, lose only the privileges
 +			   we have no right to have */
 +
 +			if ((fs->st_mode & S_ISUID) && (fs->st_uid != myuid))
 +				fs->st_mode &= ~S_ISUID;
 +			if ((fs->st_mode & S_ISGID) && (fs->st_gid != ts.st_gid))
 +				fs->st_mode &= ~S_ISGID;
 +
 +			/* I'm not quite sure what to do about the sticky bit -
 +			   for a file it doesn't matter, for a dir - I think
 +			   it's a good thing to have, just in case the world
 +			   decides to storm into our little newborn dir ;)
 +			   Think I'll just leave it alone. */
  		}
  
  	if (!gotstat || fs->st_mode != ts.st_mode)
 
 

From: Peter Pentchev <roam@orbitel.bg>
To: Sheldon Hearn <sheldonh@uunet.co.za>
Cc:  
Subject: Re: bin/20646: [PATCH] /bin/cp -p whines on set[ug]id immutable files
Date: Wed, 16 Aug 2000 23:24:38 +0300

 On Wed, Aug 16, 2000 at 10:11:50PM +0200, Sheldon Hearn wrote:
 > 
 > 
 > On Wed, 16 Aug 2000 21:16:21 +0200, Sheldon Hearn wrote:
 > 
 > > I'd just like to explore possible alternatives, especially if we can
 > > find a more elegant solution.
 > 
 > Hi Peter,
 > 
 > Could you give me a hand here?  I'm looking at this excerpt from
 > utils.c:
 > 
 > 
 > 	/*
 > 	 * Don't remove the target even after an error.  The target might
 > 	 * not be a regular file, or its attributes might be important,
 > 	 * or its contents might be irreplaceable.  It would only be safe
 > 	 * to remove it if we created it and its length is 0.
 > 	 */
 > 
 > 	if (pflag && setfile(fs, to_fd))
 > 		rval = 1;
 > 	/*
 > 	 * If the source was setuid or setgid, lose the bits unless the
 > 	 * copy is owned by the same user and group.
 > 	 */
 > #define	RETAINBITS \
 > 	(S_ISUID | S_ISGID | S_ISVTX | S_IRWXU | S_IRWXG | S_IRWXO)
 > 	else if (fs->st_mode & (S_ISUID | S_ISGID) && fs->st_uid == myuid) {
 > 		if (fstat(to_fd, &to_stat)) {
 > 			warn("%s", to.p_path);
 > 			rval = 1;
 > 		} else if (fs->st_gid == to_stat.st_gid &&
 > 		    fchmod(to_fd, fs->st_mode & RETAINBITS & ~myumask)) {
 > 			warn("%s", to.p_path);
 > 			rval = 1;
 > 		}
 > 	}
 > 	[...]
 > 
 > It's that fchmod() that generates the bogus error.  My question, though,
 > is why the else if clause depend on (fs->st_uid == myuid) instead of
 > (fs->st_uid != myuid).  The comment immediately preceding implies that
 > we don't need to lose any mode bits if the copy and the source are owned by
 > the same user and group.
 > 
 > In fact, it also doesn't make sense to me why S_ISUID and S_ISGID are
 > included in RETAINBITS, since the comment implies that these bits aren't
 > wanted in this case.  So clearly I'm missing something.
 > 
 > Can you explain what's going on in there? :-)
 > 
 > Thanks,
 > Sheldon.
 > 
 
 Well, actually, with the 15 minutes I mentioned in my previous e-mail,
 and with half the 2-liter bottle of Coke here, I think I can :)
 
 The comment is misleading - this excerpt does not *lose* any bits,
 it tries to *reinstate* the ones lost in utils.c, line 302 - setfile(),
 the case when chown() fails.  Apparently the idea is that if we
 cannot chown() the new file, we are not root, or it is not our file,
 so cp cannot afford to possibly let a cracker elevate his privileges
 in any way.  Then, copy_file() thinks: well, this file was owned by
 me anyway, there can be no additional gain in a setuid.  Same for groups.
 
 This is just what I'm addressing in the new patch I sent in my previous
 e-mail - setfile() only resets the setuid bit if the original file was
 not owned by me, and the setgid bit if it was not in the group I can
 create files as.  No additional arguments, no ugly checks - straight
 to the point (and no, cp doesn't whine on schg anymore :)
 
 G'luck,
 Peter
 
 -- 
 I had to translate this sentence into English because I could not read the original Sanskrit.
 
 
State-Changed-From-To: open->closed 
State-Changed-By: bde 
State-Changed-When: Thu Jul 12 05:10:13 PDT 2001 
State-Changed-Why:  
Fixed in -current rev.1.30 of utils.c, etc. 
Fixed in RELENG_4 in rev.1.27.2.2 utils.c, etc. 

http://www.FreeBSD.org/cgi/query-pr.cgi?pr=20646 
>Unformatted:
