Add the #vars device to export kernel variables
authorBarret Rhoden <brho@cs.berkeley.edu>
Tue, 24 Nov 2015 13:29:44 +0000 (08:29 -0500)
committerBarret Rhoden <brho@cs.berkeley.edu>
Mon, 30 Nov 2015 21:06:38 +0000 (16:06 -0500)
With #vars, you can specify certain variables, such as num_cores, to be
exposed to userspace.  If you want, you can:

$ bind -a \#vars /dev
$ cat /dev/num_cores!dw

For debugging, you can add entries with the DEVVARS_ENTRY(name, format)
macro.  'format' is two chars, the data_format and the data_size, using
Qemu's notation.

Privileged users (i.e. anyone) can add new entries to #vars, which
internally will do a lookup in the symbol table and trust the format
string.  This is a little dangerous.

Signed-off-by: Barret Rhoden <brho@cs.berkeley.edu>
kern/drivers/dev/Kbuild
kern/drivers/dev/Kconfig
kern/drivers/dev/vars.c [new file with mode: 0644]
kern/include/ns.h

index 57fd936..400a9b7 100644 (file)
@@ -12,5 +12,6 @@ obj-y                                         += proc.o
 obj-$(CONFIG_REGRESS)                          += regress.o
 obj-y                                          += root.o
 obj-y                                          += srv.o
+obj-$(CONFIG_DEVVARS)          += vars.o
 
 obj-$(CONFIG_NIX)                              += nix.o
index eb737b8..d01c6dd 100644 (file)
@@ -4,3 +4,9 @@ config REGRESS
         help
                 The regression test device allows you to push commands to monitor()
                for testing. Defaults to 'y' for now.
+
+config DEVVARS
+       bool "#vars kernel variable exporter"
+       default y
+       help
+               The #vars device exports read access to select kernel variables.
diff --git a/kern/drivers/dev/vars.c b/kern/drivers/dev/vars.c
new file mode 100644 (file)
index 0000000..bf9d685
--- /dev/null
@@ -0,0 +1,441 @@
+/* Copyright (c) 2015 Google Inc
+ * Barret Rhoden <brho@cs.berkeley.edu>
+ * See LICENSE for details.
+ *
+ * #vars device, exports read access to select kernel variables.  These
+ * variables are statically set.
+ *
+ * To add a variable, add a DEVVARS_ENTRY(name, format) somewhere in the kernel.
+ * The format is a string consisting of two characters, using a modified version
+ * of QEMU's formatting rules (ignoring count): [data_format][size]
+ *
+ * data_format is:
+ *     x (hex)
+ *     d (decimal)
+ *     u (unsigned)
+ *     o (octal)
+ *     c (char)                                does not need a size
+ *     s (string)                      does not need a size
+ * size is:
+ *     b (8 bits)
+ *     h (16 bits)
+ *     w (32 bits)
+ *     g (64 bits)
+ *
+ * e.g. DEVVARS_ENTRY(num_cores, "dw");
+ *
+ * Another thing we can consider doing is implementing create() to add variables
+ * on the fly.  We can easily get the address (symbol table), but not the type,
+ * unless we get debugging info.  We could consider a CTL command to allow the
+ * user to change the type, though that might overload write() if we also allow
+ * setting variables. */
+
+#include <ns.h>
+#include <kmalloc.h>
+#include <kref.h>
+#include <atomic.h>
+#include <string.h>
+#include <stdio.h>
+#include <assert.h>
+#include <error.h>
+#include <sys/queue.h>
+#include <fdtap.h>
+#include <syscall.h>
+
+struct dev vars_devtab;
+
+static char *devname(void)
+{
+       return vars_devtab.name;
+}
+
+static struct dirtab *vars_dir;
+static size_t nr_vars;
+static qlock_t vars_lock;
+
+struct dirtab __attribute__((__section__("devvars")))
+              __devvars_dot = {".", {0, 0, QTDIR}, 0, DMDIR | 0555};
+
+DEVVARS_ENTRY(num_cores, "dw");
+
+static bool var_is_valid(struct dirtab *dir)
+{
+       return dir->qid.vers != -1;
+}
+
+/* Careful with this.  c->name->s is the full path, at least sometimes. */
+static struct dirtab *find_var_by_name(const char *name)
+{
+       for (size_t i = 0; i < nr_vars; i++)
+               if (!strcmp(vars_dir[i].name, name))
+                       return &vars_dir[i];
+       return 0;
+}
+
+static void vars_init(void)
+{
+       /* If you name a section without a '.', GCC will create start and stop
+        * symbols, e.g. __start_SECTION */
+       extern struct dirtab __start_devvars;
+       extern struct dirtab __stop_devvars;
+       struct dirtab *dot, temp;
+
+       nr_vars = &__stop_devvars - &__start_devvars;
+       vars_dir = kmalloc_array(nr_vars, sizeof(struct dirtab), KMALLOC_WAIT);
+       if (!vars_dir)
+               error(ENOMEM, "kmalloc_array failed, nr_vars was %p", nr_vars);
+       memcpy(vars_dir, &__start_devvars, nr_vars * sizeof(struct dirtab));
+       /* "." needs to be the first entry in a devtable.  It might already be
+        * first, but we can do the swap regardless. */
+       temp = vars_dir[0];
+       dot = find_var_by_name(".");
+       assert(dot);
+       vars_dir[0] = *dot;
+       *dot = temp;
+       qlock_init(&vars_lock);
+}
+
+static struct chan *vars_attach(char *spec)
+{
+       struct chan *c;
+
+       c = devattach(devname(), spec);
+       mkqid(&c->qid, 0, 0, QTDIR);
+       return c;
+}
+
+static struct walkqid *vars_walk(struct chan *c, struct chan *nc, char **name,
+                                                                int nname)
+{
+       ERRSTACK(1);
+       struct walkqid *ret;
+
+       qlock(&vars_lock);
+       if (waserror()) {
+               qunlock(&vars_lock);
+               nexterror();
+       }
+       ret = devwalk(c, nc, name, nname, vars_dir, nr_vars, devgen);
+       poperror();
+       qunlock(&vars_lock);
+       return ret;
+}
+
+static int vars_stat(struct chan *c, uint8_t *db, int n)
+{
+       ERRSTACK(1);
+       int ret;
+
+       qlock(&vars_lock);
+       if (waserror()) {
+               qunlock(&vars_lock);
+               nexterror();
+       }
+       ret = devstat(c, db, n, vars_dir, nr_vars, devgen);
+       poperror();
+       qunlock(&vars_lock);
+       return ret;
+}
+
+static struct chan *vars_open(struct chan *c, int omode)
+{
+       ERRSTACK(1);
+       struct chan *ret;
+
+       qlock(&vars_lock);
+       if (waserror()) {
+               qunlock(&vars_lock);
+               nexterror();
+       }
+       ret = devopen(c, omode, vars_dir, nr_vars, devgen);
+       poperror();
+       qunlock(&vars_lock);
+       return ret;
+}
+
+static void vars_close(struct chan *c)
+{
+}
+
+static struct dirtab *find_free_var(void)
+{
+       for (size_t i = 0; i < nr_vars; i++)
+               if (!var_is_valid(&vars_dir[i]))
+                       return &vars_dir[i];
+       return 0;
+}
+
+/* We ignore the perm - they are all hard-coded in the dirtab */
+static void vars_create(struct chan *c, char *name, int omode, uint32_t perm)
+{
+       struct dirtab *new_slot;
+       uintptr_t addr;
+       char *bang;
+       size_t size;
+
+       /* TODO: check that the user is privileged */
+       bang = strchr(name, '!');
+       if (!bang)
+               error(EINVAL, "Var %s has no ! in its format string", name);
+       *bang = 0;
+       addr = get_symbol_addr(name);
+       *bang = '!';
+       if (!addr)
+               error(EINVAL, "Could not find symbol for %s", name);
+       bang++;
+       /* Note that we don't check the symbol type against the format.  We're
+        * trusting the user here.  o/w we'd need dwarf support. */
+       switch (*bang) {
+       case 'c':
+               size = sizeof(char);
+               break;
+       case 's':
+               size = sizeof(char*);
+               break;
+       case 'd':
+       case 'x':
+       case 'u':
+       case 'o':
+               bang++;
+               switch (*bang) {
+               case 'b':
+                       size = sizeof(uint8_t);
+                       break;
+               case 'h':
+                       size = sizeof(uint16_t);
+                       break;
+               case 'w':
+                       size = sizeof(uint32_t);
+                       break;
+               case 'g':
+                       size = sizeof(uint64_t);
+                       break;
+               default:
+                       error(EINVAL, "Bad var size '%c'", *bang);
+               }
+               break;
+       default:
+               error(EINVAL, "Unknown var data_format '%c'", *bang);
+       }
+       bang++;
+       if (*bang)
+               error(EINVAL, "Extra chars for var %s", name);
+
+       qlock(&vars_lock);
+       new_slot = find_free_var();
+       if (!new_slot) {
+               vars_dir = kreallocarray(vars_dir, nr_vars * 2, sizeof(struct dirtab),
+                                        KMALLOC_WAIT);
+               if (!vars_dir)
+                       error(ENOMEM, "krealloc_array failed, nr_vars was %p", nr_vars);
+               memset(vars_dir + nr_vars, 0, nr_vars * sizeof(struct dirtab));
+               for (size_t i = nr_vars; i < nr_vars * 2; i++)
+                       vars_dir[i].qid.vers = -1;
+               new_slot = vars_dir + nr_vars;
+               nr_vars *= 2;
+       }
+       strncpy(new_slot->name, name, sizeof(new_slot->name));
+       new_slot->qid.path = addr;
+       new_slot->qid.vers = 0;
+       new_slot->qid.type = QTFILE;
+       new_slot->length = size;
+       new_slot->perm = 0444;
+       c->qid = new_slot->qid;         /* need to update c with its new qid */
+       qunlock(&vars_lock);
+       c->mode = openmode(omode);
+}
+
+static const char *get_integer_fmt(char data_fmt, char data_size)
+{
+       switch (data_fmt) {
+       case 'x':
+               switch (data_size) {
+               case 'b':
+               case 'h':
+               case 'w':
+                       return "0x%x";
+               case 'g':
+                       return "0x%lx";
+               }
+       case 'd':
+               switch (data_size) {
+               case 'b':
+               case 'h':
+               case 'w':
+                       return "%d";
+               case 'g':
+                       return "%ld";
+               }
+       case 'u':
+               switch (data_size) {
+               case 'b':
+               case 'h':
+               case 'w':
+                       return "%u";
+               case 'g':
+                       return "%lu";
+               }
+       case 'o':
+               switch (data_size) {
+               case 'b':
+               case 'h':
+               case 'w':
+                       return "0%o";
+               case 'g':
+                       return "0%lo";
+               }
+       }
+       return 0;
+}
+
+static long vars_read(struct chan *c, void *ubuf, long n, int64_t offset)
+{
+       ERRSTACK(1);
+       char tmp[128];  /* big enough for any number and most strings */
+       size_t size = sizeof(tmp);
+       char data_size, data_fmt, *fmt;
+       const char *fmt_int;
+       bool is_signed = FALSE;
+       long ret;
+
+       qlock(&vars_lock);
+       if (waserror()) {
+               qunlock(&vars_lock);
+               nexterror();
+       }
+
+       if (c->qid.type == QTDIR) {
+               ret = devdirread(c, ubuf, n, vars_dir, nr_vars, devgen);
+               poperror();
+               qunlock(&vars_lock);
+               return ret;
+       }
+
+       /* These checks are mostly for the static variables.  They are a
+        * double-check for the user-provided vars. */
+       fmt = strchr(c->name->s, '!');
+       if (!fmt)
+               error(EINVAL, "var %s has no ! in its format string", c->name->s);
+       fmt++;
+       data_fmt = *fmt;
+       if (!data_fmt)
+               error(EINVAL, "var %s has no data_format", c->name->s);
+
+       switch (data_fmt) {
+       case 'c':
+               size = snprintf(tmp, size, "%c", *(char*)c->qid.path);
+               break;
+       case 's':
+               size = snprintf(tmp, size, "%s", *(char**)c->qid.path);
+               break;
+       case 'd':
+               is_signed = TRUE;
+               /* fall through */
+       case 'x':
+       case 'u':
+       case 'o':
+               fmt++;
+               data_size = *fmt;
+               if (!data_size)
+                       error(EINVAL, "var %s has no size", c->name->s);
+               fmt_int = get_integer_fmt(data_fmt, data_size);
+               if (!fmt_int)
+                       error(EINVAL, "#%s was unable to get an int fmt for %s",
+                             devname(), c->name->s);
+               switch (data_size) {
+               case 'b':
+                       if (is_signed)
+                               size = snprintf(tmp, size, fmt_int, *(int8_t*)c->qid.path);
+                       else
+                               size = snprintf(tmp, size, fmt_int, *(uint8_t*)c->qid.path);
+                       break;
+               case 'h':
+                       if (is_signed)
+                               size = snprintf(tmp, size, fmt_int, *(int16_t*)c->qid.path);
+                       else
+                               size = snprintf(tmp, size, fmt_int, *(uint16_t*)c->qid.path);
+                       break;
+               case 'w':
+                       if (is_signed)
+                               size = snprintf(tmp, size, fmt_int, *(int32_t*)c->qid.path);
+                       else
+                               size = snprintf(tmp, size, fmt_int, *(uint32_t*)c->qid.path);
+                       break;
+               case 'g':
+                       if (is_signed)
+                               size = snprintf(tmp, size, fmt_int, *(int64_t*)c->qid.path);
+                       else
+                               size = snprintf(tmp, size, fmt_int, *(uint64_t*)c->qid.path);
+                       break;
+               default:
+                       error(EINVAL, "Bad #%s size %c", devname(), data_size);
+               }
+               break;
+       default:
+               error(EINVAL, "Unknown #%s data_format %c", devname(), data_fmt);
+       }
+       fmt++;
+       if (*fmt)
+               error(EINVAL, "Extra characters after var %s", c->name->s);
+       ret = readmem(offset, ubuf, n, tmp, size + 1);
+       poperror();
+       qunlock(&vars_lock);
+       return ret;
+}
+
+static long vars_write(struct chan *c, void *ubuf, long n, int64_t offset)
+{
+       error(EFAIL, "Can't write to a #%s file", devname());
+}
+
+/* remove is interesting.  we mark the qid in the dirtab as -1, which is a
+ * signal to devgen that it is an invalid entry.  someone could already have
+ * done a walk (before we qlocked) and grabbed the qid before it was -1.  as far
+ * as they are concerned, they have a valid entry, since "the qid is the file"
+ * for devvars.  (i.e. the chan gets a copy of the entire file, which fits into
+ * the qid). */
+static void vars_remove(struct chan *c)
+{
+       ERRSTACK(1);
+       struct dirtab *dir;
+       char *dir_name;
+
+       /* chan's name may have multiple elements in the path; get the last one. */
+       dir_name = strrchr(c->name->s, '/');
+       dir_name = dir_name ? dir_name + 1 : c->name->s;
+
+       qlock(&vars_lock);
+       if (waserror()) {
+               qunlock(&vars_lock);
+               nexterror();
+       }
+       dir = find_var_by_name(dir_name);
+       if (!dir || dir->qid.vers == -1)
+               error(ENOENT, "Failed to remove %s, was it already removed?",
+                     c->name->s);
+       dir->qid.vers = -1;
+       poperror();
+       qunlock(&vars_lock);
+}
+
+struct dev vars_devtab __devtab = {
+       .name = "vars",
+       .reset = devreset,
+       .init = vars_init,
+       .shutdown = devshutdown,
+       .attach = vars_attach,
+       .walk = vars_walk,
+       .stat = vars_stat,
+       .open = vars_open,
+       .create = vars_create,
+       .close = vars_close,
+       .read = vars_read,
+       .bread = devbread,
+       .write = vars_write,
+       .bwrite = devbwrite,
+       .remove = vars_remove,
+       .wstat = devwstat,
+       .power = devpower,
+       .chaninfo = devchaninfo,
+       .tapfd = 0,
+};
index 1661584..c24f4c8 100644 (file)
@@ -1000,3 +1000,10 @@ extern unsigned int qiomaxatomic;
 
 /* special sections */
 #define __devtab  __attribute__((__section__(".devtab")))
+
+#define DEVVARS_ENTRY(name, fmt)                                               \
+struct dirtab __attribute__((__section__("devvars"))) __devvars_##name =       \
+              {#name "!" fmt,                                                  \
+               {(uint64_t)&(name), 0, QTFILE},                                 \
+               sizeof((name)),                                                 \
+               0444}