X-Git-Url: https://git.decadent.org.uk/gitweb/?p=nfs-utils.git;a=blobdiff_plain;f=support%2Fnsm%2Ffile.c;fp=support%2Fnsm%2Ffile.c;h=4dfc235dce5bc4f28562145420f236cc07123603;hp=0000000000000000000000000000000000000000;hb=f16fb1cdc99fb1bd62767b133bec6358e7a793e1;hpb=917cd9a5532d90745d94045f49ee49d0a7636d6d diff --git a/support/nsm/file.c b/support/nsm/file.c new file mode 100644 index 0000000..4dfc235 --- /dev/null +++ b/support/nsm/file.c @@ -0,0 +1,819 @@ +/* + * Copyright 2009 Oracle. All rights reserved. + * + * This file is part of nfs-utils. + * + * nfs-utils is free software; you can redistribute it and/or modify + * 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. + * + * nfs-utils 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 nfs-utils. If not, see . + */ + +/* + * NSM for Linux. + * + * Callback information and NSM state is stored in files, usually + * under /var/lib/nfs. A database of information contained in local + * files stores NLM callback data and what remote peers to notify of + * reboots. + * + * For each monitored remote peer, a text file is created under the + * directory specified by NSM_MONITOR_DIR. The name of the file + * is a valid DNS hostname. The hostname string must be a valid + * ASCII DNS name, and must not contain slash characters, white space, + * or '\0' (ie. anything that might have some special meaning in a + * path name). + * + * The contents of each file include seven blank-separated fields of + * text, finished with '\n'. The first field contains the network + * address of the NLM service to call back. The current implementation + * supports using only IPv4 addresses, so the only contents of this + * field are a network order IPv4 address expressed in 8 hexadecimal + * characters. + * + * The next four fields are text strings of hexadecimal characters, + * representing: + * + * 2. A 4 byte RPC program number of the NLM service to call back + * 3. A 4 byte RPC version number of the NLM service to call back + * 4. A 4 byte RPC procedure number of the NLM service to call back + * 5. A 16 byte opaque cookie that the NLM service uses to identify + * the monitored host + * + * The sixth field is the monitored host's mon_name, passed to statd + * via an SM_MON request. + * + * The seventh field is the my_name for this peer, which is the + * hostname of the local NLM (currently on Linux, the result of + * `uname -n`). This can be used as the source address/hostname + * when sending SM_NOTIFY requests. + * + * The NSM protocol does not limit the contents of these strings + * in any way except that they must fit into 1024 bytes. Our + * implementation requires that these strings not contain + * white space or '\0'. + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include + +#include +#include +#include +#ifndef S_SPLINT_S +#include +#endif +#include +#include +#include +#include +#include +#include + +#include "xlog.h" +#include "nsm.h" + +#define RPCARGSLEN (4 * (8 + 1)) +#define LINELEN (RPCARGSLEN + SM_PRIV_SIZE * 2 + 1) + +#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" +#define NSM_NOTIFY_DIR "sm.bak" +#define NSM_STATE_FILE "state" + + +static _Bool +error_check(const int len, const size_t buflen) +{ + return (len < 0) || ((size_t)len >= buflen); +} + +static _Bool +exact_error_check(const ssize_t len, const size_t buflen) +{ + return (len < 0) || ((size_t)len != buflen); +} + +/* + * 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_record_pathname(const char *directory, const char *hostname) +{ + const char *c; + size_t size; + char *path; + int len; + + /* + * Block hostnames that contain characters that have + * meaning to the file system (like '/'), or that can + * be confusing on visual inspection (like ' '). + */ + for (c = hostname; *c != '\0'; c++) + if (*c == '/' || isspace((int)*c) != 0) { + xlog(D_GENERAL, "Hostname contains invalid characters"); + return NULL; + } + + size = strlen(nsm_base_dirname) + strlen(directory) + strlen(hostname) + 3; + if (size > PATH_MAX) { + xlog(D_GENERAL, "Hostname results in pathname that is too long"); + return NULL; + } + + path = malloc(size); + if (path == NULL) { + xlog(D_GENERAL, "Failed to allocate memory for pathname"); + return NULL; + } + + len = snprintf(path, size, "%s/%s/%s", + nsm_base_dirname, directory, hostname); + if (error_check(len, size)) { + xlog(D_GENERAL, "Pathname did not fit in specified buffer"); + free(path); + return NULL; + } + + 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_pathname(const char *directory) +{ + size_t size; + char *path; + int len; + + size = strlen(nsm_base_dirname) + strlen(directory) + 2; + if (size > PATH_MAX) + return NULL; + + path = malloc(size); + if (path == NULL) + return NULL; + + len = snprintf(path, size, "%s/%s", nsm_base_dirname, directory); + if (error_check(len, size)) { + free(path); + return NULL; + } + + return path; +} + +/** + * nsm_setup_pathnames - set up pathname + * @progname: C string containing name of program, for error messages + * @parentdir: C string containing pathname to on-disk state, or NULL + * + * This runs before logging is set up, so error messages are directed + * to stderr. + * + * Returns true and sets up our pathnames, if @parentdir was valid + * and usable; otherwise false is returned. + */ +_Bool +nsm_setup_pathnames(const char *progname, const char *parentdir) +{ + static char buf[PATH_MAX]; + struct stat st; + char *path; + + /* First: test length of name and whether it exists */ + if (lstat(parentdir, &st) == -1) { + (void)fprintf(stderr, "%s: Failed to stat %s: %s", + progname, parentdir, strerror(errno)); + return false; + } + + /* Ensure we have a clean directory pathname */ + strncpy(buf, parentdir, sizeof(buf)); + path = dirname(buf); + if (*path == '.') { + (void)fprintf(stderr, "%s: Unusable directory %s", + progname, parentdir); + return false; + } + + xlog(D_CALL, "Using %s as the state directory", parentdir); + strncpy(nsm_base_dirname, parentdir, sizeof(nsm_base_dirname)); + return true; +} + +/** + * nsm_is_default_parentdir - check if parent directory is default + * + * Returns true if the active statd parent directory, set by + * nsm_change_pathname(), is the same as the built-in default + * parent directory; otherwise false is returned. + */ +_Bool +nsm_is_default_parentdir(void) +{ + return strcmp(nsm_base_dirname, NSM_DEFAULT_STATEDIR) == 0; +} + +/** + * nsm_drop_privileges - drop root privileges + * @pidfd: file descriptor of a pid file + * + * Returns true if successful, or false if some error occurred. + * + * Set our effective UID and GID to that of our on-disk database. + */ +_Bool +nsm_drop_privileges(const int pidfd) +{ + struct stat st; + + (void)umask(S_IRWXO); + + /* + * XXX: If we can't stat dirname, or if dirname is owned by + * root, we should use "statduser" instead, which is set up + * by configure.ac. Nothing in nfs-utils seems to use + * "statduser," though. + */ + if (lstat(nsm_base_dirname, &st) == -1) { + xlog(L_ERROR, "Failed to stat %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 (chdir(nsm_base_dirname) == -1) { + xlog(L_ERROR, "Failed to change working directory to %s: %m", + nsm_base_dirname); + return false; + } + + /* + * If the pidfile happens to reside on NFS, dropping privileges + * will probably cause us to lose access, even though we are + * holding it open. Chown it to prevent this. + */ + if (pidfd >= 0) + if (fchown(pidfd, st.st_uid, st.st_gid) == -1) + xlog_warn("Failed to change owner of pidfile: %m"); + + if (setgroups(0, NULL) == -1) { + xlog(L_ERROR, "Failed to drop supplementary groups: %m"); + return false; + } + + /* + * ORDER + * + * setgid(2) first, as setuid(2) may remove privileges needed + * to set the group id. + */ + if (setgid(st.st_gid) == -1 || setuid(st.st_uid) == -1) { + xlog(L_ERROR, "Failed to drop privileges: %m"); + return false; + } + + xlog(D_CALL, "Effective UID, GID: %u, %u", st.st_uid, st.st_gid); + return true; +} + +/** + * nsm_get_state - retrieve on-disk NSM state number + * + * Returns an odd NSM state number read from disk, or an initial + * state number. Zero is returned if some error occurs. + */ +int +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) { + xlog(L_ERROR, "Failed to allocate path for " NSM_STATE_FILE); + goto out; + } + + fd = open(path, O_RDONLY); + if (fd == -1) { + if (errno != ENOENT) { + xlog(L_ERROR, "Failed to open %s: %m", path); + goto out; + } + + xlog(L_NOTICE, "Initializing NSM state"); + state = 1; + update = true; + goto update; + } + + result = read(fd, &state, sizeof(state)); + if (exact_error_check(result, sizeof(state))) { + xlog_warn("Failed to read %s: %m", path); + + xlog(L_NOTICE, "Initializing NSM state"); + state = 1; + update = true; + goto update; + } + + if ((state & 1) == 0) + state++; + +update: + (void)close(fd); + + 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); + 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; +} + +/** + * nsm_update_kernel_state - attempt to post new NSM state to kernel + * @state: NSM state number + * + */ +void +nsm_update_kernel_state(const int state) +{ + ssize_t result; + char buf[20]; + int fd, len; + + fd = open(NSM_KERNEL_STATE_FILE, O_WRONLY); + if (fd == -1) { + xlog(D_GENERAL, "Failed to open " NSM_KERNEL_STATE_FILE ": %m"); + return; + } + + len = snprintf(buf, sizeof(buf), "%d", state); + if (error_check(len, sizeof(buf))) { + xlog_warn("Failed to form NSM state number string"); + return; + } + + result = write(fd, buf, strlen(buf)); + if (exact_error_check(result, strlen(buf))) + xlog_warn("Failed to write NSM state number: %m"); + + if (close(fd) == -1) + xlog(L_ERROR, "Failed to close NSM state file " + NSM_KERNEL_STATE_FILE ": %m"); +} + +/** + * nsm_retire_monitored_hosts - back up all hosts from "sm/" to "sm.bak/" + * + * Returns the count of host records that were moved. + * + * Note that if any error occurs during this process, some monitor + * records may be left in the "sm" directory. + */ +unsigned int +nsm_retire_monitored_hosts(void) +{ + unsigned int count = 0; + struct dirent *de; + char *path; + DIR *dir; + + path = nsm_make_pathname(NSM_MONITOR_DIR); + if (path == NULL) { + xlog(L_ERROR, "Failed to allocate path for " NSM_MONITOR_DIR); + return count; + } + + dir = opendir(path); + free(path); + if (dir == NULL) { + xlog_warn("Failed to open " NSM_MONITOR_DIR ": %m"); + return count; + } + + while ((de = readdir(dir)) != NULL) { + char *src, *dst; + + if (de->d_type != (unsigned char)DT_REG) + continue; + if (de->d_name[0] == '.') + continue; + + src = nsm_make_record_pathname(NSM_MONITOR_DIR, de->d_name); + if (src == NULL) { + xlog_warn("Bad monitor file name, skipping"); + continue; + } + + dst = nsm_make_record_pathname(NSM_NOTIFY_DIR, de->d_name); + if (dst == NULL) { + free(src); + xlog_warn("Bad notify file name, skipping"); + continue; + } + + if (rename(src, dst) == -1) + xlog_warn("Failed to rename %s -> %s: %m", + src, dst); + else { + xlog(D_GENERAL, "Retired record for mon_name %s", + de->d_name); + count++; + } + + free(dst); + free(src); + } + + (void)closedir(dir); + return count; +} + +/* + * Returns the length in bytes of the created record. + */ +__attribute_noinline__ +static size_t +nsm_create_monitor_record(char *buf, const size_t buflen, + const struct sockaddr *sap, const struct mon *m) +{ + const struct sockaddr_in *sin = (const struct sockaddr_in *)sap; + size_t remaining = buflen; + int i, len; + + len = snprintf(buf, remaining, "%08x %08x %08x %08x ", + (unsigned int)sin->sin_addr.s_addr, + (unsigned int)m->mon_id.my_id.my_prog, + (unsigned int)m->mon_id.my_id.my_vers, + (unsigned int)m->mon_id.my_id.my_proc); + if (error_check(len, remaining)) + return 0; + buf += len; + remaining -= (size_t)len; + + for (i = 0; i < SM_PRIV_SIZE; i++) { + len = snprintf(buf, remaining, "%02x", + (unsigned int)(0xff & m->priv[i])); + if (error_check(len, remaining)) + return 0; + buf += len; + remaining -= (size_t)len; + } + + len = snprintf(buf, remaining, " %s %s\n", + m->mon_id.mon_name, m->mon_id.my_id.my_name); + if (error_check(len, remaining)) + return 0; + remaining -= (size_t)len; + + return buflen - remaining; +} + +/** + * nsm_insert_monitored_host - write callback data for one host to disk + * @hostname: C string containing a hostname + * @sap: sockaddr containing NLM callback address + * @mon: SM_MON arguments to save + * + * Returns true if successful, otherwise false if some error occurs. + */ +_Bool +nsm_insert_monitored_host(const char *hostname, const struct sockaddr *sap, + const struct mon *m) +{ + static char buf[LINELEN + 1 + SM_MAXSTRLEN + 2]; + char *path; + _Bool result = false; + ssize_t len; + size_t size; + int fd; + + path = nsm_make_record_pathname(NSM_MONITOR_DIR, hostname); + if (path == NULL) { + xlog(L_ERROR, "Failed to insert: bad monitor hostname '%s'", + hostname); + return false; + } + + size = nsm_create_monitor_record(buf, sizeof(buf), sap, m); + if (size == 0) { + xlog(L_ERROR, "Failed to insert: record too long"); + goto out; + } + + fd = open(path, O_WRONLY | O_CREAT | O_SYNC, S_IRUSR | S_IWUSR); + if (fd == -1) { + xlog(L_ERROR, "Failed to insert: creating %s: %m", path); + goto out; + } + result = true; + + len = write(fd, buf, size); + if (exact_error_check(len, size)) { + xlog_warn("Failed to insert: writing %s: %m", path); + (void)unlink(path); + result = false; + } + + if (close(fd) == -1) { + xlog(L_ERROR, "Failed to insert: closing %s: %m", path); + (void)unlink(path); + result = false; + } + +out: + free(path); + return result; +} + +__attribute_noinline__ +static _Bool +nsm_parse_line(char *line, struct sockaddr_in *sin, struct mon *m) +{ + unsigned int i, tmp; + int count; + char *c; + + c = strchr(line, '\n'); + if (c != NULL) + *c = '\0'; + + count = sscanf(line, "%8x %8x %8x %8x ", + (unsigned int *)&sin->sin_addr.s_addr, + (unsigned int *)&m->mon_id.my_id.my_prog, + (unsigned int *)&m->mon_id.my_id.my_vers, + (unsigned int *)&m->mon_id.my_id.my_proc); + if (count != 4) + return false; + + c = line + RPCARGSLEN; + for (i = 0; i < SM_PRIV_SIZE; i++) { + if (sscanf(c, "%2x", &tmp) != 1) + return false; + m->priv[i] = (char)tmp; + c += 2; + } + + c++; + m->mon_id.mon_name = c; + while (*c != '\0' && *c != ' ') + c++; + if (*c != '\0') + *c++ = '\0'; + while (*c == ' ') + c++; + m->mon_id.my_id.my_name = c; + + return true; +} + +/* + * Stuff a 'struct mon' with callback data, and call @func. + * + * Returns the count of in-core records created. + */ +static unsigned int +nsm_read_line(const char *hostname, const time_t timestamp, char *line, + nsm_populate_t func) +{ + struct sockaddr_in sin = { + .sin_family = AF_INET, + }; + struct mon m; + + if (!nsm_parse_line(line, &sin, &m)) + return 0; + + return func(hostname, (struct sockaddr *)(char *)&sin, &m, timestamp); +} + +/* + * Given a filename, reads data from a file under NSM_MONITOR_DIR + * and invokes @func so caller can populate their in-core + * database with this data. + */ +static unsigned int +nsm_load_host(const char *directory, const char *filename, nsm_populate_t func) +{ + char buf[LINELEN + 1 + SM_MAXSTRLEN + 2]; + unsigned int result = 0; + struct stat stb; + char *path; + FILE *f; + + path = nsm_make_record_pathname(directory, filename); + if (path == NULL) + goto out_err; + + if (stat(path, &stb) == -1) { + xlog(L_ERROR, "Failed to stat %s: %m", path); + goto out_freepath; + } + + f = fopen(path, "r"); + if (f == NULL) { + xlog(L_ERROR, "Failed to open %s: %m", path); + goto out_freepath; + } + + while (fgets(buf, (int)sizeof(buf), f) != NULL) { + buf[sizeof(buf) - 1] = '\0'; + result += nsm_read_line(filename, stb.st_mtime, buf, func); + } + if (result == 0) + xlog(L_ERROR, "Failed to read monitor data from %s", path); + + (void)fclose(f); + +out_freepath: + free(path); +out_err: + return result; +} + +static unsigned int +nsm_load_dir(const char *directory, nsm_populate_t func) +{ + unsigned int count = 0; + struct dirent *de; + char *path; + DIR *dir; + + path = nsm_make_pathname(directory); + if (path == NULL) { + xlog(L_ERROR, "Failed to allocate path for directory %s", + directory); + return 0; + } + + dir = opendir(path); + free(path); + if (dir == NULL) { + xlog(L_ERROR, "Failed to open directory %s: %m", + directory); + return 0; + } + + while ((de = readdir(dir)) != NULL) { + if (de->d_type != (unsigned char)DT_REG) + continue; + if (de->d_name[0] == '.') + continue; + + count += nsm_load_host(directory, de->d_name, func); + } + + (void)closedir(dir); + return count; +} + +/** + * nsm_load_monitor_list - load list of hosts to monitor + * @func: callback function to create entry for one host + * + * Returns the count of hosts that were found in the directory. + */ +unsigned int +nsm_load_monitor_list(nsm_populate_t func) +{ + return nsm_load_dir(NSM_MONITOR_DIR, func); +} + +/** + * nsm_load_notify_list - load list of hosts to notify + * @func: callback function to create entry for one host + * + * Returns the count of hosts that were found in the directory. + */ +unsigned int +nsm_load_notify_list(nsm_populate_t func) +{ + return nsm_load_dir(NSM_NOTIFY_DIR, func); +} + +static void +nsm_delete_host(const char *directory, const char *hostname) +{ + char *path; + + path = nsm_make_record_pathname(directory, hostname); + if (path == NULL) { + xlog(L_ERROR, "Bad filename, not deleting"); + return; + } + + if (unlink(path) == -1) + xlog(L_ERROR, "Failed to unlink %s: %m", path); + + free(path); +} + +/** + * nsm_delete_monitored_host - delete on-disk record for monitored host + * @hostname: '\0'-terminated C string containing hostname of record to delete + * + */ +void +nsm_delete_monitored_host(const char *hostname) +{ + nsm_delete_host(NSM_MONITOR_DIR, hostname); +} + +/** + * nsm_delete_notified_host - delete on-disk host record after notification + * @hostname: '\0'-terminated C string containing hostname of record to delete + * + */ +void +nsm_delete_notified_host(const char *hostname) +{ + nsm_delete_host(NSM_NOTIFY_DIR, hostname); +}