| /* |
| * nfsdcltrack.c -- NFSv4 client name tracking program |
| * |
| * Copyright (C) 2012 Jeff Layton <jlayton@redhat.com> |
| * |
| * This program 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. |
| * |
| * This program 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 this program; if not, write to the Free Software |
| * Foundation, Inc., 51 Franklin Street, Fifth Floor, |
| * Boston, MA 02110-1301, USA. |
| */ |
| |
| #ifdef HAVE_CONFIG_H |
| #include "config.h" |
| #endif /* HAVE_CONFIG_H */ |
| |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <ctype.h> |
| #include <errno.h> |
| #include <stdbool.h> |
| #include <getopt.h> |
| #include <string.h> |
| #include <sys/stat.h> |
| #include <sys/types.h> |
| #include <fcntl.h> |
| #include <inttypes.h> |
| #include <unistd.h> |
| #include <libgen.h> |
| #include <sys/inotify.h> |
| #include <dirent.h> |
| #include <limits.h> |
| #ifdef HAVE_SYS_CAPABILITY_H |
| #include <sys/prctl.h> |
| #include <sys/capability.h> |
| #endif |
| |
| #include "conffile.h" |
| #include "xlog.h" |
| #include "sqlite.h" |
| |
| #ifndef CLD_DEFAULT_STORAGEDIR |
| #define CLD_DEFAULT_STORAGEDIR NFS_STATEDIR "/nfsdcltrack" |
| #endif |
| |
| #define NFSD_END_GRACE_FILE "/proc/fs/nfsd/v4_end_grace" |
| |
| /* defined by RFC 3530 */ |
| #define NFS4_OPAQUE_LIMIT 1024 |
| |
| /* private data structures */ |
| struct cltrack_cmd { |
| char *name; |
| bool needs_arg; |
| int (*func)(const char *arg); |
| }; |
| |
| /* forward declarations */ |
| static int cltrack_init(const char *unused); |
| static int cltrack_create(const char *id); |
| static int cltrack_remove(const char *id); |
| static int cltrack_check(const char *id); |
| static int cltrack_gracedone(const char *gracetime); |
| |
| /* global variables */ |
| static struct option longopts[] = |
| { |
| { "help", 0, NULL, 'h' }, |
| { "debug", 0, NULL, 'd' }, |
| { "foreground", 0, NULL, 'f' }, |
| { "storagedir", 1, NULL, 's' }, |
| { NULL, 0, 0, 0 }, |
| }; |
| |
| static struct cltrack_cmd commands[] = |
| { |
| { "init", false, cltrack_init }, |
| { "create", true, cltrack_create }, |
| { "remove", true, cltrack_remove }, |
| { "check", true, cltrack_check }, |
| { "gracedone", true, cltrack_gracedone }, |
| { NULL, false, NULL }, |
| }; |
| |
| static char *storagedir = CLD_DEFAULT_STORAGEDIR; |
| |
| /* common buffer for holding id4 blobs */ |
| static unsigned char blob[NFS4_OPAQUE_LIMIT]; |
| |
| static void |
| usage(char *progname) |
| { |
| printf("Usage: %s [ -hfd ] [ -s dir ] < cmd > < arg >\n", progname); |
| printf("Where < cmd > is one of the following and takes the following < arg >:\n"); |
| printf(" init\n"); |
| printf(" create <nfs_client_id4>\n"); |
| printf(" remove <nfs_client_id4>\n"); |
| printf(" check <nfs_client_id4>\n"); |
| printf(" gracedone <epoch time>\n"); |
| } |
| |
| |
| /** |
| * hex_to_bin - convert a hex digit to its real value |
| * @ch: ascii character represents hex digit |
| * |
| * hex_to_bin() converts one hex digit to its actual value or -1 in case of bad |
| * input. |
| * |
| * Note: borrowed from lib/hexdump.c in the Linux kernel sources. |
| */ |
| static int |
| hex_to_bin(char ch) |
| { |
| if ((ch >= '0') && (ch <= '9')) |
| return ch - '0'; |
| ch = tolower(ch); |
| if ((ch >= 'a') && (ch <= 'f')) |
| return ch - 'a' + 10; |
| return -1; |
| } |
| |
| /** |
| * hex_str_to_bin - convert a hexidecimal string into a binary blob |
| * |
| * @src: string of hex digit pairs |
| * @dst: destination buffer to hold binary data |
| * @dstsize: size of the destination buffer |
| * |
| * Walk a string of hex digit pairs and convert them into binary data. Returns |
| * the resulting length of the binary data or a negative error code. If the |
| * data will not fit in the buffer, it returns -ENOBUFS (but will likely have |
| * clobbered the dst buffer in the process of determining that). If there are |
| * non-hexidecimal characters in the src, or an odd number of them then it |
| * returns -EINVAL. |
| */ |
| static ssize_t |
| hex_str_to_bin(const char *src, unsigned char *dst, ssize_t dstsize) |
| { |
| unsigned char *tmpdst = dst; |
| |
| while (*src) { |
| int hi, lo; |
| |
| /* make sure we don't overrun the dst buffer */ |
| if ((tmpdst - dst) >= dstsize) |
| return -ENOBUFS; |
| |
| hi = hex_to_bin(*src++); |
| |
| /* did we get an odd number of characters? */ |
| if (!*src) |
| return -EINVAL; |
| lo = hex_to_bin(*src++); |
| |
| /* one of the characters isn't a hex digit */ |
| if (hi < 0 || lo < 0) |
| return -EINVAL; |
| |
| /* now place it in the dst buffer */ |
| *tmpdst++ = (hi << 4) | lo; |
| } |
| |
| return (ssize_t)(tmpdst - dst); |
| } |
| |
| /* |
| * This program will almost always be run with root privileges since the |
| * kernel will call out to run it. Drop all capabilities prior to doing |
| * anything important to limit the exposure to potential compromise. |
| * |
| * FIXME: should we setuid to a different user early on instead? |
| */ |
| static int |
| cltrack_set_caps(void) |
| { |
| int ret = 0; |
| #ifdef HAVE_SYS_CAPABILITY_H |
| unsigned long i; |
| cap_t caps; |
| |
| /* prune the bounding set to nothing */ |
| for (i = 0; prctl(PR_CAPBSET_READ, i, 0, 0, 0) >= 0 ; ++i) { |
| ret = prctl(PR_CAPBSET_DROP, i, 0, 0, 0); |
| if (ret) { |
| xlog(L_ERROR, "Unable to prune capability %lu from " |
| "bounding set: %m", i); |
| return -errno; |
| } |
| } |
| |
| /* get a blank capset */ |
| caps = cap_init(); |
| if (caps == NULL) { |
| xlog(L_ERROR, "Unable to get blank capability set: %m"); |
| return -errno; |
| } |
| |
| /* reset the process capabilities */ |
| if (cap_set_proc(caps) != 0) { |
| xlog(L_ERROR, "Unable to set process capabilities: %m"); |
| ret = -errno; |
| } |
| cap_free(caps); |
| #endif |
| return ret; |
| } |
| |
| /* Inform the kernel that it's OK to lift nfsd's grace period */ |
| static void |
| cltrack_lift_grace_period(void) |
| { |
| int fd; |
| |
| fd = open(NFSD_END_GRACE_FILE, O_WRONLY); |
| if (fd < 0) { |
| /* Don't warn if file isn't present */ |
| if (errno != ENOENT) |
| xlog(L_WARNING, "Unable to open %s: %m", |
| NFSD_END_GRACE_FILE); |
| return; |
| } |
| |
| if (write(fd, "Y", 1) < 0) |
| xlog(L_WARNING, "Unable to write to %s: %m", |
| NFSD_END_GRACE_FILE); |
| |
| close(fd); |
| return; |
| } |
| |
| /* |
| * Fetch the contents of the NFSDCLTRACK_GRACE_START env var. If it's not set |
| * or there's an error converting it to time_t, then return LONG_MAX. |
| */ |
| static time_t |
| cltrack_get_grace_start(void) |
| { |
| time_t grace_start; |
| char *end; |
| char *grace_start_str = getenv("NFSDCLTRACK_GRACE_START"); |
| |
| if (!grace_start_str) |
| return LONG_MAX; |
| |
| errno = 0; |
| grace_start = strtol(grace_start_str, &end, 0); |
| /* Problem converting or value is too large? */ |
| if (errno) |
| return LONG_MAX; |
| |
| return grace_start; |
| } |
| |
| static bool |
| cltrack_reclaims_complete(void) |
| { |
| time_t grace_start = cltrack_get_grace_start(); |
| |
| /* Don't query DB if we didn't get a valid boot time */ |
| if (grace_start == LONG_MAX) |
| return false; |
| |
| return !sqlite_query_reclaiming(grace_start); |
| } |
| |
| static int |
| cltrack_init(const char __attribute__((unused)) *unused) |
| { |
| int ret; |
| |
| /* |
| * see if the storagedir is writable by root w/o CAP_DAC_OVERRIDE. |
| * If it isn't then give the user a warning but proceed as if |
| * everything is OK. If the DB has already been created, then |
| * everything might still work. If it doesn't exist at all, then |
| * assume that the maindb init will be able to create it. Fail on |
| * anything else. |
| */ |
| if (access(storagedir, W_OK) == -1) { |
| switch (errno) { |
| case EACCES: |
| xlog(L_WARNING, "Storage directory %s is not writable. " |
| "Should be owned by root and writable " |
| "by owner!", storagedir); |
| break; |
| case ENOENT: |
| /* ignore and assume that we can create dir as root */ |
| break; |
| default: |
| xlog(L_ERROR, "Unexpected error when checking access " |
| "on %s: %m", storagedir); |
| return -errno; |
| } |
| } |
| |
| /* set up storage db */ |
| ret = sqlite_prepare_dbh(storagedir); |
| if (ret) { |
| xlog(L_ERROR, "Failed to init database: %d", ret); |
| /* |
| * Convert any error here into -EACCES. It's not truly |
| * accurate in all cases, but it should cause the kernel to |
| * stop upcalling until the problem is resolved. |
| */ |
| ret = -EACCES; |
| } else { |
| if (cltrack_reclaims_complete()) |
| cltrack_lift_grace_period(); |
| } |
| |
| return ret; |
| } |
| |
| /* |
| * Fetch the contents of the NFSDCLTRACK_CLIENT_HAS_SESSION env var. If |
| * it's set and the first character is 'Y' then return true. Otherwise |
| * return false. |
| */ |
| static bool |
| cltrack_client_has_session(void) |
| { |
| char *has_session = getenv("NFSDCLTRACK_CLIENT_HAS_SESSION"); |
| |
| if (has_session && *has_session == 'Y') |
| return true; |
| |
| return false; |
| } |
| |
| static int |
| cltrack_create(const char *id) |
| { |
| int ret; |
| ssize_t len; |
| bool has_session; |
| |
| xlog(D_GENERAL, "%s: create client record.", __func__); |
| |
| ret = sqlite_prepare_dbh(storagedir); |
| if (ret) |
| return ret; |
| |
| len = hex_str_to_bin(id, blob, sizeof(blob)); |
| if (len < 0) |
| return (int)len; |
| |
| has_session = cltrack_client_has_session(); |
| |
| ret = sqlite_insert_client(blob, len, has_session, false); |
| |
| if (!ret && has_session && cltrack_reclaims_complete()) |
| cltrack_lift_grace_period(); |
| |
| return ret ? -EREMOTEIO : ret; |
| } |
| |
| static int |
| cltrack_remove(const char *id) |
| { |
| int ret; |
| ssize_t len; |
| |
| xlog(D_GENERAL, "%s: remove client record.", __func__); |
| |
| ret = sqlite_prepare_dbh(storagedir); |
| if (ret) |
| return ret; |
| |
| len = hex_str_to_bin(id, blob, sizeof(blob)); |
| if (len < 0) |
| return (int)len; |
| |
| ret = sqlite_remove_client(blob, len); |
| |
| return ret ? -EREMOTEIO : ret; |
| } |
| |
| static int |
| cltrack_check_legacy(const unsigned char *blob, const ssize_t len, |
| bool has_session) |
| { |
| int ret; |
| struct stat st; |
| char *recdir = getenv("NFSDCLTRACK_LEGACY_RECDIR"); |
| |
| if (!recdir) { |
| xlog(D_GENERAL, "No NFSDCLTRACK_LEGACY_RECDIR env var"); |
| return -EOPNOTSUPP; |
| } |
| |
| /* fail recovery on any stat failure */ |
| ret = stat(recdir, &st); |
| if (ret) { |
| xlog(D_GENERAL, "Unable to stat %s: %d", recdir, errno); |
| return -errno; |
| } |
| |
| /* fail if it isn't a directory */ |
| if (!S_ISDIR(st.st_mode)) { |
| xlog(D_GENERAL, "%s is not a directory: mode=0%o", recdir |
| , st.st_mode); |
| return -ENOTDIR; |
| } |
| |
| /* Dir exists, try to insert record into db */ |
| ret = sqlite_insert_client(blob, len, has_session, has_session); |
| if (ret) { |
| xlog(D_GENERAL, "Failed to insert client: %d", ret); |
| return -EREMOTEIO; |
| } |
| |
| /* remove the legacy recoverydir */ |
| ret = rmdir(recdir); |
| if (ret) { |
| xlog(D_GENERAL, "Failed to rmdir %s: %d", recdir, errno); |
| return -errno; |
| } |
| return 0; |
| } |
| |
| static int |
| cltrack_check(const char *id) |
| { |
| int ret; |
| ssize_t len; |
| bool has_session; |
| |
| xlog(D_GENERAL, "%s: check client record", __func__); |
| |
| ret = sqlite_prepare_dbh(storagedir); |
| if (ret) |
| return ret; |
| |
| len = hex_str_to_bin(id, blob, sizeof(blob)); |
| if (len < 0) |
| return (int)len; |
| |
| has_session = cltrack_client_has_session(); |
| |
| ret = sqlite_check_client(blob, len, has_session); |
| if (ret) |
| ret = cltrack_check_legacy(blob, len, has_session); |
| |
| return ret ? -EPERM : ret; |
| } |
| |
| /* Clean out the v4recoverydir -- best effort here */ |
| static void |
| cltrack_legacy_gracedone(void) |
| { |
| DIR *v4recovery; |
| struct dirent *entry; |
| char *dirname = getenv("NFSDCLTRACK_LEGACY_TOPDIR"); |
| |
| if (!dirname) |
| return; |
| |
| v4recovery = opendir(dirname); |
| if (!v4recovery) |
| return; |
| |
| while ((entry = readdir(v4recovery))) { |
| int len; |
| |
| /* skip "." and ".." */ |
| if (entry->d_name[0] == '.') { |
| switch (entry->d_name[1]) { |
| case '\0': |
| continue; |
| case '.': |
| if (entry->d_name[2] == '\0') |
| continue; |
| } |
| } |
| |
| /* borrow the clientid blob for this */ |
| len = snprintf((char *)blob, sizeof(blob), "%s/%s", dirname, |
| entry->d_name); |
| |
| /* if there's a problem, then skip this entry */ |
| if (len < 0 || (size_t)len >= sizeof(blob)) { |
| xlog(L_WARNING, "%s: unable to build filename for %s!", |
| __func__, entry->d_name); |
| continue; |
| } |
| |
| len = rmdir((char *)blob); |
| if (len) |
| xlog(L_WARNING, "%s: unable to rmdir %s: %d", __func__, |
| (char *)blob, len); |
| } |
| |
| closedir(v4recovery); |
| } |
| |
| static int |
| cltrack_gracedone(const char *timestr) |
| { |
| int ret; |
| char *tail; |
| uint64_t gracetime; |
| |
| |
| ret = sqlite_prepare_dbh(storagedir); |
| if (ret) |
| return ret; |
| |
| errno = 0; |
| gracetime = strtol(timestr, &tail, 0); |
| |
| /* did the resulting value overflow? (Probably -ERANGE here) */ |
| if (errno) |
| return -errno; |
| |
| /* string wasn't fully converted */ |
| if (*tail) |
| return -EINVAL; |
| |
| xlog(D_GENERAL, "%s: grace done. gracetime=%"PRIu64, __func__, gracetime); |
| |
| ret = sqlite_remove_unreclaimed(gracetime); |
| |
| cltrack_legacy_gracedone(); |
| |
| return ret ? -EREMOTEIO : ret; |
| } |
| |
| static struct cltrack_cmd * |
| find_cmd(char *cmdname) |
| { |
| struct cltrack_cmd *current = &commands[0]; |
| |
| while (current->name) { |
| if (!strcmp(cmdname, current->name)) |
| return current; |
| ++current; |
| } |
| |
| xlog(L_ERROR, "%s: '%s' doesn't match any known command", |
| __func__, cmdname); |
| return NULL; |
| } |
| inline static void |
| read_nfsdcltrack_conf(void) |
| { |
| char *val; |
| |
| conf_init_file(NFS_CONFFILE); |
| xlog_set_debug("nfsdcltrack"); |
| val = conf_get_str("nfsdcltrack", "storagedir"); |
| if (val) |
| storagedir = val; |
| } |
| int |
| main(int argc, char **argv) |
| { |
| int arg; |
| int rc = 0; |
| char *progname, *cmdarg = NULL; |
| struct cltrack_cmd *cmd; |
| |
| progname = basename(argv[0]); |
| |
| xlog_syslog(1); |
| xlog_stderr(0); |
| |
| /* Read in config setting */ |
| read_nfsdcltrack_conf(); |
| |
| /* process command-line options */ |
| while ((arg = getopt_long(argc, argv, "hdfs:", longopts, |
| NULL)) != EOF) { |
| switch (arg) { |
| case 'd': |
| xlog_config(D_ALL, 1); |
| break; |
| case 'f': |
| xlog_syslog(0); |
| xlog_stderr(1); |
| break; |
| case 's': |
| storagedir = optarg; |
| break; |
| default: |
| usage(progname); |
| return 1; |
| } |
| } |
| |
| xlog_open(progname); |
| |
| /* we expect a command, at least */ |
| if (optind >= argc) { |
| xlog(L_ERROR, "Missing command name\n"); |
| rc = -EINVAL; |
| goto out; |
| } |
| |
| /* drop all capabilities */ |
| rc = cltrack_set_caps(); |
| if (rc) |
| goto out; |
| |
| cmd = find_cmd(argv[optind]); |
| if (!cmd) { |
| /* |
| * In the event that we get a command that we don't understand |
| * then return a distinct error. The kernel can use this to |
| * determine a new kernel/old userspace situation and cope |
| * with it. |
| */ |
| rc = -ENOSYS; |
| goto out; |
| } |
| |
| /* populate arg var if command needs it */ |
| if (cmd->needs_arg) { |
| if (optind + 1 >= argc) { |
| xlog(L_ERROR, "Command %s requires an argument\n", |
| cmd->name); |
| rc = -EINVAL; |
| goto out; |
| } |
| cmdarg = argv[optind + 1]; |
| } |
| rc = cmd->func(cmdarg); |
| out: |
| return rc; |
| } |