]> git.decadent.org.uk Git - nfs-utils.git/blobdiff - support/nsm/file.c
statd: Decouple statd's state directory from the NFS state directory
[nfs-utils.git] / support / nsm / file.c
index 10769d9df9e87fdc4ff7302606d7fa59ea818e88..5dd52c1e2640fb2effd07004c8902700811aa775 100644 (file)
 #endif
 
 #include <sys/types.h>
+#ifdef HAVE_SYS_CAPABILITY_H
+#include <sys/capability.h>
+#endif
+#include <sys/prctl.h>
 #include <sys/stat.h>
 
 #include <ctype.h>
 
 #define NSM_KERNEL_STATE_FILE  "/proc/sys/fs/nfs/nsm_local_state"
 
-/*
- * Some distributions place statd's files in a subdirectory
- */
-#define NSM_PATH_EXTENSION
-/* #define NSM_PATH_EXTENSION  "/statd" */
-
-#define NSM_DEFAULT_STATEDIR           NFS_STATEDIR NSM_PATH_EXTENSION
-
 static char nsm_base_dirname[PATH_MAX] = NSM_DEFAULT_STATEDIR;
 
 #define NSM_MONITOR_DIR        "sm"
@@ -122,7 +118,7 @@ exact_error_check(const ssize_t len, const size_t buflen)
  * containing an appropriate pathname, or NULL if an error
  * occurs.  Caller must free the returned result with free(3).
  */
-__attribute_malloc__
+__attribute__((__malloc__))
 static char *
 nsm_make_record_pathname(const char *directory, const char *hostname)
 {
@@ -170,7 +166,7 @@ nsm_make_record_pathname(const char *directory, const char *hostname)
  * containing an appropriate pathname, or NULL if an error
  * occurs.  Caller must free the returned result with free(3).
  */
-__attribute_malloc__
+__attribute__((__malloc__))
 static char *
 nsm_make_pathname(const char *directory)
 {
@@ -200,7 +196,7 @@ nsm_make_pathname(const char *directory)
  * containing an appropriate pathname, or NULL if an error
  * occurs.  Caller must free the returned result with free(3).
  */
-__attribute_malloc__
+__attribute__((__malloc__))
 static char *
 nsm_make_temp_pathname(const char *pathname)
 {
@@ -335,6 +331,36 @@ nsm_is_default_parentdir(void)
        return strcmp(nsm_base_dirname, NSM_DEFAULT_STATEDIR) == 0;
 }
 
+/*
+ * Clear all capabilities but CAP_NET_BIND_SERVICE.  This permits
+ * callers to acquire privileged source ports, but all other root
+ * capabilities are disallowed.
+ *
+ * Returns true if successful, or false if some error occurred.
+ */
+static _Bool
+nsm_clear_capabilities(void)
+{
+#ifdef HAVE_SYS_CAPABILITY_H
+       cap_t caps;
+
+       caps = cap_from_text("cap_net_bind_service=ep");
+       if (caps == NULL) {
+               xlog(L_ERROR, "Failed to allocate capability: %m");
+               return false;
+       }
+
+       if (cap_set_proc(caps) == -1) {
+               xlog(L_ERROR, "Failed to set capability flags: %m");
+               (void)cap_free(caps);
+               return false;
+       }
+
+       (void)cap_free(caps);
+#endif
+       return true;
+}
+
 /**
  * nsm_drop_privileges - drop root privileges
  * @pidfd: file descriptor of a pid file
@@ -361,18 +387,18 @@ nsm_drop_privileges(const int pidfd)
                return false;
        }
 
-       if (st.st_uid == 0) {
-               xlog_warn("Running as root.  "
-                       "chown %s to choose different user", nsm_base_dirname);
-               return true;
-       }
-
        if (chdir(nsm_base_dirname) == -1) {
                xlog(L_ERROR, "Failed to change working directory to %s: %m",
                                nsm_base_dirname);
                return false;
        }
 
+       if (st.st_uid == 0) {
+               xlog_warn("Running as root.  "
+                       "chown %s to choose different user", nsm_base_dirname);
+               return true;
+       }
+
        /*
         * If the pidfile happens to reside on NFS, dropping privileges
         * will probably cause us to lose access, even though we are
@@ -382,6 +408,14 @@ nsm_drop_privileges(const int pidfd)
                if (fchown(pidfd, st.st_uid, st.st_gid) == -1)
                        xlog_warn("Failed to change owner of pidfile: %m");
 
+       /*
+        * Don't clear capabilities when dropping root.
+        */
+        if (prctl(PR_SET_KEEPCAPS, 1, 0, 0, 0) == -1) {
+                xlog(L_ERROR, "prctl(PR_SET_KEEPCAPS) failed: %m");
+               return false;
+       }
+
        if (setgroups(0, NULL) == -1) {
                xlog(L_ERROR, "Failed to drop supplementary groups: %m");
                return false;
@@ -399,7 +433,8 @@ nsm_drop_privileges(const int pidfd)
        }
 
        xlog(D_CALL, "Effective UID, GID: %u, %u", st.st_uid, st.st_gid);
-       return true;
+
+       return nsm_clear_capabilities();
 }
 
 /**
@@ -525,9 +560,8 @@ nsm_retire_monitored_hosts(void)
 
        while ((de = readdir(dir)) != NULL) {
                char *src, *dst;
+               struct stat stb;
 
-               if (de->d_type != (unsigned char)DT_REG)
-                       continue;
                if (de->d_name[0] == '.')
                        continue;
 
@@ -537,6 +571,20 @@ nsm_retire_monitored_hosts(void)
                        continue;
                }
 
+               /* NB: not all file systems fill in d_type correctly */
+               if (lstat(src, &stb) == -1) {
+                       xlog_warn("Bad monitor file %s, skipping: %m",
+                                       de->d_name);
+                       free(src);
+                       continue;
+               }
+               if (!S_ISREG(stb.st_mode)) {
+                       xlog(D_GENERAL, "Skipping non-regular file %s",
+                                       de->d_name);
+                       free(src);
+                       continue;
+               }
+
                dst = nsm_make_record_pathname(NSM_NOTIFY_DIR, de->d_name);
                if (dst == NULL) {
                        free(src);
@@ -591,7 +639,7 @@ nsm_priv_to_hex(const char *priv, char *buf, const size_t buflen)
 /*
  * Returns the length in bytes of the created record.
  */
-__attribute_noinline__
+__attribute__((__noinline__))
 static size_t
 nsm_create_monitor_record(char *buf, const size_t buflen,
                const struct sockaddr *sap, const struct mon *m)
@@ -625,6 +673,56 @@ nsm_create_monitor_record(char *buf, const size_t buflen,
        return buflen - remaining;
 }
 
+static _Bool
+nsm_append_monitored_host(const char *path, const char *line)
+{
+       _Bool result = false;
+       char *buf = NULL;
+       struct stat stb;
+       size_t buflen;
+       ssize_t len;
+       int fd;
+
+       if (stat(path, &stb) == -1) {
+               xlog(L_ERROR, "Failed to insert: "
+                       "could not stat original file %s: %m", path);
+               goto out;
+       }
+       buflen = (size_t)stb.st_size + strlen(line);
+
+       buf = malloc(buflen + 1);
+       if (buf == NULL) {
+               xlog(L_ERROR, "Failed to insert: no memory");
+               goto out;
+       }
+       memset(buf, 0, buflen + 1);
+
+       fd = open(path, O_RDONLY);
+       if (fd == -1) {
+               xlog(L_ERROR, "Failed to insert: "
+                       "could not open original file %s: %m", path);
+               goto out;
+       }
+
+       len = read(fd, buf, (size_t)stb.st_size);
+       if (exact_error_check(len, (size_t)stb.st_size)) {
+               xlog(L_ERROR, "Failed to insert: "
+                       "could not read original file %s: %m", path);
+               (void)close(fd);
+               goto out;
+       }
+       (void)close(fd);
+
+       strcat(buf, line);
+
+       if (nsm_atomic_write(path, buf, buflen))
+               result = true;
+
+out:
+       free(buf);
+       return result;
+}
+
 /**
  * nsm_insert_monitored_host - write callback data for one host to disk
  * @hostname: C string containing a hostname
@@ -657,9 +755,18 @@ nsm_insert_monitored_host(const char *hostname, const struct sockaddr *sap,
                goto out;
        }
 
-       fd = open(path, O_WRONLY | O_CREAT | O_SYNC, S_IRUSR | S_IWUSR);
+       /*
+        * If exclusive create fails, we're adding a new line to an
+        * existing file.
+        */
+       fd = open(path, O_WRONLY | O_CREAT | O_EXCL | O_SYNC, S_IRUSR | S_IWUSR);
        if (fd == -1) {
-               xlog(L_ERROR, "Failed to insert: creating %s: %m", path);
+               if (errno != EEXIST) {
+                       xlog(L_ERROR, "Failed to insert: creating %s: %m", path);
+                       goto out;
+               }
+
+               result = nsm_append_monitored_host(path, buf);
                goto out;
        }
        result = true;
@@ -682,7 +789,7 @@ out:
        return result;
 }
 
-__attribute_noinline__
+__attribute__((__noinline__))
 static _Bool
 nsm_parse_line(char *line, struct sockaddr_in *sin, struct mon *m)
 {
@@ -744,7 +851,7 @@ nsm_read_line(const char *hostname, const time_t timestamp, char *line,
 }
 
 /*
- * Given a filename, reads data from a file under NSM_MONITOR_DIR
+ * Given a filename, reads data from a file under "directory"
  * and invokes @func so caller can populate their in-core
  * database with this data.
  */
@@ -761,10 +868,15 @@ nsm_load_host(const char *directory, const char *filename, nsm_populate_t func)
        if (path == NULL)
                goto out_err;
 
-       if (stat(path, &stb) == -1) {
+       if (lstat(path, &stb) == -1) {
                xlog(L_ERROR, "Failed to stat %s: %m", path);
                goto out_freepath;
        }
+       if (!S_ISREG(stb.st_mode)) {
+               xlog(D_GENERAL, "Skipping non-regular file %s",
+                               path);
+               goto out_freepath;
+       }
 
        f = fopen(path, "r");
        if (f == NULL) {
@@ -811,8 +923,6 @@ nsm_load_dir(const char *directory, nsm_populate_t func)
        }
 
        while ((de = readdir(dir)) != NULL) {
-               if (de->d_type != (unsigned char)DT_REG)
-                       continue;
                if (de->d_name[0] == '.')
                        continue;
 
@@ -848,9 +958,15 @@ nsm_load_notify_list(nsm_populate_t func)
 }
 
 static void
-nsm_delete_host(const char *directory, const char *hostname)
+nsm_delete_host(const char *directory, const char *hostname,
+               const char *mon_name, const char *my_name)
 {
-       char *path;
+       char line[LINELEN + 1 + SM_MAXSTRLEN + 2];
+       char *outbuf = NULL;
+       struct stat stb;
+       char *path, *next;
+       size_t remaining;
+       FILE *f;
 
        path = nsm_make_record_pathname(directory, hostname);
        if (path == NULL) {
@@ -858,30 +974,106 @@ nsm_delete_host(const char *directory, const char *hostname)
                return;
        }
 
-       if (unlink(path) == -1)
-               xlog(L_ERROR, "Failed to unlink %s: %m", path);
+       if (stat(path, &stb) == -1) {
+               xlog(L_ERROR, "Failed to delete: "
+                       "could not stat original file %s: %m", path);
+               goto out;
+       }
+       remaining = (size_t)stb.st_size + 1;
+
+       outbuf = malloc(remaining);
+       if (outbuf == NULL) {
+               xlog(L_ERROR, "Failed to delete: no memory");
+               goto out;
+       }
+
+       f = fopen(path, "r");
+       if (f == NULL) {
+               xlog(L_ERROR, "Failed to delete: "
+                       "could not open original file %s: %m", path);
+               goto out;
+       }
+
+       /*
+        * Walk the records in the file, and copy the non-matching
+        * ones to our output buffer.
+        */
+       next = outbuf;
+       while (fgets(line, (int)sizeof(line), f) != NULL) {
+               struct sockaddr_in sin;
+               struct mon m;
+               size_t len;
+
+               if (!nsm_parse_line(line, &sin, &m)) {
+                       xlog(L_ERROR, "Failed to delete: "
+                               "could not parse original file %s", path);
+                       (void)fclose(f);
+                       goto out;
+               }
+
+               if (strcmp(mon_name, m.mon_id.mon_name) == 0 &&
+                        strcmp(my_name, m.mon_id.my_id.my_name) == 0)
+                       continue;
+
+               /* nsm_parse_line destroys the contents of line[], so
+                * reconstruct the copy in our output buffer. */
+               len = nsm_create_monitor_record(next, remaining,
+                                       (struct sockaddr *)(char *)&sin, &m);
+               if (len == 0) {
+                       xlog(L_ERROR, "Failed to delete: "
+                               "could not construct output record");
+                       (void)fclose(f);
+                       goto out;
+               }
+               next += len;
+               remaining -= len;
+       }
+
+       (void)fclose(f);
 
+       /*
+        * If nothing was copied when we're done, then unlink the file.
+        * Otherwise, atomically update the contents of the file.
+        */
+       if (next != outbuf) {
+               if (!nsm_atomic_write(path, outbuf, strlen(outbuf)))
+                       xlog(L_ERROR, "Failed to delete: "
+                               "could not write new file %s: %m", path);
+       } else {
+               if (unlink(path) == -1)
+                       xlog(L_ERROR, "Failed to delete: "
+                               "could not unlink file %s: %m", path);
+       }
+
+out:
+       free(outbuf);
        free(path);
 }
 
 /**
  * nsm_delete_monitored_host - delete on-disk record for monitored host
  * @hostname: '\0'-terminated C string containing hostname of record to delete
+ * @mon_name: '\0'-terminated C string containing monname of record to delete
+ * @my_name: '\0'-terminated C string containing myname of record to delete
  *
  */
 void
-nsm_delete_monitored_host(const char *hostname)
+nsm_delete_monitored_host(const char *hostname, const char *mon_name,
+               const char *my_name)
 {
-       nsm_delete_host(NSM_MONITOR_DIR, hostname);
+       nsm_delete_host(NSM_MONITOR_DIR, hostname, mon_name, my_name);
 }
 
 /**
  * nsm_delete_notified_host - delete on-disk host record after notification
  * @hostname: '\0'-terminated C string containing hostname of record to delete
+ * @mon_name: '\0'-terminated C string containing monname of record to delete
+ * @my_name: '\0'-terminated C string containing myname of record to delete
  *
  */
 void
-nsm_delete_notified_host(const char *hostname)
+nsm_delete_notified_host(const char *hostname, const char *mon_name,
+               const char *my_name)
 {
-       nsm_delete_host(NSM_NOTIFY_DIR, hostname);
+       nsm_delete_host(NSM_NOTIFY_DIR, hostname, mon_name, my_name);
 }