X-Git-Url: https://git.decadent.org.uk/gitweb/?p=nfs-utils.git;a=blobdiff_plain;f=support%2Fnsm%2Ffile.c;h=5dd52c1e2640fb2effd07004c8902700811aa775;hp=fc3241a1a4d9a10d128d22f3d352dcbc2445863d;hb=5c3e26650a54af7826a2b4c6759b56681a350c37;hpb=a8449be2ab58d67d5028a280f94e661358b58e97 diff --git a/support/nsm/file.c b/support/nsm/file.c index fc3241a..5dd52c1 100644 --- a/support/nsm/file.c +++ b/support/nsm/file.c @@ -67,6 +67,10 @@ #endif #include +#ifdef HAVE_SYS_CAPABILITY_H +#include +#endif +#include #include #include @@ -90,14 +94,6 @@ #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) { @@ -195,6 +191,94 @@ nsm_make_pathname(const char *directory) return path; } +/* + * Returns a dynamically allocated, '\0'-terminated buffer + * containing an appropriate pathname, or NULL if an error + * occurs. Caller must free the returned result with free(3). + */ +__attribute__((__malloc__)) +static char * +nsm_make_temp_pathname(const char *pathname) +{ + size_t size; + char *path; + int len; + + size = strlen(pathname) + sizeof(".new") + 2; + if (size > PATH_MAX) + return NULL; + + path = malloc(size); + if (path == NULL) + return NULL; + + len = snprintf(path, size, "%s.new", pathname); + if (error_check(len, size)) { + free(path); + return NULL; + } + + return path; +} + +/* + * Use "mktemp, write, rename" to update the contents of a file atomically. + * + * Returns true if completely successful, or false if some error occurred. + */ +static _Bool +nsm_atomic_write(const char *path, const void *buf, const size_t buflen) +{ + _Bool result = false; + ssize_t len; + char *temp; + int fd; + + temp = nsm_make_temp_pathname(path); + if (temp == NULL) { + xlog(L_ERROR, "Failed to create new path for %s", path); + goto out; + } + + fd = open(temp, O_CREAT | O_TRUNC | O_SYNC | O_WRONLY, 0644); + if (fd == -1) { + xlog(L_ERROR, "Failed to create %s: %m", temp); + goto out; + } + + len = write(fd, buf, buflen); + if (exact_error_check(len, buflen)) { + xlog(L_ERROR, "Failed to write %s: %m", temp); + (void)close(fd); + (void)unlink(temp); + goto out; + } + + if (close(fd) == -1) { + xlog(L_ERROR, "Failed to close %s: %m", temp); + (void)unlink(temp); + goto out; + } + + if (rename(temp, path) == -1) { + xlog(L_ERROR, "Failed to rename %s -> %s: %m", + temp, path); + (void)unlink(temp); + goto out; + } + + /* Ostensibly, a sync(2) is not needed here because + * open(O_CREAT), write(O_SYNC), and rename(2) are + * already synchronous with persistent storage, for + * any file system we care about. */ + + result = true; + +out: + free(temp); + return result; +} + /** * nsm_setup_pathnames - set up pathname * @progname: C string containing name of program, for error messages @@ -247,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 @@ -273,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 @@ -294,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; @@ -311,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(); } /** @@ -326,7 +449,6 @@ nsm_get_state(_Bool update) int fd, state = 0; ssize_t result; char *path = NULL; - char *newpath = NULL; path = nsm_make_pathname(NSM_STATE_FILE); if (path == NULL) { @@ -365,54 +487,11 @@ update: if (update) { state += 2; - - newpath = nsm_make_pathname(NSM_STATE_FILE ".new"); - if (newpath == NULL) { - xlog(L_ERROR, - "Failed to create path for " NSM_STATE_FILE ".new"); - state = 0; - goto out; - } - - fd = open(newpath, O_CREAT | O_SYNC | O_WRONLY, 0644); - if (fd == -1) { - xlog(L_ERROR, "Failed to create %s: %m", newpath); - state = 0; - goto out; - } - - result = write(fd, &state, sizeof(state)); - if (exact_error_check(result, sizeof(state))) { - xlog(L_ERROR, "Failed to write %s: %m", newpath); - (void)close(fd); - (void)unlink(newpath); - state = 0; - goto out; - } - - if (close(fd) == -1) { - xlog(L_ERROR, "Failed to close %s: %m", newpath); - (void)unlink(newpath); + if (!nsm_atomic_write(path, &state, sizeof(state))) state = 0; - goto out; - } - - if (rename(newpath, path) == -1) { - xlog(L_ERROR, "Failed to rename %s -> %s: %m", - newpath, path); - (void)unlink(newpath); - state = 0; - goto out; - } - - /* Ostensibly, a sync(2) is not needed here because - * open(O_CREAT), write(O_SYNC), and rename(2) are - * already synchronous with persistent storage, for - * any file system we care about. */ } out: - free(newpath); free(path); return state; } @@ -481,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; @@ -493,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); @@ -547,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) @@ -581,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 @@ -613,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; @@ -638,7 +789,7 @@ out: return result; } -__attribute_noinline__ +__attribute__((__noinline__)) static _Bool nsm_parse_line(char *line, struct sockaddr_in *sin, struct mon *m) { @@ -700,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. */ @@ -717,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) { @@ -767,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; @@ -804,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) { @@ -814,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); }