busybox/util-linux/mdev.c
<<
>>
Prefs
   1/* vi: set sw=4 ts=4: */
   2/*
   3 * mdev - Mini udev for busybox
   4 *
   5 * Copyright 2005 Rob Landley <rob@landley.net>
   6 * Copyright 2005 Frank Sorenson <frank@tuxrocks.com>
   7 *
   8 * Licensed under GPL version 2, see file LICENSE in this tarball for details.
   9 */
  10#include "libbb.h"
  11#include "xregex.h"
  12
  13/* "mdev -s" scans /sys/class/xxx, looking for directories which have dev
  14 * file (it is of the form "M:m\n"). Example: /sys/class/tty/tty0/dev
  15 * contains "4:0\n". Directory name is taken as device name, path component
  16 * directly after /sys/class/ as subsystem. In this example, "tty0" and "tty".
  17 * Then mdev creates the /dev/device_name node.
  18 * If /sys/class/.../dev file does not exist, mdev still may act
  19 * on this device: see "@|$|*command args..." parameter in config file.
  20 *
  21 * mdev w/o parameters is called as hotplug helper. It takes device
  22 * and subsystem names from $DEVPATH and $SUBSYSTEM, extracts
  23 * maj,min from "/sys/$DEVPATH/dev" and also examines
  24 * $ACTION ("add"/"delete") and $FIRMWARE.
  25 *
  26 * If action is "add", mdev creates /dev/device_name similarly to mdev -s.
  27 * (todo: explain "delete" and $FIRMWARE)
  28 *
  29 * If /etc/mdev.conf exists, it may modify /dev/device_name's properties.
  30 * /etc/mdev.conf file format:
  31 *
  32 * [-][subsystem/]device  user:grp  mode  [>|=path] [@|$|*command args...]
  33 * [-]@maj,min[-min2]     user:grp  mode  [>|=path] [@|$|*command args...]
  34 * [-]$envvar=val         user:grp  mode  [>|=path] [@|$|*command args...]
  35 *
  36 * Leading minus in 1st field means "don't stop on this line", otherwise
  37 * search is stopped after the matching line is encountered.
  38 *
  39 * The device name or "subsystem/device" combo is matched against 1st field
  40 * (which is a regex), or maj,min is matched against 1st field,
  41 * or specified environment variable (as regex) is matched against 1st field.
  42 *
  43 * $envvar=val format is useful for loading modules for hot-plugged devices
  44 * which do not have driver loaded yet. In this case /sys/class/.../dev
  45 * does not exist, but $MODALIAS is set to needed module's name
  46 * (actually, an alias to it) by kernel. This rule instructs mdev
  47 * to load the module and exit:
  48 *    $MODALIAS=.* 0:0 660 @modprobe "$MODALIAS"
  49 * The kernel will generate another hotplug event when /sys/class/.../dev
  50 * file appears.
  51 *
  52 * When line matches, the device node is created, chmod'ed and chown'ed,
  53 * moved to path, and if >path, a symlink to moved node is created,
  54 * all this if /sys/class/.../dev exists.
  55 *    Examples:
  56 *    =loop/      - moves to /dev/loop
  57 *    >disk/sda%1 - moves to /dev/disk/sdaN, makes /dev/sdaN a symlink
  58 *
  59 * Then "command args..." is executed (via sh -c 'command args...').
  60 * @:execute on creation, $:on deletion, *:on both.
  61 * This happens regardless of /sys/class/.../dev existence.
  62 */
  63
  64struct globals {
  65        int root_major, root_minor;
  66        char *subsystem;
  67};
  68#define G (*(struct globals*)&bb_common_bufsiz1)
  69#define root_major (G.root_major)
  70#define root_minor (G.root_minor)
  71#define subsystem  (G.subsystem )
  72
  73/* Prevent infinite loops in /sys symlinks */
  74#define MAX_SYSFS_DEPTH 3
  75
  76/* We use additional 64+ bytes in make_device() */
  77#define SCRATCH_SIZE 80
  78
  79/* Builds an alias path.
  80 * This function potentionally reallocates the alias parameter.
  81 * Only used for ENABLE_FEATURE_MDEV_RENAME
  82 */
  83static char *build_alias(char *alias, const char *device_name)
  84{
  85        char *dest;
  86
  87        /* ">bar/": rename to bar/device_name */
  88        /* ">bar[/]baz": rename to bar[/]baz */
  89        dest = strrchr(alias, '/');
  90        if (dest) { /* ">bar/[baz]" ? */
  91                *dest = '\0'; /* mkdir bar */
  92                bb_make_directory(alias, 0755, FILEUTILS_RECUR);
  93                *dest = '/';
  94                if (dest[1] == '\0') { /* ">bar/" => ">bar/device_name" */
  95                        dest = alias;
  96                        alias = concat_path_file(alias, device_name);
  97                        free(dest);
  98                }
  99        }
 100
 101        return alias;
 102}
 103
 104/* mknod in /dev based on a path like "/sys/block/hda/hda1"
 105 * NB1: path parameter needs to have SCRATCH_SIZE scratch bytes
 106 * after NUL, but we promise to not mangle (IOW: to restore if needed)
 107 * path string.
 108 * NB2: "mdev -s" may call us many times, do not leak memory/fds!
 109 */
 110static void make_device(char *path, int delete)
 111{
 112        char *device_name;
 113        int major, minor, type, len;
 114        int mode;
 115        parser_t *parser;
 116
 117        /* Try to read major/minor string.  Note that the kernel puts \n after
 118         * the data, so we don't need to worry about null terminating the string
 119         * because sscanf() will stop at the first nondigit, which \n is.
 120         * We also depend on path having writeable space after it.
 121         */
 122        major = -1;
 123        if (!delete) {
 124                char *dev_maj_min = path + strlen(path);
 125
 126                strcpy(dev_maj_min, "/dev");
 127                len = open_read_close(path, dev_maj_min + 1, 64);
 128                *dev_maj_min = '\0';
 129                if (len < 1) {
 130                        if (!ENABLE_FEATURE_MDEV_EXEC)
 131                                return;
 132                        /* no "dev" file, but we can still run scripts
 133                         * based on device name */
 134                } else if (sscanf(++dev_maj_min, "%u:%u", &major, &minor) != 2) {
 135                        major = -1;
 136                }
 137        }
 138
 139        /* Determine device name, type, major and minor */
 140        device_name = (char*) bb_basename(path);
 141        /* http://kernel.org/doc/pending/hotplug.txt says that only
 142         * "/sys/block/..." is for block devices. "/sys/bus" etc is not.
 143         * But since 2.6.25 block devices are also in /sys/class/block,
 144         * we use strstr("/block/") to forestall future surprises. */
 145        type = S_IFCHR;
 146        if (strstr(path, "/block/"))
 147                type = S_IFBLK;
 148
 149        /* Make path point to "subsystem/device_name" */
 150        if (path[5] == 'b') /* legacy /sys/block? */
 151                path += sizeof("/sys/") - 1;
 152        else
 153                path += sizeof("/sys/class/") - 1;
 154
 155        /* If we have config file, look up user settings */
 156        if (ENABLE_FEATURE_MDEV_CONF)
 157                parser = config_open2("/etc/mdev.conf", fopen_for_read);
 158
 159        do {
 160                int keep_matching;
 161                struct bb_uidgid_t ugid;
 162                char *tokens[4];
 163                char *command = NULL;
 164                char *alias = NULL;
 165                char aliaslink = aliaslink; /* for compiler */
 166
 167                /* Defaults in case we won't match any line */
 168                ugid.uid = ugid.gid = 0;
 169                keep_matching = 0;
 170                mode = 0660;
 171
 172                if (ENABLE_FEATURE_MDEV_CONF
 173                 && config_read(parser, tokens, 4, 3, "# \t", PARSE_NORMAL)
 174                ) {
 175                        char *val;
 176                        char *str_to_match;
 177                        regmatch_t off[1 + 9 * ENABLE_FEATURE_MDEV_RENAME_REGEXP];
 178
 179                        val = tokens[0];
 180                        keep_matching = ('-' == val[0]);
 181                        val += keep_matching; /* swallow leading dash */
 182
 183                        /* Match against either "subsystem/device_name"
 184                         * or "device_name" alone */
 185                        str_to_match = strchr(val, '/') ? path : device_name;
 186
 187                        /* Fields: regex uid:gid mode [alias] [cmd] */
 188
 189                        if (val[0] == '@') {
 190                                /* @major,minor[-minor2] */
 191                                /* (useful when name is ambiguous:
 192                                 * "/sys/class/usb/lp0" and
 193                                 * "/sys/class/printer/lp0") */
 194                                int cmaj, cmin0, cmin1, sc;
 195                                if (major < 0)
 196                                        continue; /* no dev, no match */
 197                                sc = sscanf(val, "@%u,%u-%u", &cmaj, &cmin0, &cmin1);
 198                                if (sc < 1 || major != cmaj
 199                                 || (sc == 2 && minor != cmin0)
 200                                 || (sc == 3 && (minor < cmin0 || minor > cmin1))
 201                                ) {
 202                                        continue; /* this line doesn't match */
 203                                }
 204                                goto line_matches;
 205                        }
 206                        if (val[0] == '$') {
 207                                /* regex to match an environment variable */
 208                                char *eq = strchr(++val, '=');
 209                                if (!eq)
 210                                        continue;
 211                                *eq = '\0';
 212                                str_to_match = getenv(val);
 213                                if (!str_to_match)
 214                                        continue;
 215                                str_to_match -= strlen(val) + 1;
 216                                *eq = '=';
 217                        }
 218                        /* else: regex to match [subsystem/]device_name */
 219
 220                        {
 221                                regex_t match;
 222                                int result;
 223
 224                                xregcomp(&match, val, REG_EXTENDED);
 225                                result = regexec(&match, str_to_match, ARRAY_SIZE(off), off, 0);
 226                                regfree(&match);
 227                                //bb_error_msg("matches:");
 228                                //for (int i = 0; i < ARRAY_SIZE(off); i++) {
 229                                //      if (off[i].rm_so < 0) continue;
 230                                //      bb_error_msg("match %d: '%.*s'\n", i,
 231                                //              (int)(off[i].rm_eo - off[i].rm_so),
 232                                //              device_name + off[i].rm_so);
 233                                //}
 234
 235                                /* If no match, skip rest of line */
 236                                /* (regexec returns whole pattern as "range" 0) */
 237                                if (result || off[0].rm_so
 238                                 || ((int)off[0].rm_eo != (int)strlen(str_to_match))
 239                                ) {
 240                                        continue; /* this line doesn't match */
 241                                }
 242                        }
 243 line_matches:
 244                        /* This line matches. Stop parsing after parsing
 245                         * the rest the line unless keep_matching == 1 */
 246
 247                        /* 2nd field: uid:gid - device ownership */
 248                        if (get_uidgid(&ugid, tokens[1], 1) == 0)
 249                                bb_error_msg("unknown user/group %s", tokens[1]);
 250
 251                        /* 3rd field: mode - device permissions */
 252                        mode = strtoul(tokens[2], NULL, 8);
 253
 254                        val = tokens[3];
 255                        /* 4th field (opt): >|=alias */
 256
 257                        if (ENABLE_FEATURE_MDEV_RENAME && val) {
 258                                aliaslink = val[0];
 259                                if (aliaslink == '>' || aliaslink == '=') {
 260                                        char *a, *s, *st;
 261                                        char *p;
 262                                        unsigned i, n;
 263
 264                                        a = val;
 265                                        s = strchrnul(val, ' ');
 266                                        st = strchrnul(val, '\t');
 267                                        if (st < s)
 268                                                s = st;
 269                                        val = (s[0] && s[1]) ? s+1 : NULL;
 270                                        s[0] = '\0';
 271
 272                                        if (ENABLE_FEATURE_MDEV_RENAME_REGEXP) {
 273                                                /* substitute %1..9 with off[1..9], if any */
 274                                                n = 0;
 275                                                s = a;
 276                                                while (*s)
 277                                                        if (*s++ == '%')
 278                                                                n++;
 279
 280                                                p = alias = xzalloc(strlen(a) + n * strlen(str_to_match));
 281                                                s = a + 1;
 282                                                while (*s) {
 283                                                        *p = *s;
 284                                                        if ('%' == *s) {
 285                                                                i = (s[1] - '0');
 286                                                                if (i <= 9 && off[i].rm_so >= 0) {
 287                                                                        n = off[i].rm_eo - off[i].rm_so;
 288                                                                        strncpy(p, str_to_match + off[i].rm_so, n);
 289                                                                        p += n - 1;
 290                                                                        s++;
 291                                                                }
 292                                                        }
 293                                                        p++;
 294                                                        s++;
 295                                                }
 296                                        } else {
 297                                                alias = xstrdup(a + 1);
 298                                        }
 299                                }
 300                        }
 301
 302                        if (ENABLE_FEATURE_MDEV_EXEC && val) {
 303                                const char *s = "$@*";
 304                                const char *s2 = strchr(s, val[0]);
 305
 306                                if (!s2) {
 307                                        bb_error_msg("bad line %u", parser->lineno);
 308                                        if (ENABLE_FEATURE_MDEV_RENAME)
 309                                                free(alias);
 310                                        continue;
 311                                }
 312
 313                                /* Are we running this command now?
 314                                 * Run $cmd on delete, @cmd on create, *cmd on both
 315                                 */
 316                                if (s2-s != delete)
 317                                        command = xstrdup(val + 1);
 318                        }
 319                }
 320
 321                /* End of field parsing */
 322
 323                /* "Execute" the line we found */
 324                {
 325                        const char *node_name;
 326
 327                        node_name = device_name;
 328                        if (ENABLE_FEATURE_MDEV_RENAME && alias)
 329                                node_name = alias = build_alias(alias, device_name);
 330
 331                        if (!delete && major >= 0) {
 332                                if (mknod(node_name, mode | type, makedev(major, minor)) && errno != EEXIST)
 333                                        bb_perror_msg_and_die("mknod %s", node_name);
 334                                if (major == root_major && minor == root_minor)
 335                                        symlink(node_name, "root");
 336                                if (ENABLE_FEATURE_MDEV_CONF) {
 337                                        chmod(node_name, mode);
 338                                        chown(node_name, ugid.uid, ugid.gid);
 339                                }
 340                                if (ENABLE_FEATURE_MDEV_RENAME && alias) {
 341                                        if (aliaslink == '>')
 342                                                symlink(node_name, device_name);
 343                                }
 344                        }
 345
 346                        if (ENABLE_FEATURE_MDEV_EXEC && command) {
 347                                /* setenv will leak memory, use putenv/unsetenv/free */
 348                                char *s = xasprintf("%s=%s", "MDEV", node_name);
 349                                char *s1 = xasprintf("%s=%s", "SUBSYSTEM", subsystem);
 350                                putenv(s);
 351                                putenv(s1);
 352                                if (system(command) == -1)
 353                                        bb_perror_msg("can't run '%s'", command);
 354                                unsetenv("SUBSYSTEM");
 355                                free(s1);
 356                                unsetenv("MDEV");
 357                                free(s);
 358                                free(command);
 359                        }
 360
 361                        if (delete) {
 362                                if (ENABLE_FEATURE_MDEV_RENAME && alias) {
 363                                        if (aliaslink == '>')
 364                                                unlink(device_name);
 365                                }
 366                                unlink(node_name);
 367                        }
 368
 369                        if (ENABLE_FEATURE_MDEV_RENAME)
 370                                free(alias);
 371                }
 372
 373                /* We found matching line.
 374                 * Stop unless it was prefixed with '-' */
 375                if (ENABLE_FEATURE_MDEV_CONF && !keep_matching)
 376                        break;
 377
 378        /* end of "while line is read from /etc/mdev.conf" */
 379        } while (ENABLE_FEATURE_MDEV_CONF);
 380
 381        if (ENABLE_FEATURE_MDEV_CONF)
 382                config_close(parser);
 383}
 384
 385/* File callback for /sys/ traversal */
 386static int FAST_FUNC fileAction(const char *fileName,
 387                struct stat *statbuf UNUSED_PARAM,
 388                void *userData,
 389                int depth UNUSED_PARAM)
 390{
 391        size_t len = strlen(fileName) - 4; /* can't underflow */
 392        char *scratch = userData;
 393
 394        /* len check is for paranoid reasons */
 395        if (strcmp(fileName + len, "/dev") != 0 || len >= PATH_MAX)
 396                return FALSE;
 397
 398        strcpy(scratch, fileName);
 399        scratch[len] = '\0';
 400        make_device(scratch, 0);
 401
 402        return TRUE;
 403}
 404
 405/* Directory callback for /sys/ traversal */
 406static int FAST_FUNC dirAction(const char *fileName UNUSED_PARAM,
 407                struct stat *statbuf UNUSED_PARAM,
 408                void *userData UNUSED_PARAM,
 409                int depth)
 410{
 411        /* Extract device subsystem -- the name of the directory
 412         * under /sys/class/ */
 413        if (1 == depth) {
 414                free(subsystem);
 415                subsystem = strrchr(fileName, '/');
 416                if (subsystem)
 417                        subsystem = xstrdup(subsystem + 1);
 418        }
 419
 420        return (depth >= MAX_SYSFS_DEPTH ? SKIP : TRUE);
 421}
 422
 423/* For the full gory details, see linux/Documentation/firmware_class/README
 424 *
 425 * Firmware loading works like this:
 426 * - kernel sets FIRMWARE env var
 427 * - userspace checks /lib/firmware/$FIRMWARE
 428 * - userspace waits for /sys/$DEVPATH/loading to appear
 429 * - userspace writes "1" to /sys/$DEVPATH/loading
 430 * - userspace copies /lib/firmware/$FIRMWARE into /sys/$DEVPATH/data
 431 * - userspace writes "0" (worked) or "-1" (failed) to /sys/$DEVPATH/loading
 432 * - kernel loads firmware into device
 433 */
 434static void load_firmware(const char *firmware, const char *sysfs_path)
 435{
 436        int cnt;
 437        int firmware_fd, loading_fd, data_fd;
 438
 439        /* check for /lib/firmware/$FIRMWARE */
 440        xchdir("/lib/firmware");
 441        firmware_fd = xopen(firmware, O_RDONLY);
 442
 443        /* in case we goto out ... */
 444        data_fd = -1;
 445
 446        /* check for /sys/$DEVPATH/loading ... give 30 seconds to appear */
 447        xchdir(sysfs_path);
 448        for (cnt = 0; cnt < 30; ++cnt) {
 449                loading_fd = open("loading", O_WRONLY);
 450                if (loading_fd != -1)
 451                        goto loading;
 452                sleep(1);
 453        }
 454        goto out;
 455
 456 loading:
 457        /* tell kernel we're loading by "echo 1 > /sys/$DEVPATH/loading" */
 458        if (full_write(loading_fd, "1", 1) != 1)
 459                goto out;
 460
 461        /* load firmware into /sys/$DEVPATH/data */
 462        data_fd = open("data", O_WRONLY);
 463        if (data_fd == -1)
 464                goto out;
 465        cnt = bb_copyfd_eof(firmware_fd, data_fd);
 466
 467        /* tell kernel result by "echo [0|-1] > /sys/$DEVPATH/loading" */
 468        if (cnt > 0)
 469                full_write(loading_fd, "0", 1);
 470        else
 471                full_write(loading_fd, "-1", 2);
 472
 473 out:
 474        if (ENABLE_FEATURE_CLEAN_UP) {
 475                close(firmware_fd);
 476                close(loading_fd);
 477                close(data_fd);
 478        }
 479}
 480
 481int mdev_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
 482int mdev_main(int argc UNUSED_PARAM, char **argv)
 483{
 484        RESERVE_CONFIG_BUFFER(temp, PATH_MAX + SCRATCH_SIZE);
 485
 486        /* We can be called as hotplug helper */
 487        /* Kernel cannot provide suitable stdio fds for us, do it ourself */
 488        bb_sanitize_stdio();
 489
 490        /* Force the configuration file settings exactly */
 491        umask(0);
 492
 493        xchdir("/dev");
 494
 495        if (argv[1] && strcmp(argv[1], "-s") == 0) {
 496                /* Scan:
 497                 * mdev -s
 498                 */
 499                struct stat st;
 500
 501                xstat("/", &st);
 502                root_major = major(st.st_dev);
 503                root_minor = minor(st.st_dev);
 504
 505                /* ACTION_FOLLOWLINKS is needed since in newer kernels
 506                 * /sys/block/loop* (for example) are symlinks to dirs,
 507                 * not real directories.
 508                 * (kernel's CONFIG_SYSFS_DEPRECATED makes them real dirs,
 509                 * but we can't enforce that on users)
 510                 */
 511                if (access("/sys/class/block", F_OK) != 0) {
 512                        /* Scan obsolete /sys/block only if /sys/class/block
 513                         * doesn't exist. Otherwise we'll have dupes.
 514                         * Also, do not complain if it doesn't exist.
 515                         * Some people configure kernel to have no blockdevs.
 516                         */
 517                        recursive_action("/sys/block",
 518                                ACTION_RECURSE | ACTION_FOLLOWLINKS | ACTION_QUIET,
 519                                fileAction, dirAction, temp, 0);
 520                }
 521                recursive_action("/sys/class",
 522                        ACTION_RECURSE | ACTION_FOLLOWLINKS,
 523                        fileAction, dirAction, temp, 0);
 524        } else {
 525                char *fw;
 526                char *seq;
 527                char *action;
 528                char *env_path;
 529
 530                /* Hotplug:
 531                 * env ACTION=... DEVPATH=... SUBSYSTEM=... [SEQNUM=...] mdev
 532                 * ACTION can be "add" or "remove"
 533                 * DEVPATH is like "/block/sda" or "/class/input/mice"
 534                 */
 535                action = getenv("ACTION");
 536                env_path = getenv("DEVPATH");
 537                subsystem = getenv("SUBSYSTEM");
 538                if (!action || !env_path /*|| !subsystem*/)
 539                        bb_show_usage();
 540                fw = getenv("FIRMWARE");
 541
 542                /* If it exists, does /dev/mdev.seq match $SEQNUM?
 543                 * If it does not match, earlier mdev is running
 544                 * in parallel, and we need to wait */
 545                seq = getenv("SEQNUM");
 546                if (seq) {
 547                        int timeout = 2000 / 32; /* 2000 msec */
 548                        do {
 549                                int seqlen;
 550                                char seqbuf[sizeof(int)*3 + 2];
 551
 552                                seqlen = open_read_close("mdev.seq", seqbuf, sizeof(seqbuf-1));
 553                                if (seqlen < 0) {
 554                                        seq = NULL;
 555                                        break;
 556                                }
 557                                seqbuf[seqlen] = '\0';
 558                                if (seqbuf[0] == '\n' /* seed file? */
 559                                 || strcmp(seq, seqbuf) == 0 /* correct idx? */
 560                                ) {
 561                                        break;
 562                                }
 563                                usleep(32*1000);
 564                        } while (--timeout);
 565                }
 566
 567                snprintf(temp, PATH_MAX, "/sys%s", env_path);
 568                if (strcmp(action, "remove") == 0) {
 569                        /* Ignoring "remove firmware". It was reported
 570                         * to happen and to cause erroneous deletion
 571                         * of device nodes. */
 572                        if (!fw)
 573                                make_device(temp, 1);
 574                }
 575                else if (strcmp(action, "add") == 0) {
 576                        make_device(temp, 0);
 577                        if (ENABLE_FEATURE_MDEV_LOAD_FIRMWARE) {
 578                                if (fw)
 579                                        load_firmware(fw, temp);
 580                        }
 581                }
 582
 583                if (seq) {
 584                        xopen_xwrite_close("mdev.seq", utoa(xatou(seq) + 1));
 585                }
 586        }
 587
 588        if (ENABLE_FEATURE_CLEAN_UP)
 589                RELEASE_CONFIG_BUFFER(temp);
 590
 591        return EXIT_SUCCESS;
 592}
 593