| /* |
| * Copyright (C) 2010 The Chromium OS Authors <chromium-os-dev@chromium.org> |
| * All Rights Reserved. |
| * |
| * This file is released under the GPL. |
| */ |
| /* |
| * Implements a Chrome OS platform specific error handler. |
| * When verity detects an invalid block, this error handling will |
| * attempt to corrupt the kernel boot image. On reboot, the bios will |
| * detect the kernel corruption and switch to the alternate kernel |
| * and root file system partitions. |
| * |
| * Assumptions: |
| * 1. Partitions are specified on the command line using uuid. |
| * 2. The kernel partition is the partition number is one less |
| * than the root partition number. |
| */ |
| #include <linux/bio.h> |
| #include <linux/blkdev.h> |
| #include <linux/chromeos_platform.h> |
| #include <linux/device.h> |
| #include <linux/device-mapper.h> |
| #include <linux/err.h> |
| #include <linux/genhd.h> |
| #include <linux/kernel.h> |
| #include <linux/module.h> |
| #include <linux/mount.h> |
| #include <linux/notifier.h> |
| #include <linux/string.h> |
| #include <asm/page.h> |
| |
| #include "dm-verity.h" |
| |
| #define DM_MSG_PREFIX "verity-chromeos" |
| |
| static void chromeos_invalidate_kernel_endio(struct bio *bio, int err) |
| { |
| const char *mode = ((bio->bi_rw & REQ_WRITE) ? "write" : "read"); |
| if (err) |
| chromeos_set_need_recovery(); |
| |
| if (bio_flagged(bio, BIO_EOPNOTSUPP)) { |
| DMERR("invalidate_kernel: %s not supported", mode); |
| chromeos_set_need_recovery(); |
| } else if (!bio_flagged(bio, BIO_UPTODATE)) { |
| DMERR("invalidate_kernel: %s not up to date", mode); |
| chromeos_set_need_recovery(); |
| } else { |
| DMERR("invalidate_kernel: partition header %s completed", mode); |
| } |
| |
| complete(bio->bi_private); |
| } |
| |
| static int chromeos_invalidate_kernel_submit(struct bio *bio, |
| struct block_device *bdev, |
| int rw, struct page *page) |
| { |
| DECLARE_COMPLETION_ONSTACK(wait); |
| |
| bio->bi_private = &wait; |
| bio->bi_end_io = chromeos_invalidate_kernel_endio; |
| bio->bi_bdev = bdev; |
| |
| bio->bi_sector = 0; |
| bio->bi_vcnt = 1; |
| bio->bi_idx = 0; |
| bio->bi_size = 512; |
| bio->bi_rw = rw; |
| bio->bi_io_vec[0].bv_page = page; |
| bio->bi_io_vec[0].bv_len = 512; |
| bio->bi_io_vec[0].bv_offset = 0; |
| |
| submit_bio(rw, bio); |
| /* Wait up to 2 seconds for completion or fail. */ |
| if (!wait_for_completion_timeout(&wait, msecs_to_jiffies(2000))) |
| return -1; |
| return 0; |
| } |
| |
| static char *get_info_from_cmdline(const char *key) |
| { |
| const char *dev; |
| const char *end; |
| int len; |
| |
| dev = strstr(saved_command_line, key); |
| if (!dev) |
| return NULL; |
| len = strlen(key); |
| dev = &dev[len]; |
| end = strchr(dev, ' '); |
| return kstrndup(dev, end - dev, GFP_ATOMIC); |
| } |
| |
| /** |
| * match_dev_by_uuid - callback for finding a partition using its uuid |
| * @dev: device passed in by the caller |
| * @uuid_data: opaque pointer to a uuid packed by part_pack_uuid(). |
| * |
| * Returns 1 if the device matches, and 0 otherwise. |
| */ |
| static int match_dev_by_uuid(struct device *dev, void *uuid_data) |
| { |
| u8 *uuid = uuid_data; |
| struct hd_struct *part = dev_to_part(dev); |
| |
| if (!part->info) |
| goto no_match; |
| |
| if (memcmp(uuid, part->info->uuid, sizeof(part->info->uuid))) |
| goto no_match; |
| |
| return 1; |
| no_match: |
| return 0; |
| } |
| |
| static dev_t get_boot_dev_from_root_dev(struct block_device *root_bdev) |
| { |
| /* Very basic sanity checking. This should be better. */ |
| if (!root_bdev || !root_bdev->bd_part || |
| MAJOR(root_bdev->bd_dev) == 254 || |
| root_bdev->bd_part->partno <= 1) { |
| return 0; |
| } |
| return MKDEV(MAJOR(root_bdev->bd_dev), MINOR(root_bdev->bd_dev) - 1); |
| } |
| |
| /* get_boot_dev is bassed on dm_get_device_by_uuid in dm_bootcache. */ |
| static dev_t get_boot_dev(void) |
| { |
| const char partuuid[] = "PARTUUID="; |
| char *uuid_str; |
| struct device *dev = NULL; |
| dev_t devt = 0; |
| u8 uuid[16]; |
| size_t uuid_length; |
| |
| uuid_str = get_info_from_cmdline(" kern_guid="); |
| if (!uuid_str) { |
| DMERR("Couldn't get uuid, try root dev"); |
| return 0; |
| } |
| uuid_length = strlen(uuid_str); |
| if (strncmp(uuid_str, partuuid, strlen(partuuid)) == 0) { |
| devt = name_to_dev_t(uuid_str); |
| } else { |
| if (uuid_length != 36) |
| goto bad_uuid; |
| /* Pack the requested UUID in the expected format. */ |
| part_pack_uuid(uuid_str, uuid); |
| |
| dev = class_find_device(&block_class, NULL, uuid, |
| &match_dev_by_uuid); |
| if (!dev) |
| goto found_nothing; |
| |
| devt = dev->devt; |
| put_device(dev); |
| } |
| return devt; |
| |
| bad_uuid: |
| kfree(uuid_str); |
| DMDEBUG("Supplied value '%s' is an invalid UUID", uuid_str); |
| return 0; |
| found_nothing: |
| kfree(uuid_str); |
| DMDEBUG("No matching partition for GUID: %s", uuid_str); |
| return 0; |
| } |
| |
| /* Replaces the first 8 bytes of a partition with DMVERROR */ |
| static int chromeos_invalidate_kernel(struct block_device *root_bdev) |
| { |
| int ret = 0; |
| struct block_device *bdev; |
| struct bio *bio; |
| struct page *page; |
| dev_t devt; |
| fmode_t dev_mode; |
| /* Ensure we do synchronous unblocked I/O. We may also need |
| * sync_bdev() on completion, but it really shouldn't. |
| */ |
| int rw = REQ_SYNC | REQ_SOFTBARRIER | REQ_NOIDLE; |
| |
| devt = get_boot_dev_from_root_dev(root_bdev); |
| if (!devt) { |
| devt = get_boot_dev(); |
| if (!devt) |
| return -EINVAL; |
| } |
| |
| /* First we open the device for reading. */ |
| dev_mode = FMODE_READ | FMODE_EXCL; |
| bdev = blkdev_get_by_dev(devt, dev_mode, chromeos_invalidate_kernel); |
| if (IS_ERR(bdev)) { |
| DMERR("invalidate_kernel: could not open device for reading"); |
| dev_mode = 0; |
| ret = -1; |
| goto failed_to_read; |
| } |
| |
| bio = bio_alloc(GFP_NOIO, 1); |
| if (!bio) { |
| ret = -1; |
| goto failed_bio_alloc; |
| } |
| |
| page = alloc_page(GFP_NOIO); |
| if (!page) { |
| ret = -ENOMEM; |
| goto failed_to_alloc_page; |
| } |
| |
| if (chromeos_invalidate_kernel_submit(bio, bdev, rw, page)) { |
| ret = -1; |
| goto failed_to_submit_read; |
| } |
| |
| /* We have a page. Let's make sure it looks right. */ |
| if (memcmp("CHROMEOS", page_address(page), 8)) { |
| DMERR("invalidate_kernel called on non-kernel partition"); |
| ret = -EINVAL; |
| goto invalid_header; |
| } else { |
| DMERR("invalidate_kernel: found CHROMEOS kernel partition"); |
| } |
| |
| /* Stamp it and rewrite */ |
| memcpy(page_address(page), "DMVERROR", 8); |
| |
| /* The block dev was being changed on read. Let's reopen here. */ |
| blkdev_put(bdev, dev_mode); |
| dev_mode = FMODE_WRITE | FMODE_EXCL; |
| bdev = blkdev_get_by_dev(devt, dev_mode, chromeos_invalidate_kernel); |
| if (IS_ERR(bdev)) { |
| DMERR("invalidate_kernel: could not open device for reading"); |
| dev_mode = 0; |
| ret = -1; |
| goto failed_to_write; |
| } |
| |
| rw |= REQ_WRITE; |
| if (chromeos_invalidate_kernel_submit(bio, bdev, rw, page)) { |
| ret = -1; |
| goto failed_to_submit_write; |
| } |
| |
| DMERR("invalidate_kernel: completed."); |
| ret = 0; |
| failed_to_submit_write: |
| failed_to_write: |
| invalid_header: |
| __free_page(page); |
| failed_to_submit_read: |
| /* Technically, we'll leak a page with the pending bio, but |
| * we're about to panic so it's safer to do the panic() we expect. |
| */ |
| failed_to_alloc_page: |
| bio_put(bio); |
| failed_bio_alloc: |
| if (dev_mode) |
| blkdev_put(bdev, dev_mode); |
| failed_to_read: |
| return ret; |
| } |
| |
| static int error_handler(struct notifier_block *nb, unsigned long transient, |
| void *opaque_err) |
| { |
| struct dm_verity_error_state *err = |
| (struct dm_verity_error_state *) opaque_err; |
| err->behavior = DM_VERITY_ERROR_BEHAVIOR_PANIC; |
| if (transient) |
| return 0; |
| |
| /* TODO(wad) Implement phase 2: |
| * - Attempt to read the dev_status_offset from the hash dev. |
| * - If the status offset is 0, replace the first byte of the sector |
| * with 01 and panic(). |
| * - If the status offset is not 0, invalidate the associated kernel |
| * partition, then reboot. |
| * - make user space tools clear the last sector |
| */ |
| if (chromeos_invalidate_kernel(err->dev)) |
| chromeos_set_need_recovery(); |
| return 0; |
| } |
| |
| static struct notifier_block chromeos_nb = { |
| .notifier_call = &error_handler, |
| .next = NULL, |
| .priority = 1, |
| }; |
| |
| static int __init dm_verity_chromeos_init(void) |
| { |
| int r; |
| |
| r = dm_verity_register_error_notifier(&chromeos_nb); |
| if (r < 0) |
| DMERR("failed to register handler: %d", r); |
| else |
| DMINFO("dm-verity-chromeos registered"); |
| return r; |
| } |
| |
| static void __exit dm_verity_chromeos_exit(void) |
| { |
| dm_verity_unregister_error_notifier(&chromeos_nb); |
| } |
| |
| module_init(dm_verity_chromeos_init); |
| module_exit(dm_verity_chromeos_exit); |
| |
| MODULE_AUTHOR("Will Drewry <wad@chromium.org>"); |
| MODULE_DESCRIPTION("chromeos-specific error handler for dm-verity"); |
| MODULE_LICENSE("GPL"); |