2 * nfsdcltrack.c -- NFSv4 client name tracking program
4 * Copyright (C) 2012 Jeff Layton <jlayton@redhat.com>
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License
8 * as published by the Free Software Foundation; either version 2
9 * of the License, or (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin Street, Fifth Floor,
19 * Boston, MA 02110-1301, USA.
24 #endif /* HAVE_CONFIG_H */
34 #include <sys/types.h>
38 #include <sys/inotify.h>
40 #ifdef HAVE_SYS_CAPABILITY_H
41 #include <sys/prctl.h>
42 #include <sys/capability.h>
48 #ifndef CLD_DEFAULT_STORAGEDIR
49 #define CLD_DEFAULT_STORAGEDIR NFS_STATEDIR "/nfsdcltrack"
52 /* defined by RFC 3530 */
53 #define NFS4_OPAQUE_LIMIT 1024
55 /* private data structures */
59 int (*func)(const char *arg);
62 /* forward declarations */
63 static int cltrack_init(const char *unused);
64 static int cltrack_create(const char *id);
65 static int cltrack_remove(const char *id);
66 static int cltrack_check(const char *id);
67 static int cltrack_gracedone(const char *gracetime);
69 /* global variables */
70 static struct option longopts[] =
72 { "help", 0, NULL, 'h' },
73 { "debug", 0, NULL, 'd' },
74 { "foreground", 0, NULL, 'f' },
75 { "storagedir", 1, NULL, 's' },
79 static struct cltrack_cmd commands[] =
81 { "init", false, cltrack_init },
82 { "create", true, cltrack_create },
83 { "remove", true, cltrack_remove },
84 { "check", true, cltrack_check },
85 { "gracedone", true, cltrack_gracedone },
86 { NULL, false, NULL },
89 static char *storagedir = CLD_DEFAULT_STORAGEDIR;
91 /* common buffer for holding id4 blobs */
92 static unsigned char blob[NFS4_OPAQUE_LIMIT];
97 printf("%s [ -hfd ] [ -s dir ] < cmd > < arg >\n", progname);
98 printf("Where < cmd > is one of the following and takes the following < arg >:\n");
100 printf(" create <nfs_client_id4>\n");
101 printf(" remove <nfs_client_id4>\n");
102 printf(" check <nfs_client_id4>\n");
103 printf(" gracedone <epoch time>\n");
108 * hex_to_bin - convert a hex digit to its real value
109 * @ch: ascii character represents hex digit
111 * hex_to_bin() converts one hex digit to its actual value or -1 in case of bad
114 * Note: borrowed from lib/hexdump.c in the Linux kernel sources.
119 if ((ch >= '0') && (ch <= '9'))
122 if ((ch >= 'a') && (ch <= 'f'))
123 return ch - 'a' + 10;
128 * hex_str_to_bin - convert a hexidecimal string into a binary blob
130 * @src: string of hex digit pairs
131 * @dst: destination buffer to hold binary data
132 * @dstsize: size of the destination buffer
134 * Walk a string of hex digit pairs and convert them into binary data. Returns
135 * the resulting length of the binary data or a negative error code. If the
136 * data will not fit in the buffer, it returns -ENOBUFS (but will likely have
137 * clobbered the dst buffer in the process of determining that). If there are
138 * non-hexidecimal characters in the src, or an odd number of them then it
142 hex_str_to_bin(const char *src, unsigned char *dst, ssize_t dstsize)
144 unsigned char *tmpdst = dst;
149 /* make sure we don't overrun the dst buffer */
150 if ((tmpdst - dst) >= dstsize)
153 hi = hex_to_bin(*src++);
155 /* did we get an odd number of characters? */
158 lo = hex_to_bin(*src++);
160 /* one of the characters isn't a hex digit */
161 if (hi < 0 || lo < 0)
164 /* now place it in the dst buffer */
165 *tmpdst++ = (hi << 4) | lo;
168 return (ssize_t)(tmpdst - dst);
172 * This program will almost always be run with root privileges since the
173 * kernel will call out to run it. Drop all capabilities prior to doing
174 * anything important to limit the exposure to potential compromise.
176 * FIXME: should we setuid to a different user early on instead?
179 cltrack_set_caps(void)
182 #ifdef HAVE_SYS_CAPABILITY_H
186 /* prune the bounding set to nothing */
187 for (i = 0; prctl(PR_CAPBSET_READ, i, 0, 0, 0) >= 0 ; ++i) {
188 ret = prctl(PR_CAPBSET_DROP, i, 0, 0, 0);
190 xlog(L_ERROR, "Unable to prune capability %lu from "
191 "bounding set: %m", i);
196 /* get a blank capset */
199 xlog(L_ERROR, "Unable to get blank capability set: %m");
203 /* reset the process capabilities */
204 if (cap_set_proc(caps) != 0) {
205 xlog(L_ERROR, "Unable to set process capabilities: %m");
214 cltrack_init(const char __attribute__((unused)) *unused)
219 * see if the storagedir is writable by root w/o CAP_DAC_OVERRIDE.
220 * If it isn't then give the user a warning but proceed as if
221 * everything is OK. If the DB has already been created, then
222 * everything might still work. If it doesn't exist at all, then
223 * assume that the maindb init will be able to create it. Fail on
226 if (access(storagedir, W_OK) == -1) {
229 xlog(L_WARNING, "Storage directory %s is not writable. "
230 "Should be owned by root and writable "
231 "by owner!", storagedir);
234 /* ignore and assume that we can create dir as root */
237 xlog(L_ERROR, "Unexpected error when checking access "
238 "on %s: %m", storagedir);
243 /* set up storage db */
244 ret = sqlite_maindb_init(storagedir);
246 xlog(L_ERROR, "Failed to init database: %d", ret);
248 * Convert any error here into -EACCES. It's not truly
249 * accurate in all cases, but it should cause the kernel to
250 * stop upcalling until the problem is resolved.
258 cltrack_create(const char *id)
263 xlog(D_GENERAL, "%s: create client record.", __func__);
265 ret = sqlite_prepare_dbh(storagedir);
269 len = hex_str_to_bin(id, blob, sizeof(blob));
273 ret = sqlite_insert_client(blob, len);
275 return ret ? -EREMOTEIO : ret;
279 cltrack_remove(const char *id)
284 xlog(D_GENERAL, "%s: remove client record.", __func__);
286 ret = sqlite_prepare_dbh(storagedir);
290 len = hex_str_to_bin(id, blob, sizeof(blob));
294 ret = sqlite_remove_client(blob, len);
296 return ret ? -EREMOTEIO : ret;
300 cltrack_check_legacy(const unsigned char *blob, const ssize_t len)
304 char *recdir = getenv("NFSDCLTRACK_LEGACY_RECDIR");
307 xlog(D_GENERAL, "No NFSDCLTRACK_LEGACY_RECDIR env var");
311 /* fail recovery on any stat failure */
312 ret = stat(recdir, &st);
314 xlog(D_GENERAL, "Unable to stat %s: %d", recdir, errno);
318 /* fail if it isn't a directory */
319 if (!S_ISDIR(st.st_mode)) {
320 xlog(D_GENERAL, "%s is not a directory: mode=0%o", recdir
325 /* Dir exists, try to insert record into db */
326 ret = sqlite_insert_client(blob, len);
328 xlog(D_GENERAL, "Failed to insert client: %d", ret);
332 /* remove the legacy recoverydir */
335 xlog(D_GENERAL, "Failed to rmdir %s: %d", recdir, errno);
342 cltrack_check(const char *id)
347 xlog(D_GENERAL, "%s: check client record", __func__);
349 ret = sqlite_prepare_dbh(storagedir);
353 len = hex_str_to_bin(id, blob, sizeof(blob));
357 ret = sqlite_check_client(blob, len);
359 ret = cltrack_check_legacy(blob, len);
361 return ret ? -EPERM : ret;
364 /* Clean out the v4recoverydir -- best effort here */
366 cltrack_legacy_gracedone(void)
369 struct dirent *entry;
370 char *dirname = getenv("NFSDCLTRACK_LEGACY_TOPDIR");
375 v4recovery = opendir(dirname);
379 while ((entry = readdir(v4recovery))) {
382 /* skip "." and ".." */
383 if (entry->d_name[0] == '.') {
384 switch (entry->d_name[1]) {
388 if (entry->d_name[2] == '\0')
393 /* borrow the clientid blob for this */
394 len = snprintf((char *)blob, sizeof(blob), "%s/%s", dirname,
397 /* if there's a problem, then skip this entry */
398 if (len < 0 || (size_t)len >= sizeof(blob)) {
399 xlog(L_WARNING, "%s: unable to build filename for %s!",
400 __func__, entry->d_name);
404 len = rmdir((char *)blob);
406 xlog(L_WARNING, "%s: unable to rmdir %s: %d", __func__,
410 closedir(v4recovery);
414 cltrack_gracedone(const char *timestr)
421 ret = sqlite_prepare_dbh(storagedir);
426 gracetime = strtol(timestr, &tail, 0);
428 /* did the resulting value overflow? (Probably -ERANGE here) */
432 /* string wasn't fully converted */
436 xlog(D_GENERAL, "%s: grace done. gracetime=%ld", __func__, gracetime);
438 ret = sqlite_remove_unreclaimed(gracetime);
440 cltrack_legacy_gracedone();
442 return ret ? -EREMOTEIO : ret;
445 static struct cltrack_cmd *
446 find_cmd(char *cmdname)
448 struct cltrack_cmd *current = &commands[0];
450 while (current->name) {
451 if (!strcmp(cmdname, current->name))
456 xlog(L_ERROR, "%s: '%s' doesn't match any known command",
462 main(int argc, char **argv)
466 char *progname, *cmdarg = NULL;
467 struct cltrack_cmd *cmd;
469 progname = basename(argv[0]);
474 /* process command-line options */
475 while ((arg = getopt_long(argc, argv, "hdfs:", longopts,
479 xlog_config(D_ALL, 1);
495 /* we expect a command, at least */
496 if (optind >= argc) {
497 xlog(L_ERROR, "Missing command name\n");
502 /* drop all capabilities */
503 rc = cltrack_set_caps();
507 cmd = find_cmd(argv[optind]);
510 * In the event that we get a command that we don't understand
511 * then return a distinct error. The kernel can use this to
512 * determine a new kernel/old userspace situation and cope
519 /* populate arg var if command needs it */
520 if (cmd->needs_arg) {
521 if (optind + 1 >= argc) {
522 xlog(L_ERROR, "Command %s requires an argument\n",
527 cmdarg = argv[optind + 1];
529 rc = cmd->func(cmdarg);