/* * 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; } /* * nsm_priv_to_hex - convert a NSM private cookie to a hex string. * * @priv: buffer holding the binary NSM private cookie * @buf: output buffer for NULL terminated hex string * @buflen: size of output buffer * * Returns the length of the resulting string or 0 on error */ size_t nsm_priv_to_hex(const char *priv, char *buf, const size_t buflen) { int i, len; size_t remaining = buflen; for (i = 0; i < SM_PRIV_SIZE; i++) { len = snprintf(buf, remaining, "%02x", (unsigned int)(0xff & priv[i])); if (error_check(len, remaining)) return 0; buf += len; remaining -= (size_t)len; } return buflen - remaining; } /* * 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 hexlen, remaining = buflen; int 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; hexlen = nsm_priv_to_hex(m->priv, buf, remaining); if (hexlen == 0) return 0; buf += hexlen; remaining -= hexlen; 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); }