blob: 037140cde2257a51171440021c060352ca33ea90 [file] [log] [blame] [edit]
/* Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*
* Implements root device discovery via sysfs with optional bells and whistles.
*/
#include "rootdev.h"
#include <ctype.h>
#include <dirent.h>
#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/sysmacros.h>
#include <sys/types.h>
#include <sys/vfs.h>
#include <unistd.h>
#include <linux/btrfs.h>
#include <linux/magic.h>
/*
* Limit prevents endless looping to find slave.
* We currently have at most 2 levels, this allows
* for future growth.
*/
#define MAX_SLAVE_DEPTH 8
static const char *kDefaultSearchPath = "/sys/block";
static const char *kDefaultDevPath = "/dev";
/* Encode the root device structuring here for Chromium OS */
static const char kActiveRoot[] = "/dev/ACTIVE_ROOT";
static const char kRootDev[] = "/dev/ROOT";
static const char kRootA[] = "/dev/ROOT0";
static const char kRootB[] = "/dev/ROOT1";
struct part_config {
const char *name;
int offset;
};
#define CHROMEOS_PRIMARY_PARTITION 3
static const struct part_config kPrimaryPart[] = { { kRootA, 0 },
{ kRootDev, -3 },
{ kRootB, 2 } };
#define CHROMEOS_SECONDARY_PARTITION 5
static const struct part_config kSecondaryPart[] = { { kRootB, 0 },
{ kRootDev, -5 },
{ kRootA, -2 } };
/* The number of entries in a part_config so we could add RootC easily. */
static const int kPartitionEntries = 3;
/* Converts a file of %u:%u -> dev_t. */
static dev_t devt_from_file(const char *file) {
char candidate[10]; /* TODO(wad) system-provided constant? */
ssize_t bytes = 0;
unsigned int major_num = 0;
unsigned int minor_num = 0;
dev_t dev = 0;
int fd = -1;
/* Never hang. Either get the data or return 0. */
fd = open(file, O_NONBLOCK | O_RDONLY);
if (fd < 0)
return 0;
bytes = read(fd, candidate, sizeof(candidate));
close(fd);
/* 0:0 should be considered the minimum size. */
if (bytes < 3)
return 0;
candidate[bytes] = 0;
if (sscanf(candidate, "%u:%u", &major_num, &minor_num) == 2) {
/* candidate's size artificially limits the size of the converted
* %u to safely convert to a signed int. */
dev = makedev(major_num, minor_num);
}
return dev;
}
/* Walks sysfs and recurses into any directory/link that represents
* a block device to find sub-devices (partitions) for dev.
* If dev == 0, the name fo the first device in the directory will be returned.
* Returns the device's name in "name" */
static int match_sysfs_device(char *name, size_t name_len,
const char *basedir, dev_t *dev, int depth) {
int found = -1;
size_t basedir_len;
DIR *dirp = NULL;
struct dirent *entry = NULL;
char *working_path = NULL;
long working_path_size = 0;
if (!name || !name_len || !basedir || !dev) {
warnx("match_sysfs_device: invalid arguments supplied");
return -1;
}
basedir_len = strlen(basedir);
if (!basedir_len) {
warnx("match_sysfs_device: basedir must not be empty");
return -1;
}
errno = 0;
dirp = opendir(basedir);
if (!dirp) {
/* Don't complain if the directory doesn't exist. */
if (errno != ENOENT)
warn("match_sysfs_device:opendir(%s)", basedir);
return found;
}
/* Grab a platform appropriate path to work with.
* Ideally, this won't vary under sys/block. */
working_path_size = pathconf(basedir, _PC_NAME_MAX) + 1;
/* Fallback to PATH_MAX on any pathconf error. */
if (working_path_size < 0)
working_path_size = PATH_MAX;
working_path = malloc(working_path_size);
if (!working_path) {
warn("malloc(dirent)");
closedir(dirp);
return found;
}
/* Allocate a properly sized entry. */
entry = malloc(offsetof(struct dirent, d_name) + working_path_size);
if (!entry) {
warn("malloc(dirent)");
free(working_path);
closedir(dirp);
return found;
}
while ((entry = readdir(dirp))) {
size_t candidate_len = strlen(entry->d_name);
size_t path_len = 0;
dev_t found_devt = 0;
/* Ignore the usual */
if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, ".."))
continue;
/* TODO(wad) determine how to best bubble up this case. */
if (candidate_len > name_len)
continue;
/* Only traverse directories or symlinks (to directories ideally) */
switch (entry->d_type) {
case DT_UNKNOWN:
case DT_DIR:
case DT_LNK:
break;
default:
continue;
}
/* Determine path to block device number */
path_len = snprintf(working_path, working_path_size, "%s/%s/dev",
basedir, entry->d_name);
/* Ignore if truncation occurs. */
if (path_len != candidate_len + basedir_len + 5)
continue;
found_devt = devt_from_file(working_path);
/* *dev == 0 is a wildcard. */
if (!*dev || found_devt == *dev) {
snprintf(name, name_len, "%s", entry->d_name);
*dev = found_devt;
found = 1;
break;
}
/* Prevent infinite recursion on symlink loops by limiting depth. */
if (depth > 5)
break;
/* Recurse one level for devices that may have a matching partition. */
if (major(found_devt) == major(*dev) && minor(*dev) > minor(found_devt)) {
sprintf(working_path, "%s/%s", basedir, entry->d_name);
found = match_sysfs_device(name, name_len, working_path, dev, depth + 1);
if (found > 0)
break;
}
}
free(working_path);
closedir(dirp);
return found;
}
const char *rootdev_get_partition(const char *dst, size_t len) {
const char *end = dst + strnlen(dst, len);
const char *part = end - 1;
if (!len)
return NULL;
if (!isdigit(*part--))
return NULL;
while (part > dst && isdigit(*part)) part--;
part++;
if (part >= end)
return NULL;
return part;
}
void rootdev_strip_partition(char *dst, size_t len) {
char *part = (char *)rootdev_get_partition(dst, len);
if (!part)
return;
/* For devices matching dm-NN, the digits are not a partition. */
if (*(part - 1) == '-')
return;
/* For devices that end with a digit, the kernel uses a 'p'
* as a separator. E.g., mmcblk1p2 and loop0p1, but not loop0. */
if (*(part - 1) == 'p') {
part--;
if (strncmp(dst, "loop", 4) == 0 && dst + 3 == part)
return;
}
*part = '\0';
}
int rootdev_symlink_active(const char *path) {
int ret = 0;
/* Don't overwrite an existing link. */
errno = 0;
if ((symlink(path, kActiveRoot)) && errno != EEXIST) {
warn("failed to symlink %s -> %s", kActiveRoot, path);
ret = -1;
}
return ret;
}
int rootdev_get_device(char *dst, size_t size, dev_t dev,
const char *search) {
struct stat active_root_statbuf;
if (search == NULL)
search = kDefaultSearchPath;
/* Check if the -s symlink exists. */
if ((stat(kActiveRoot, &active_root_statbuf) == 0) &&
active_root_statbuf.st_rdev == dev) {
/* Note, if the link is not fully qualified, this won't be
* either. */
ssize_t len = readlink(kActiveRoot, dst, PATH_MAX);
if (len > 0) {
dst[len] = 0;
return 0;
}
/* If readlink fails or is empty, fall through */
}
snprintf(dst, size, "%s", search);
if (match_sysfs_device(dst, size, dst, &dev, 0) <= 0) {
fprintf (stderr, "unable to find match\n");
return 1;
}
return 0;
}
/*
* rootdev_get_device_slave returns results in slave which
* may be the original device or the name of the slave.
*
* Because slave and device may point to the same data,
* must be careful how they are handled because slave
* is modified (can't use snprintf).
*/
void rootdev_get_device_slave(char *slave, size_t size, dev_t *dev,
const char *device, const char *search) {
char dst[PATH_MAX];
int len = 0;
int i;
if (search == NULL)
search = kDefaultSearchPath;
/*
* With stacked device mappers, we have to chain through all the levels
* and find the last device. For example, verity can be stacked on bootcache
* that is stacked on a disk partition.
*/
if (slave != device)
strncpy(slave, device, size);
slave[size - 1] = '\0';
for (i = 0; i < MAX_SLAVE_DEPTH; i++) {
len = snprintf(dst, sizeof(dst), "%s/%s/slaves", search, slave);
if (len != strlen(device) + strlen(search) + 8) {
warnx("rootdev_get_device_slave: device name too long");
return;
}
*dev = 0;
if (match_sysfs_device(slave, size, dst, dev, 0) <= 0) {
return;
}
}
warnx("slave depth greater than %d at %s", i, slave);
}
int rootdev_create_devices(const char *name, dev_t dev, bool symlink) {
int ret = 0;
unsigned int major_num = major(dev);
unsigned int minor_num = minor(dev);
int i;
const struct part_config *config;
const char *part_s = rootdev_get_partition(name, strlen(name));
if (part_s == NULL) {
warnx("create_devices: unable to determine partition");
return -1;
}
switch (atoi(part_s)) {
case CHROMEOS_PRIMARY_PARTITION:
config = kPrimaryPart;
break;
case CHROMEOS_SECONDARY_PARTITION:
config = kSecondaryPart;
break;
default:
warnx("create_devices: unable to determine partition: %s",
part_s);
return -1;
}
for (i = 0; i < kPartitionEntries; ++i) {
dev = makedev(major_num, minor_num + config[i].offset);
errno = 0;
if (mknod(config[i].name,
S_IFBLK | S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH,
dev) && errno != EEXIST) {
warn("failed to create %s", config[i].name);
return -1;
}
}
if (symlink)
ret = rootdev_symlink_active(config[0].name);
return ret;
}
int rootdev_get_path(char *path, size_t size, const char *device,
dev_t dev, const char *dev_path) {
int path_len;
struct stat dev_statbuf;
if (!dev_path)
dev_path = kDefaultDevPath;
if (!path || !size || !device)
return -1;
path_len = snprintf(path, size, "%s/%s", dev_path, device);
if (path_len != strlen(dev_path) + 1 + strlen(device))
return -1;
if (stat(path, &dev_statbuf) != 0)
return 1;
if (dev && dev != dev_statbuf.st_rdev)
return 2;
return 0;
}
static int get_rootdev_btrfs(char *path, size_t size, const char *mount_path) {
const char *di_path;
/* Open the mount point path */
int fd = open(mount_path, O_RDONLY | O_CLOEXEC);
if (fd == -1) {
return -1;
}
/* Create space to hold the ioctl dev info. */
struct btrfs_ioctl_dev_info_args di_args;
/* Since we use always use single device in chromebook rootfs,
* the device id for this device is always 1. */
di_args.devid = 1;
/* Read the ioctl device info (btrfs). */
if (ioctl(fd, BTRFS_IOC_DEV_INFO, &di_args) != 0) {
close(fd);
return -1;
}
close(fd);
di_path = (const char *)di_args.path;
/* Try to access the device at di_args->path to verify its existence. */
if (access((char *)di_path, F_OK) != 0) {
return -1;
}
if (strlen(di_path) >= size)
return -1;
strcpy(path, di_path);
return 0;
}
int rootdev_wrapper(char *path, size_t size,
bool full, bool strip,
dev_t *dev,
const char *mount_path,
const char *search, const char *dev_path) {
int res = 0;
char devname[PATH_MAX];
struct statfs mount_statfs;
if (!search)
search = kDefaultSearchPath;
if (!dev_path)
dev_path = kDefaultDevPath;
if (!dev)
return -1;
if (statfs(mount_path, &mount_statfs) == 0) {
if (mount_statfs.f_type == BTRFS_SUPER_MAGIC) {
/* BTRFS uses virtual device id which is different from actual
* device id. So we zero-out dev_t indicating that dev_t is not
* a valid device id. */
*dev = 0;
return get_rootdev_btrfs(path, size, mount_path);
}
}
res = rootdev_get_device(devname, sizeof(devname), *dev, search);
if (res != 0)
return res;
if (full)
rootdev_get_device_slave(devname, sizeof(devname), dev, devname,
search);
/* TODO(wad) we should really just track the block dev, partition number, and
* dev path. When we rewrite this, we can track all the sysfs info
* in the class. */
if (strip) {
/* When we strip the partition, we don't want get_path to return non-zero
* because of dev mismatch. Passing in 0 tells it to not test. */
*dev = 0;
rootdev_strip_partition(devname, size);
}
res = rootdev_get_path(path, size, devname, *dev, dev_path);
return res;
}
int rootdev(char *path, size_t size, bool full, bool strip) {
struct stat root_statbuf;
dev_t _root_dev, *root_dev = &_root_dev;
/* Yields the containing dev_t in st_dev. */
if (stat("/", &root_statbuf) != 0)
return -1;
/* Some ABIs (like mips o32) are broken and the st_dev field isn't actually
* a dev_t. In that case, pass a pointer to a local dev_t who we took care
* of truncating the value into. On sane arches, gcc can optimize this to
* the same code, so should only be a penalty when the ABI is broken. */
if (sizeof(root_statbuf.st_dev) == sizeof(*root_dev)) {
/* Cast is OK since we verified size here. */
root_dev = (dev_t *)&root_statbuf.st_dev;
} else {
*root_dev = root_statbuf.st_dev;
}
return rootdev_wrapper(path,
size,
full,
strip,
root_dev,
"/",
NULL, /* default /sys dir */
NULL); /* default /dev dir */
}