Subject: bug#8391: chmod setuid & setguid bits

From: Paul Eggert <[email-masked]>
Date: Mon, 05 Mar 2012 23:26:56 -0800

On 03/05/2012 02:27 PM, Bruno Haible wrote:

> Then how about using "==" or ":=" to designate the assignment?

That's too fancy.  Plain '=' would be better.

We can also support notations like '+700' and '-77' to
OR-in or AND-out arbitrary masks.  This would be
a clear and straightforward extension to the
existing symbolic modes.

Something like the attached patches for gnulib and
coreutils, perhaps?  If this looks OK
I'll add test cases.
modechange: add notations +40, 00440, etc.
* lib/modechange.c (mode_compile): Support new notations
+40, -40, =440, 00440.  See <http://debbugs.gnu.org/8391>.
diff --git a/lib/modechange.c b/lib/modechange.c
index 4ae90ca..ea46b6c 100644
--- a/lib/modechange.c
+++ b/lib/modechange.c
@@ -136,6 +136,7 @@ mode_compile (char const *mode_string)
   /* The array of mode-change directives to be returned.  */
   struct mode_change *mc;
   size_t used = 0;
+  char const *p;

   if ('0' <= *mode_string && *mode_string < '8')
     {
@@ -143,40 +144,43 @@ mode_compile (char const *mode_string)
       mode_t mode;
       mode_t mentioned;

+      p = mode_string;
       do
         {
-          octal_mode = 8 * octal_mode + *mode_string++ - '0';
+          octal_mode = 8 * octal_mode + *p++ - '0';
           if (ALLM < octal_mode)
             return NULL;
         }
-      while ('0' <= *mode_string && *mode_string < '8');
+      while ('0' <= *p && *p < '8');

-      if (*mode_string)
+      if (*p)
         return NULL;

       mode = octal_to_mode (octal_mode);
-      mentioned = (mode & (S_ISUID | S_ISGID)) | S_ISVTX | S_IRWXUGO;
+      mentioned = (p - mode_string < 5
+                   ? (mode & (S_ISUID | S_ISGID)) | S_ISVTX | S_IRWXUGO
+                   : CHMOD_MODE_BITS);
       return make_node_op_equals (mode, mentioned);
     }

   /* Allocate enough space to hold the result.  */
   {
     size_t needed = 1;
-    char const *p;
     for (p = mode_string; *p; p++)
       needed += (*p == '=' || *p == '+' || *p == '-');
     mc = xnmalloc (needed, sizeof *mc);
   }

-  /* One loop iteration for each '[ugoa]*([-+=]([rwxXst]*|[ugo]))+'.  */
-  for (;; mode_string++)
+  /* One loop iteration for each
+     '[ugoa]*([-+=]([rwxXst]*|[ugo]))+|[-+=][0-7]+'.  */
+  for (p = mode_string; ; p++)
     {
       /* Which bits in the mode are operated on.  */
       mode_t affected = 0;

       /* Turn on all the bits in 'affected' for each group given.  */
-      for (;; mode_string++)
-        switch (*mode_string)
+      for (;; p++)
+        switch (*p)
           {
           default:
             goto invalid;
@@ -199,35 +203,59 @@ mode_compile (char const *mode_string)

       do
         {
-          char op = *mode_string++;
+          char op = *p++;
           mode_t value;
           char flag = MODE_COPY_EXISTING;
           struct mode_change *change;

-          switch (*mode_string++)
+          switch (*p)
             {
+            case '0': case '1': case '2': case '3':
+            case '4': case '5': case '6': case '7':
+              {
+                unsigned int octal_mode = 0;
+
+                do
+                  {
+                    octal_mode = 8 * octal_mode + *p++ - '0';
+                    if (ALLM < octal_mode)
+                      return NULL;
+                  }
+                while ('0' <= *p && *p < '8');
+
+                if (affected || (*p && *p != ','))
+                  return NULL;
+                affected = CHMOD_MODE_BITS;
+                value = octal_to_mode (octal_mode);
+                flag = MODE_ORDINARY_CHANGE;
+                break;
+              }
+
             case 'u':
               /* Set the affected bits to the value of the "u" bits
                  on the same file.  */
               value = S_IRWXU;
+              p++;
               break;
             case 'g':
               /* Set the affected bits to the value of the "g" bits
                  on the same file.  */
               value = S_IRWXG;
+              p++;
               break;
             case 'o':
               /* Set the affected bits to the value of the "o" bits
                  on the same file.  */
               value = S_IRWXO;
+              p++;
               break;

             default:
               value = 0;
               flag = MODE_ORDINARY_CHANGE;

-              for (mode_string--;; mode_string++)
-                switch (*mode_string)
+              for (;; p++)
+                switch (*p)
                   {
                   case 'r':
                     value |= S_IRUSR | S_IRGRP | S_IROTH;
@@ -262,14 +290,13 @@ mode_compile (char const *mode_string)
           change->value = value;
           change->mentioned = (affected ? affected & value : value);
         }
-      while (*mode_string == '=' || *mode_string == '+'
-             || *mode_string == '-');
+      while (*p == '=' || *p == '+' || *p == '-');

-      if (*mode_string != ',')
+      if (*p != ',')
         break;
     }

-  if (*mode_string == 0)
+  if (*p == 0)
     {
       mc[used].flag = MODE_DONE;
       return mc;
chmod: add notations +40, 00440, etc.
* NEWS: Document this.
* doc/perm.texi (Operator Numeric Modes): New section.
(Numeric Modes, Directory Setuid and Setgid): Document new behavior.
* src/chmod.c (usage): Document new behavior.
(main): Support new options -0, -1, etc.
diff --git a/NEWS b/NEWS
index 8006669..4f12a62 100644
--- a/NEWS
+++ b/NEWS
@@ -4,6 +4,21 @@ GNU coreutils NEWS                                    -*- outline -*-

 ** New features

+  As a GNU extension, 'chmod', 'mkdir', and 'install' now accept operators
+  '-', '+', '=' followed by octal modes; for example, 'chmod +40 FOO' enables
+  and 'chmod -40 FOO' disables FOO's group-read permissions.  Operator
+  numeric modes can be combined with symbolic modes by separating them with
+  commas; for example, =0,u+r clears all permissions except for enabling
+  user-read permissions.  Unlike ordinary numeric modes, operator numeric
+  modes do not preserve directory setuid and setgid bits; for example,
+  'chmod =0 FOO' clears all of FOO's permissions, including setuid and setgid.
+
+  Also, ordinary numeric modes with five or more digits no longer preserve
+  setuid and setgid bits, so that 'chmod 00755 FOO' now clears FOO's setuid
+  and setgid bits.  This allows scripts to be portable to other systems which
+  lack the GNU extension mentioned previously, and where ordinary numeric
+  modes do not preserve directory setuid and setgid bits.
+
   dd now accepts the count_bytes, skip_bytes iflags and the seek_bytes
   oflag, to more easily allow processing portions of a file.

diff --git a/doc/perm.texi b/doc/perm.texi
index 84f8500..c36f8ac 100644
--- a/doc/perm.texi
+++ b/doc/perm.texi
@@ -17,6 +17,7 @@ symbolic form or as an octal number.
 * Mode Structure::              Structure of file mode bits.
 * Symbolic Modes::              Mnemonic representation of file mode bits.
 * Numeric Modes::               File mode bits as octal numbers.
+* Operator Numeric Modes::      ANDing, ORing, and setting modes octally.
 * Directory Setuid and Setgid:: Set-user-ID and set-group-ID on directories.
 @end menu

@@ -495,13 +496,16 @@ alternative to giving a symbolic mode, you can give an octal (base 8)
 number that represents the mode.
 This number is always interpreted in octal; you do not have to add a
 leading @samp{0}, as you do in C.  Mode @samp{0055} is the same as
-mode @samp{55}.
+mode @samp{55}.  (However, modes of five digits or more, such as
+@samp{00055}, are treated specially.  @xref{Directory Setuid and Setgid}.)

 A numeric mode is usually shorter than the corresponding symbolic
 mode, but it is limited in that normally it cannot take into account the
 previous file mode bits; it can only set them absolutely.
-(As discussed in the next section, the set-user-ID and set-group-ID
-bits of directories are an exception to this general limitation.)
+The set-user-ID and set-group-ID bits of directories are an exception
+to this general limitation; @xref{Directory Setuid and Setgid}.
+Also, operator numeric modes can take previous file mode bits into
+account; @xref{Operator Numeric Modes}.

 The permissions granted to the user,
 to other users in the file's group,
@@ -541,6 +545,26 @@ For example, numeric mode @samp{4755} corresponds to symbolic mode
 @samp{ug=rw,o=r}.  Numeric mode @samp{0} corresponds to symbolic mode
 @samp{a=}.

+@node Operator Numeric Modes
+@section Operator Numeric Modes
+
+An operator numeric mode is a numeric mode that is prefixed by a
+@samp{-}, @samp{+}, or @samp{=} operator, which has the same
+interpretation as in symbolic modes.  For example, @samp{+440} enables
+read permission for the file's owner and group, @samp{-1} disables
+execute permission for other users, and @samp{=600} clears all
+permissions that it enables read-write permissions for the file's
+owner.  Operator numeric modes can be combined with symbolic modes by
+separating them with a comma; for example, @samp{=0,u+r} clears all
+permissions except for enabling read permission for the file's owner.
+
+The commands @samp{chmod =755 @var{dir}} and @samp{chmod 755
+@var{dir}} differ in that the former clears the directory @var{dir}'s
+setuid and setgid bits, whereas the latter preserves them.
+@xref{Directory Setuid and Setgid}.
+
+Operator numeric modes are a @acronym{GNU} extension.
+
 @node Directory Setuid and Setgid
 @section Directories and the Set-User-ID and Set-Group-ID Bits

@@ -559,8 +583,10 @@ bits of directories.  If commands like @command{chmod} and
 mechanisms would be less convenient and it would be harder to share
 files.  Therefore, a command like @command{chmod} does not affect the
 set-user-ID or set-group-ID bits of a directory unless the user
-specifically mentions them in a symbolic mode, or sets them in
-a numeric mode.  For example, on systems that support
+specifically mentions them in a symbolic mode, or uses an operator
+numeric mode such as @samp{=755}, or sets them in a numeric mode, or
+clears them in a numeric mode that has five or more octal digits.
+For example, on systems that support
 set-group-ID inheritance:

 @example
@@ -582,20 +608,27 @@ explicitly in the symbolic or numeric modes, e.g.:
 @example
 # These commands try to set the set-user-ID
 # and set-group-ID bits of the subdirectories.
-mkdir G H
+mkdir G
 chmod 6755 G
-chmod u=rwx,go=rx,a+s H
-mkdir -m 6755 I
+chmod +6000 G
+chmod u=rwx,go=rx,a+s G
+mkdir -m 6755 H
+mkdir -m +6000 I
 mkdir -m u=rwx,go=rx,a+s J
 @end example

 If you want to try to clear these bits, you must mention them
-explicitly in a symbolic mode, e.g.:
+explicitly in a symbolic mode, or use a symbolic mode containing
+=@var{octal-digits}, or specify a numeric mode with five or more octal
+digits, e.g.:

 @example
-# This command tries to clear the set-user-ID
+# These commands try to clear the set-user-ID
 # and set-group-ID bits of the directory D.
 chmod a-s D
+chmod -6000 D
+chmod =755 D
+chmod 00755 D
 @end example

 This behavior is a @acronym{GNU} extension.  Portable scripts should
diff --git a/src/chmod.c b/src/chmod.c
index a134e3f..aa4ac77 100644
--- a/src/chmod.c
+++ b/src/chmod.c
@@ -398,7 +398,7 @@ With --reference, change the mode of each FILE to that of RFILE.\n\
       fputs (VERSION_OPTION_DESCRIPTION, stdout);
       fputs (_("\
 \n\
-Each MODE is of the form '[ugoa]*([-+=]([rwxXst]*|[ugo]))+'.\n\
+Each MODE is of the form '[ugoa]*([-+=]([rwxXst]*|[ugo]))+|[-+=][0-7]+'.\n\
 "), stdout);
       emit_ancillary_info ();
     }
@@ -430,7 +430,8 @@ main (int argc, char **argv)
   recurse = force_silent = diagnose_surprises = false;

   while ((c = getopt_long (argc, argv,
-                           "Rcfvr::w::x::X::s::t::u::g::o::a::,::+::=::",
+                           ("Rcfvr::w::x::X::s::t::u::g::o::a::,::+::=::"
+                            "0::1::2::3::4::5::6::7::"),
                            long_options, NULL))
          != -1)
     {
@@ -449,6 +450,8 @@ main (int argc, char **argv)
         case ',':
         case '+':
         case '=':
+        case '0': case '1': case '2': case '3':
+        case '4': case '5': case '6': case '7':
           /* Support nonportable uses like "chmod -w", but diagnose
              surprises due to umask confusion.  Even though "--", "--r",
              etc., are valid modes, there is no "case '-'" here since