chiark / gitweb /
checkpath.c: Move the message buffer into the state structure.
[checkpath] / checkpath.c
index 993cce2df222b7a2826a8ad5ab74fd55e4c91f75..2cb73cda2fec3dd09839403ffd76018eb7901ee9 100644 (file)
@@ -1,13 +1,11 @@
 /* -*-c-*-
- *
- * $Id: checkpath.c,v 1.5 2003/01/25 23:58:44 mdw Exp $
  *
  * Check a path for safety
  *
  * (c) 1999 Mark Wooding
  */
 
-/*----- Licensing notice --------------------------------------------------* 
+/*----- Licensing notice --------------------------------------------------*
  *
  * This file is part of chkpath.
  *
  * it under the terms of the GNU General Public License as published by
  * the Free Software Foundation; either version 2 of the License, or
  * (at your option) any later version.
- * 
+ *
  * chkpath 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 General Public License for more details.
- * 
+ *
  * You should have received a copy of the GNU General Public License
  * along with chkpath; if not, write to the Free Software Foundation,
  * Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
  */
 
-/*----- Revision history --------------------------------------------------* 
- *
- * $Log: checkpath.c,v $
- * Revision 1.5  2003/01/25 23:58:44  mdw
- * Make guts into official library.
- *
- * Revision 1.4  2001/01/25 22:16:02  mdw
- * Make flags be unsigned.
- *
- * Revision 1.3  1999/05/21 22:07:20  mdw
- * Take advantage of new dynamic string macros.
- *
- * Revision 1.2  1999/05/18 20:49:12  mdw
- * Use a dynamic string for reading symlinks.
- *
- * Revision 1.1.1.1  1999/04/06 20:12:07  mdw
- * Import new project.
- *
- */
-
 /*----- Header files ------------------------------------------------------*/
 
+#include "config.h"
+
 #include <errno.h>
 #include <stdarg.h>
 #include <stdio.h>
@@ -63,6 +43,8 @@
 
 #include <mLib/alloc.h>
 #include <mLib/dstr.h>
+#include <mLib/macros.h>
+#include <mLib/pool.h>
 
 #include "checkpath.h"
 
@@ -78,24 +60,28 @@ struct elt {
   struct elt *e_link;                  /* Pointer to the next one along */
   size_t e_offset;                     /* Offset of name in path string */
   unsigned e_flags;                    /* Various useful flags */
+#define EF_STICKY 1u                   /*   Directory has sticky bit set */
   char e_name[1];                      /* Name of the directory */
 };
 
-#define f_sticky 1u                    /* Directory has sticky bit set */
-
-#define f_last 1u                      /* This is the final item to check */
+struct state {
+  pool *p;                             /* Allocation pool */
+  struct elt *sp;                      /* Stack pointer for list */
+  dstr path;                           /* Current path string */
+  dstr link;                           /* Symbolic link target string */
+  dstr msg;                            /* Message string */
+};
 
 /*----- Static variables --------------------------------------------------*/
 
-static struct elt rootnode = { 0, 0, 0 }; /* Root of the list */
-static struct elt *sp;                 /* Stack pointer for list */
-static dstr d = DSTR_INIT;             /* Current path string */
+static const struct elt rootnode = { 0, 0, 0 }; /* Root of the list */
 
 /*----- Main code ---------------------------------------------------------*/
 
 /* --- @splitpath@ --- *
  *
- * Arguments:  @const char *path@ = path string to break apart
+ * Arguments:  @struct state *state@ = pointer to state
+ *             @const char *path@ = path string to break apart
  *             @struct elt *tail@ = tail block to attach to end of list
  *
  * Returns:    Pointer to the new list head.
@@ -105,12 +91,13 @@ static dstr d = DSTR_INIT;         /* Current path string */
  *             be pushed onto the directory stack as required.
  */
 
-static struct elt *splitpath(const char *path, struct elt *tail)
+static struct elt *splitpath(struct state *state,
+                            const char *path, struct elt *tail)
 {
   struct elt *head, **ee = &head, *e;
+  size_t n;
 
   while (*path) {
-    size_t n;
 
     /* --- Either a leading `/', or a doubled one --- *
      *
@@ -128,7 +115,7 @@ static struct elt *splitpath(const char *path, struct elt *tail)
      */
 
     n = strcspn(path, "/");
-    e = xmalloc(sizeof(struct elt) + n + 1);
+    e = pool_alloc(state->p, offsetof(struct elt, e_name) + n + 1);
     memcpy(e->e_name, path, n);
     e->e_name[n] = 0;
     e->e_flags = 0;
@@ -145,59 +132,60 @@ static struct elt *splitpath(const char *path, struct elt *tail)
 
 /* --- @pop@ --- *
  *
- * Arguments:  ---
+ * Arguments:  @struct state *state@ = working state
  *
  * Returns:    ---
  *
  * Use:                Removes the top item from the directory stack.
  */
 
-static void pop(void)
+static void pop(struct state *state)
 {
+  struct elt *sp = state->sp, *e;
+
   if (sp->e_link) {
-    struct elt *e = sp->e_link;
-    d.len = sp->e_offset;
-    DPUTZ(&d);
-    sp = e;
+    e = sp->e_link;
+    state->path.len = sp->e_offset;
+    DPUTZ(&state->path);
+    state->sp = e;
   }
 }
 
 /* --- @popall@ --- *
  *
- * Arguments:  ---
+ * Arguments:  @struct state *state@ = working state
  *
  * Returns:    ---
  *
  * Use:                Removes all the items from the directory stack.
  */
 
-static void popall(void)
-{
-  while (sp->e_link)
-    pop();
-}
+static void popall(struct state *state)
+  { state->sp = (/*unconst*/ struct elt *)&rootnode; state->path.len = 0; }
 
 /* --- @push@ --- *
  *
- * Arguments:  @struct elt *e@ = pointer to directory element
+ * Arguments:  @struct state *state@ = working state
+ *             @struct elt *e@ = pointer to directory element
  *
  * Returns:    ---
  *
  * Use:                Pushes a new subdirectory onto the stack.
  */
 
-static void push(struct elt *e)
+static void push(struct state *state, struct elt *e)
 {
-  e->e_link = sp;
-  e->e_offset = d.len;
-  DPUTC(&d, '/');
-  DPUTS(&d, e->e_name);
-  sp = e;
+  e->e_link = state->sp;
+  e->e_offset = state->path.len;
+  DPUTC(&state->path, '/');
+  DPUTS(&state->path, e->e_name);
+  state->sp = e;
 }
 
 /* --- @report@ --- *
  *
- * Arguments:  @const struct checkpath *cp@ = pointer to context
+ * Arguments:  @struct state *state@ = pointer to state
+ *             @const struct checkpath *cp@ = pointer to query
  *             @unsigned what@ = what sort of report is this?
  *             @int verbose@ = how verbose is this?
  *             @const char *p@ = what path does it refer to?
@@ -208,116 +196,130 @@ static void push(struct elt *e)
  * Use:                Formats and presents messages to the client.
  */
 
-static void report(const struct checkpath *cp, unsigned what, int verbose,
+static void report(struct state *state, const struct checkpath *cp,
+                  unsigned what, int verbose,
                   const char *p, const char *msg, ...)
 {
+  va_list ap;
+  const char *q = msg;
+  const char *s;
+  size_t n;
+  int e = errno;
+  uid_t u;
+  struct passwd *pw;
+  gid_t g;
+  struct group *gr;
+
   /* --- Decide whether to bin this message --- */
 
   if (!cp->cp_report || verbose > cp->cp_verbose || !(cp->cp_what & what))
     return;
 
+  /* --- If no reporting, do the easy thing --- */
+
+  if (!(cp->cp_what & CP_REPORT)) {
+    cp->cp_report(what, verbose, p, 0, cp->cp_arg);
+    return;
+  }
+
   /* --- Format the message nicely --- */
 
-  if (cp->cp_what & CP_REPORT) {
-    dstr d = DSTR_INIT;
-    va_list ap;
-    const char *q = msg;
-    size_t n;
-    int e = errno;
-
-    va_start(ap, msg);
-    if (verbose > 1)
-      dstr_puts(&d, "[ ");
-    if (p)
-      dstr_putf(&d, "Path: %s: ", p);
-    while (*q) {
-      if (*q == '%') {
-       q++;
-       switch (*q) {
-         case 'e':
-           dstr_puts(&d, strerror(e));
-           break;
-         case 'u': {
-           uid_t u = (uid_t)va_arg(ap, int);
-           struct passwd *pw = getpwuid(u);
-           if (pw)
-             dstr_putf(&d, "`%s'", pw->pw_name);
-           else
-             dstr_putf(&d, "%i", (int)u);
-         } break;
-         case 'g': {
-           gid_t g = (gid_t)va_arg(ap, int);
-           struct group *gr = getgrgid(g);
-           if (gr)
-             dstr_putf(&d, "`%s'", gr->gr_name);
-           else
-             dstr_putf(&d, "%i", (int)g);
-         } break;
-         case 's': {
-           const char *s = va_arg(ap, const char *);
-           dstr_puts(&d, s);
-         } break;
-         case '%':
-           dstr_putc(&d, '%');
-           break;
-         default:
-           dstr_putc(&d, '%');
-           dstr_putc(&d, *q);
-           break;
-       }
-       q++;
-      } else {
-       n = strcspn(q, "%");
-       DPUTM(&d, q, n);
-       q += n;
+  dstr_reset(&state->msg);
+  va_start(ap, msg);
+  if (verbose > 1)
+    dstr_puts(&state->msg, "[ ");
+  if (p)
+    dstr_putf(&state->msg, "Path: %s: ", p);
+  while (*q) {
+    if (*q == '%') {
+      q++;
+      switch (*q) {
+       case 'e':
+         dstr_puts(&state->msg, strerror(e));
+         break;
+       case 'u':
+         u = (uid_t)va_arg(ap, long);
+         if ((pw = getpwuid(u)) != 0)
+           dstr_putf(&state->msg, "`%s'", pw->pw_name);
+         else
+           dstr_putf(&state->msg, "#%ld", (long)u);
+         break;
+       case 'g':
+         g = (gid_t)va_arg(ap, long);
+         if ((gr = getgrgid(g)) != 0)
+           dstr_putf(&state->msg, "`%s'", gr->gr_name);
+         else
+           dstr_putf(&state->msg, "#%ld", (long)g);
+         break;
+       case 's':
+         s = va_arg(ap, const char *);
+         dstr_puts(&state->msg, s);
+         break;
+       case '%':
+         dstr_putc(&state->msg, '%');
+         break;
+       default:
+         dstr_putc(&state->msg, '%');
+         dstr_putc(&state->msg, *q);
+         break;
       }
+      q++;
+    } else {
+      n = strcspn(q, "%");
+      DPUTM(&state->msg, q, n);
+      q += n;
     }
-    if (verbose > 1)
-      dstr_puts(&d, " ]");
-    DPUTZ(&d);
-    cp->cp_report(what, verbose, p, d.buf, cp->cp_arg);
-    dstr_destroy(&d);
-    va_end(ap);
-  } else
-    cp->cp_report(what, verbose, p, 0, cp->cp_arg);
+  }
+  if (verbose > 1)
+    dstr_puts(&state->msg, " ]");
+  DPUTZ(&state->msg);
+  cp->cp_report(what, verbose, p, state->msg.buf, cp->cp_arg);
+  va_end(ap);
 }
 
 /* --- @sanity@ --- *
  *
  * Arguments:  @const char *p@ = name of directory to check
  *             @struct stat *st@ = pointer to @stat@(2) block for it
- *             @const struct checkpath *cp@ = pointer to caller parameters
- *             @unsigned f@ = various flags
+ *             @struct state *state@ = pointer to state
+ *             @const struct checkpath *cp@ = pointer to query
+ *             @unsigned f@ = various flags (@SF_...@)
  *
  * Returns:    Zero if everything's OK, else bitmask of problems.
  *
  * Use:                Performs the main load of sanity-checking on a directory.
+ *             If @SF_LAST@ is not set then sticky directories are always
+ *             acceptable.
  */
 
+#define SF_LAST 1u                     /* This is the final item to check */
+
 static unsigned sanity(const char *p, struct stat *st,
-                      const struct checkpath *cp, unsigned f)
+                      struct state *state, const struct checkpath *cp,
+                      unsigned f)
 {
   unsigned bad = 0;
   int stickyok = 0;
+  int i;
+  unsigned b;
 
   if (S_ISDIR(st->st_mode) &&
-      (!(f & f_last) || (cp->cp_what & CP_STICKYOK)))
-    stickyok = 01000;
+      (!(f & SF_LAST) || (cp->cp_what & CP_STICKYOK)))
+    stickyok = S_ISVTX;
 
   /* --- Check for world-writability --- */
 
   if ((cp->cp_what & CP_WRWORLD) &&
-      (st->st_mode & (0002 | stickyok)) == 0002) {
+      (st->st_mode & (S_IWOTH | stickyok)) == S_IWOTH) {
     bad |= CP_WRWORLD;
-    report(cp, CP_WRWORLD, 1, p, "** world writable **");
+    report(state, cp, CP_WRWORLD, 1, p, "** world writable **");
   }
 
   /* --- Check for group-writability --- */
 
   if ((cp->cp_what & (CP_WRGRP | CP_WROTHGRP)) &&
-      (st->st_mode & (0020 | stickyok)) == 0020) {
-    int i;
-    unsigned b = CP_WRGRP;
+      (st->st_mode & (S_IWGRP | stickyok)) == S_IWGRP) {
+    b = CP_WRGRP;
 
     if (cp->cp_what & CP_WROTHGRP) {
       b = CP_WROTHGRP;
@@ -328,8 +330,8 @@ static unsigned sanity(const char *p, struct stat *st,
     }
     if (b) {
       bad |= b;
-      report(cp, b, 1, p, "writable by %sgroup %g",
-            (b == CP_WROTHGRP) ? "other " : "", st->st_gid);
+      report(state, cp, b, 1, p, "writable by %sgroup %g",
+            (b == CP_WROTHGRP) ? "other " : "", (long)st->st_gid);
     }
   }
 
@@ -339,7 +341,8 @@ static unsigned sanity(const char *p, struct stat *st,
       st->st_uid != cp->cp_uid &&
       st->st_uid != 0) {
     bad |= CP_WROTHUSR;
-    report(cp, CP_WROTHUSR, 1, p, "owner is user %u", st->st_uid);
+    report(state, cp, CP_WROTHUSR, 1, p,
+          "owner is user %u", (long)st->st_uid);
   }
 
   /* --- Done sanity check --- */
@@ -362,36 +365,44 @@ unsigned checkpath(const char *p, const struct checkpath *cp)
 {
   char cwd[PATH_MAX];
   struct elt *e, *ee;
+  struct state *state;
+  pool *pp;
   struct stat st;
   unsigned bad = 0;
+  int i;
 
-  /* --- Initialize stack pointer and path string --- */
+  /* --- Initialize the state --- */
 
-  sp = &rootnode;
-  dstr_destroy(&d);
+  pp = pool_create(arena_global);
+  state = pool_alloc(pp, sizeof(*state));
+  state->p = pp;
+  state->sp = (/*unconst*/ struct elt *)&rootnode;
+  dstr_create(&state->path);
+  dstr_create(&state->link);
+  dstr_create(&state->msg);
 
   /* --- Try to find the current directory --- */
 
   if (!getcwd(cwd, sizeof(cwd))) {
-    report(cp, CP_ERROR, 0, 0, "can't find current directory: %e");
+    report(state, cp, CP_ERROR, 0, 0, "can't find current directory: %e");
     return (CP_ERROR);
   }
 
   /* --- Check that the root directory is OK --- */
 
   if (stat("/", &st)) {
-    report(cp, CP_ERROR, 0, 0, "can't stat root directory: %e");
+    report(state, cp, CP_ERROR, 0, 0, "can't stat root directory: %e");
     return (CP_ERROR);
   }
 
-  report(cp, CP_REPORT, 3, p, "begin scan");
-  bad |= sanity("/", &st, cp, 0);
+  report(state, cp, CP_REPORT, 3, p, "begin scan");
+  bad |= sanity("/", &st, state, cp, 0);
 
   /* --- Get the initial list of things to process --- */
 
-  ee = splitpath(p, 0);
+  ee = splitpath(state, p, 0);
   if (*p != '/')
-    ee = splitpath(cwd, ee);
+    ee = splitpath(state, cwd, ee);
 
   /* --- While there are list items which still need doing --- */
 
@@ -401,7 +412,6 @@ unsigned checkpath(const char *p, const struct checkpath *cp)
     /* --- Strip off simple `.' elements --- */
 
     if (strcmp(ee->e_name, ".") == 0) {
-      free(ee);
       ee = e;
       continue;
     }
@@ -409,21 +419,20 @@ unsigned checkpath(const char *p, const struct checkpath *cp)
     /* --- Backtrack on `..' elements --- */
 
     else if (strcmp(ee->e_name, "..") == 0) {
-      pop();
-      free(ee);
+      pop(state);
       ee = e;
       continue;
     }
 
     /* --- Everything else gets pushed on the end --- */
 
-    push(ee);
+    push(state, ee);
     ee = e;
 
     /* --- Find out what sort of a thing this is --- */
 
-    if (lstat(d.buf, &st)) {
-      report(cp, CP_ERROR, 0, d.buf, "can't stat: %e");
+    if (lstat(state->path.buf, &st)) {
+      report(state, cp, CP_ERROR, 0, state->path.buf, "can't stat: %e");
       bad |= CP_ERROR;
       break;
     }
@@ -431,19 +440,21 @@ unsigned checkpath(const char *p, const struct checkpath *cp)
     /* --- Handle symbolic links specially --- */
 
     if (S_ISLNK(st.st_mode)) {
-      dstr buf = DSTR_INIT;
-      int i;
 
       /* --- Resolve the link --- */
 
-      dstr_ensure(&buf, st.st_size + 1);
-      if ((i = readlink(d.buf, buf.buf, buf.sz)) < 0) {
-       report(cp, CP_ERROR, 0, d.buf, "can't readlink: %e");
+      dstr_reset(&state->link);
+      dstr_ensure(&state->link, st.st_size + 1);
+      if ((i = readlink(state->path.buf,
+                       state->link.buf, state->link.sz)) < 0) {
+       report(state, cp, CP_ERROR, 0, state->path.buf,
+              "can't readlink: %e");
        bad |= CP_ERROR;
        break;
       }
-      buf.buf[i] = 0;
-      report(cp, CP_SYMLINK, 2, d.buf, "symlink -> `%s'", buf.buf);
+      state->link.buf[i] = 0;
+      report(state, cp, CP_SYMLINK, 2, state->path.buf,
+            "symlink -> `%s'", state->link.buf);
 
       /* --- Handle sticky parents --- *
        *
@@ -453,32 +464,31 @@ unsigned checkpath(const char *p, const struct checkpath *cp)
        */
 
       if ((cp->cp_what & CP_WROTHUSR) &&
-         (sp->e_link->e_flags & f_sticky) &&
+         (state->sp->e_link->e_flags & EF_STICKY) &&
          st.st_uid != cp->cp_uid && st.st_uid != 0) {
        bad |= CP_WROTHUSR;
-       report(cp, CP_WROTHUSR, 1, d.buf,
-              "symlink modifiable by user %u", st.st_uid);
+       report(state, cp, CP_WROTHUSR, 1, state->path.buf,
+              "symlink modifiable by user %u", (long)st.st_uid);
       }
 
       /* --- Sort out what to do from here --- */
 
-      if (buf.buf[0] == '/')
-       popall();
+      if (state->link.buf[0] == '/')
+       popall(state);
       else
-       pop();
-      ee = splitpath(buf.buf, ee);
-      dstr_destroy(&buf);
+       pop(state);
+      ee = splitpath(state, state->link.buf, ee);
       continue;
     }
 
     /* --- Run the sanity check on this path element --- */
 
-    bad |= sanity(d.buf, &st, cp, ee ? 0 : f_last);
+    bad |= sanity(state->path.buf, &st, state, cp, ee ? 0 : SF_LAST);
 
     if (S_ISDIR(st.st_mode)) {
-      if (st.st_mode & 01000)
-       sp->e_flags |= f_sticky;
-      report(cp, CP_REPORT, 4, d.buf, "directory");
+      if (st.st_mode & S_ISVTX)
+       state->sp->e_flags |= EF_STICKY;
+      report(state, cp, CP_REPORT, 4, state->path.buf, "directory");
       continue;
     }
 
@@ -491,42 +501,109 @@ unsigned checkpath(const char *p, const struct checkpath *cp)
 
   if (ee) {
     if (!(bad & CP_ERROR))
-      report(cp, CP_ERROR, 0, 0, "junk left over after reaching leaf");
+      report(state, cp, CP_ERROR, 0, 0, "junk left over after reaching leaf");
     while (ee) {
       e = ee->e_link;
-      free(ee);
       ee = e;
     }
   }
 
-  popall();
+  dstr_destroy(&state->path);
+  dstr_destroy(&state->link);
+  dstr_destroy(&state->msg);
+  pool_destroy(state->p);
   return (bad);
 }
 
-/* --- @checkpath_setids@ --- *
+/* --- @checkpath_addgid@ --- *
+ *
+ * Arguments:  @struct checkpath *cp@ = pointer to block to fill in
+ *             @gid_t g@ = group id to add
+ *
+ * Returns:    Zero if successful, nonzero if the array is full.
+ *
+ * Use:                Adds the group @g@ to the structure.
+ */
+
+int checkpath_addgid(struct checkpath *cp, gid_t g)
+{
+  int i;
+
+  for (i = 0; i < cp->cp_gids; i++) {
+    if (cp->cp_gid[i] == g)
+      return (0);
+  }
+  if (cp->cp_gids >= N(cp->cp_gid))
+    return (-1);
+  cp->cp_gid[cp->cp_gids++] = g;
+  return (0);
+}
+
+/* --- @checkpath_setuid@ --- *
  *
  * Arguments:  @struct checkpath *cp@ = pointer to block to fill in
  *
  * Returns:    ---
  *
- * Use:                Fills in the user ids and things in the structure.
+ * Use:                Fills in the @cp_uid@ slot of the structure with the real uid
+ *             of the current process.
  */
 
-void checkpath_setids(struct checkpath *cp)
+void checkpath_setuid(struct checkpath *cp) { cp->cp_uid = getuid(); }
+
+/* --- @checkpath_setgid@ --- *
+ *
+ * Arguments:  @struct checkpath *cp@ = pointer to block to fill in
+ *
+ * Returns:    Zero if successful, nonzero if the array is full.
+ *
+ * Use:                Adds the real gid of the current process to the @cp_gid@
+ *             array.
+ */
+
+int checkpath_setgid(struct checkpath *cp)
+  { return (checkpath_addgid(cp, getgid())); }
+
+/* --- @checkpath_setgroups@ --- *
+ *
+ * Arguments:  @struct checkpath *cp@ = pointer to block to fill in
+ *
+ * Returns:    Zero if successful, nonzero if the array is full.
+ *
+ * Use:                Adds the current process's supplementary groups to the
+ *             @cp_gid@ table.
+ */
+
+int checkpath_setgroups(struct checkpath *cp)
 {
-  int n, i;
-  gid_t g = getgid();
+  int i, n;
+  gid_t gg[NGROUPS_MAX];
 
-  cp->cp_uid = getuid();
-  n = getgroups(sizeof(cp->cp_gid) / sizeof(cp->cp_gid[0]), cp->cp_gid);
-  
+  n = getgroups(N(gg), gg);
   for (i = 0; i < n; i++) {
-    if (cp->cp_gid[i] == g)
-      goto gid_ok;
+    if (checkpath_addgid(cp, gg[i]))
+      return (-1);
   }
-  cp->cp_gid[n++] = g;
-gid_ok:
-  cp->cp_gids = n;
+  return (0);
+}
+
+/* --- @checkpath_setids@ --- *
+ *
+ * Arguments:  @struct checkpath *cp@ = pointer to block to fill in
+ *
+ * Returns:    ---
+ *
+ * Use:                Fills in the user ids and things in the structure.  This is
+ *             equivalent to setting @cp_gids = 0@ and then calling
+ *             @_setuid@, @_setgid@ and @_setgroups@.  It can't fail.
+ */
+
+void checkpath_setids(struct checkpath *cp)
+{
+  cp->cp_gids = 0;
+  checkpath_setuid(cp);
+  checkpath_setgid(cp);
+  checkpath_setgroups(cp);
 }
 
 /*----- That's all, folks -------------------------------------------------*/