blob: 21367ea6ea8e94d891fc6c258bb1511ab88e1c7b [file] [log] [blame]
// Copyright 2020 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "src/storage/fshost/block-device-manager.h"
#include <fidl/fuchsia.device/cpp/markers.h>
#include <fidl/fuchsia.device/cpp/wire.h>
#include <inttypes.h>
#include <lib/fdio/cpp/caller.h>
#include <lib/syslog/cpp/macros.h>
#include <zircon/device/block.h>
#include <zircon/hw/gpt.h>
#include <set>
#include <utility>
#include "lib/fdio/directory.h"
#include "lib/fidl/llcpp/channel.h"
#include "lib/service/llcpp/service.h"
#include "src/lib/storage/fs_management/cpp/format.h"
#include "src/storage/fshost/block-device-interface.h"
#include "src/storage/fshost/constants.h"
#include "src/storage/fshost/copier.h"
#include "zircon/errors.h"
namespace fshost {
namespace {
// Setting for the maximum bytes to allow a partition to grow to.
struct PartitionLimit {
// When unset, this limit will apply only to non-ramdisk devices. See
// Config::kApplyLimitsToRamdisk.
bool apply_to_ramdisk = false;
// Partition max size in bytes, 0 means "no limit".
uint64_t max_bytes = 0;
};
// Splits the path into a directory and the last component.
std::pair<std::string_view, std::string_view> SplitPath(std::string_view path) {
size_t separator = path.rfind('/');
if (separator != std::string::npos) {
return std::make_pair(path.substr(0, separator), path.substr(separator + 1));
}
return std::make_pair(std::string_view(), path);
}
bool IsRamdisk(const BlockDeviceInterface& device) {
constexpr std::string_view kRamdiskPrefix = "/dev/sys/platform/00:00:2d/ramctl/";
return device.topological_path().compare(0, kRamdiskPrefix.length(), kRamdiskPrefix) == 0;
}
// Matches all NAND devices.
class NandMatcher : public BlockDeviceManager::Matcher {
public:
fs_management::DiskFormat Match(const BlockDeviceInterface& device) override {
if (device.IsNand()) {
return fs_management::kDiskFormatNandBroker;
}
return fs_management::kDiskFormatUnknown;
}
zx_status_t Add(BlockDeviceInterface& device) override {
zx_status_t status = device.Add();
if (status != ZX_OK) {
return status;
}
if (path_.empty()) {
path_ = device.topological_path();
}
return ZX_OK;
}
const std::string& path() const { return path_; }
private:
std::string path_;
};
// Matches anything that appears to have the given content and keeps track of the first device it
// finds.
class ContentMatcher : public BlockDeviceManager::Matcher {
public:
// If |allow_multiple| is true, multiple devices will be matched. Otherwise, only the first
// device that appears will match.
ContentMatcher(fs_management::DiskFormat format, bool allow_multiple)
: format_(format), allow_multiple_(allow_multiple) {}
fs_management::DiskFormat Match(const BlockDeviceInterface& device) override {
if (!allow_multiple_ && !path_.empty()) {
// Only match the first occurrence.
return fs_management::kDiskFormatUnknown;
}
if (device.content_format() == format_) {
return format_;
}
return fs_management::kDiskFormatUnknown;
}
zx_status_t Add(BlockDeviceInterface& device) override {
zx_status_t status = device.Add();
if (status != ZX_OK) {
return status;
}
if (path_.empty()) {
path_ = device.topological_path();
}
return ZX_OK;
}
const std::string& path() const { return path_; }
private:
const fs_management::DiskFormat format_;
const bool allow_multiple_;
std::string path_;
};
// Matches devices that handle groups of partitions.
class PartitionMapMatcher : public ContentMatcher {
public:
// |suffix| is a device that is expected to appear when the driver is bound. For example, FVM,
// will add a "/fvm" device before adding children whilst GPT won't add anything. If
// |ramdisk_required| is set, this matcher will only match against a ram-disk.
PartitionMapMatcher(fs_management::DiskFormat format, bool allow_multiple,
std::string_view suffix, bool ramdisk_required)
: ContentMatcher(format, allow_multiple),
suffix_(suffix),
ramdisk_required_(ramdisk_required) {}
bool ramdisk_required() const { return ramdisk_required_; }
fs_management::DiskFormat Match(const BlockDeviceInterface& device) override {
if (ramdisk_required_ && !IsRamdisk(device)) {
return fs_management::kDiskFormatUnknown;
}
return ContentMatcher::Match(device);
}
// Returns true if |device| is a child of the device matched by this matcher.
bool IsChild(const BlockDeviceInterface& device) const {
if (path().empty()) {
return false;
}
// Child partitions should have topological paths of the form:
// .../<suffix>/<partition-name>/block
auto [dir1, base1] = SplitPath(device.topological_path());
if (base1 != "block") {
return false;
}
auto [dir2, base2] = SplitPath(dir1);
// base should be something like <partition-name>-p-1, but we ignore that.
return path() + suffix_ == dir2;
}
private:
const std::string suffix_;
const bool ramdisk_required_;
};
// Extracts the path that the FVM driver responds to FIDL requests at given the PartitionMapMatcher
// for the path.
std::string GetFvmPathForPartitionMap(const PartitionMapMatcher& matcher) {
return matcher.path() + "/fvm";
}
// Matches a partition with a given name and expected type GUID.
class SimpleMatcher : public BlockDeviceManager::Matcher {
public:
SimpleMatcher(PartitionMapMatcher& map, std::string partition_name,
const fuchsia_hardware_block_partition::wire::Guid& type_guid,
fs_management::DiskFormat format, PartitionLimit limit)
: map_(map),
partition_name_(std::move(partition_name)),
type_guid_(type_guid),
format_(format),
limit_(limit) {}
fs_management::DiskFormat Match(const BlockDeviceInterface& device) override {
if (map_.IsChild(device) && device.partition_name() == partition_name_ &&
!memcmp(&device.GetTypeGuid(), &type_guid_, sizeof(type_guid_))) {
return format_;
}
return fs_management::kDiskFormatUnknown;
}
zx_status_t Add(BlockDeviceInterface& device) override {
if (limit_.max_bytes) {
if (limit_.apply_to_ramdisk || !IsRamdisk(device)) {
// Set the max size for this partition in FVM. Ignore failures since the max size is
// mostly a guard rail against bad behavior and we can still function.
auto status =
device.SetPartitionMaxSize(GetFvmPathForPartitionMap(map_), limit_.max_bytes);
ZX_DEBUG_ASSERT(status == ZX_OK);
}
}
return device.Add();
}
private:
const PartitionMapMatcher& map_;
const std::string partition_name_;
const fuchsia_hardware_block_partition::wire::Guid type_guid_;
const fs_management::DiskFormat format_;
const PartitionLimit limit_;
};
constexpr std::string_view kZxcryptSuffix = "/zxcrypt/unsealed/block";
// Matches Fxfs partitions and manages migrations that may need to happen, e.g. removing zxcrypt
// from beneath Fxfs or migrating from a zxcrypt+minfs partition.
class FxfsMatcher : public BlockDeviceManager::Matcher {
public:
using PartitionNames = std::set<std::string, std::less<>>;
FxfsMatcher(const PartitionMapMatcher& map, PartitionNames partition_names,
const fuchsia_hardware_block_partition::wire::Guid& type_guid, PartitionLimit limit,
bool format_on_corruption)
: map_(map),
partition_names_(std::move(partition_names)),
type_guid_(type_guid),
limit_(limit),
format_on_corruption_(format_on_corruption) {}
fs_management::DiskFormat Match(const BlockDeviceInterface& device) override {
bool is_child =
zxcrypt_parent_path_.empty()
? map_.IsChild(device)
: device.topological_path() == zxcrypt_parent_path_ + std::string(kZxcryptSuffix);
if (!is_child || memcmp(&device.GetTypeGuid(), &type_guid_, sizeof(type_guid_)) != 0 ||
partition_names_.find(device.partition_name()) == partition_names_.end()) {
return fs_management::kDiskFormatUnknown;
}
// We don't actually want to mount a zxcrypt-contained data partition, but we need to extract
// any data stored therein (to support paving flows which currently only create zxcrypt+minfs
// partitions). When we find a zxcrypt-formatted data partition, we will bind it, pull the data
// off, and then reformat to Fxfs (without zxcrypt).
if (device.content_format() == fs_management::kDiskFormatZxcrypt) {
if (!zxcrypt_parent_path_.empty()) {
FX_LOGS(WARNING) << "Unexpectedly found nested zxcrypt devices. Not proceeding.";
return fs_management::kDiskFormatUnknown;
}
return fs_management::kDiskFormatZxcrypt;
}
return fs_management::kDiskFormatFxfs;
}
zx_status_t Add(BlockDeviceInterface& device) override {
if (limit_.max_bytes) {
if (limit_.apply_to_ramdisk || !IsRamdisk(device)) {
// Set the max size for this partition in FVM. This is not persisted so we need to set it
// every time on mount. Ignore failures since the max size is mostly a guard rail against
// bad behavior and we can still function.
auto status =
device.SetPartitionMaxSize(GetFvmPathForPartitionMap(map_), limit_.max_bytes);
ZX_DEBUG_ASSERT(status == ZX_OK);
}
}
if (device.GetFormat() == fs_management::kDiskFormatZxcrypt) {
// The channel needs to be cloned before Add is called, since BlockDevice::Add consumes the
// device channel for zxcrypt.
zxcrypt_parent_path_ = device.topological_path();
return device.Add(format_on_corruption_);
}
if (zxcrypt_parent_path_.empty()) {
return device.Add(format_on_corruption_);
}
// Copy the data out of the child device.
FX_LOGS(INFO) << "Copying data out of " << device.topological_path();
auto copier_or = device.ExtractData();
Copier copied_data;
if (copier_or.is_error()) {
FX_LOGS(WARNING) << "Failed to copy data out from old partition: "
<< copier_or.status_string() << ". Reformatting. Expect data loss!";
} else {
copied_data = std::move(*copier_or);
}
// Once we have done so, tear down the zxcrypt device so that we can use it for Fxfs.
FX_LOGS(INFO) << "Shutting down zxcrypt...";
auto controller_or = service::Connect<fuchsia_device::Controller>(zxcrypt_parent_path_.c_str());
if (controller_or.is_error()) {
FX_LOGS(ERROR) << "Failed to connect to zcxrypt: " << controller_or.status_string();
return ZX_ERR_BAD_STATE;
}
auto resp = fidl::WireCall(*controller_or)->UnbindChildren();
zx_status_t status = resp.status();
if (status != ZX_OK) {
FX_LOGS(WARNING) << "Failed to send UnbindChildren: " << zx_status_get_string(status);
return ZX_ERR_BAD_STATE;
}
if (resp->is_error()) {
FX_LOGS(WARNING) << "UnbindChildren failed: " << zx_status_get_string(resp->error_value());
return ZX_ERR_BAD_STATE;
}
FX_LOGS(INFO) << "Shut down zxcrypt. Re-adding device " << zxcrypt_parent_path_;
auto parent_or = device.OpenBlockDevice(zxcrypt_parent_path_.c_str());
if (parent_or.is_error()) {
FX_LOGS(WARNING) << "Failed to open parent: " << parent_or.status_string();
return ZX_ERR_BAD_STATE;
}
zxcrypt_parent_path_.clear();
parent_or->AddData(std::move(copied_data));
parent_or->SetFormat(fs_management::DiskFormat::kDiskFormatFxfs);
return parent_or->Add();
}
private:
const PartitionMapMatcher& map_;
const PartitionNames partition_names_;
const fuchsia_hardware_block_partition::wire::Guid type_guid_;
const PartitionLimit limit_;
const bool format_on_corruption_;
// Set to the topological path of the block device containing zxcrypt once it's been bound.
std::string zxcrypt_parent_path_;
};
// Matches a data partition, which is a mutable filesystem (e.g. minfs) optionally backed by
// zxcrypt.
// Note that Fxfs partitions are matched by FxfsMatcher.
class DataPartitionMatcher : public BlockDeviceManager::Matcher {
public:
using PartitionNames = std::set<std::string, std::less<>>;
enum class ZxcryptVariant {
// A regular data partition backed by zxcrypt.
kNormal,
// A data partition not backed by zxcrypt.
kNoZxcrypt,
// Only attach and unseal the zxcrypt partition; doesn't mount the filesystem.
kZxcryptOnly
};
struct Variant {
ZxcryptVariant zxcrypt = ZxcryptVariant::kNormal;
fs_management::DiskFormat format = fs_management::kDiskFormatMinfs;
bool format_data_on_corruption = true;
};
DataPartitionMatcher(const PartitionMapMatcher& map, PartitionNames partition_names,
std::string_view preferred_name,
const fuchsia_hardware_block_partition::wire::Guid& type_guid,
Variant variant, PartitionLimit limit)
: map_(map),
partition_names_(std::move(partition_names)),
preferred_name_(preferred_name),
type_guid_(type_guid),
variant_(variant),
limit_(limit) {}
static Variant GetVariantFromConfig(const fshost_config::Config& config) {
Variant variant;
if (config.no_zxcrypt()) {
variant.zxcrypt = ZxcryptVariant::kNoZxcrypt;
} else {
variant.zxcrypt = ZxcryptVariant::kNormal;
}
if (!config.data_filesystem_format().empty())
variant.format = fs_management::DiskFormatFromString(config.data_filesystem_format());
variant.format_data_on_corruption = config.format_data_on_corruption();
return variant;
}
fs_management::DiskFormat Match(const BlockDeviceInterface& device) override {
if (expected_inner_path_.empty()) {
if (map_.IsChild(device) && !memcmp(&device.GetTypeGuid(), &type_guid_, sizeof(type_guid_))) {
if (partition_names_.find(device.partition_name()) == partition_names_.end()) {
FX_LOGS(INFO) << "Ignoring data partition with label '" << device.partition_name() << "'";
return fs_management::kDiskFormatUnknown;
}
switch (variant_.zxcrypt) {
case ZxcryptVariant::kNormal:
return map_.ramdisk_required() ? variant_.format : fs_management::kDiskFormatZxcrypt;
case ZxcryptVariant::kNoZxcrypt:
return variant_.format;
case ZxcryptVariant::kZxcryptOnly:
return fs_management::kDiskFormatZxcrypt;
}
}
} else if (variant_.zxcrypt == ZxcryptVariant::kNormal &&
device.topological_path() == expected_inner_path_ &&
!memcmp(&device.GetTypeGuid(), &type_guid_, sizeof(type_guid_))) {
return variant_.format;
}
return fs_management::kDiskFormatUnknown;
}
zx_status_t Add(BlockDeviceInterface& device) override {
if (limit_.max_bytes) {
if (limit_.apply_to_ramdisk || !IsRamdisk(device)) {
// Set the max size for this partition in FVM. This is not persisted so we need to set it
// every time on mount. Ignore failures since the max size is mostly a guard rail against
// bad behavior and we can still function.
auto status =
device.SetPartitionMaxSize(GetFvmPathForPartitionMap(map_), limit_.max_bytes);
ZX_DEBUG_ASSERT(status == ZX_OK);
}
}
if (expected_inner_path_.empty() && !preferred_name_.empty() &&
device.partition_name() != preferred_name_) {
if (zx_status_t status =
device.SetPartitionName(GetFvmPathForPartitionMap(map_), preferred_name_);
status != ZX_OK) {
FX_LOGS(ERROR) << "Failed to change data partition name to '" << preferred_name_
<< "': " << zx_status_get_string(status);
// Continue since not fatal...
} else {
FX_LOGS(INFO) << "Changed data partition name to '" << preferred_name_ << "'";
}
}
// If the volume doesn't appear to be zxcrypt, assume that it's because it was never formatted
// as such, or the keys have been shredded, so skip straight to reformatting. Strictly
// speaking, it's not necessary, because attempting to unseal should trigger the same
// behaviour, but the log messages in that case are scary.
if (device.GetFormat() == fs_management::kDiskFormatZxcrypt) {
if (device.content_format() != fs_management::kDiskFormatZxcrypt) {
FX_LOGS(INFO) << "Formatting as zxcrypt partition";
zx_status_t status = device.FormatZxcrypt();
if (status != ZX_OK) {
return status;
}
// Set the reformat_ flag so that when the Minfs device appears we can skip straight to
// reformatting it (and skip any fsck). Again, this isn't strictly required because
// mounting should fail and we'll reformat, but we can skip that when we know we need to
// reformat.
reformat_ = true;
}
} else if (reformat_) {
// We formatted zxcrypt, so skip straight to formatting the filesystem.
zx_status_t status = device.FormatFilesystem();
if (status != ZX_OK) {
return status;
}
reformat_ = false;
}
zx_status_t status = device.Add(variant_.format_data_on_corruption);
if (status != ZX_OK) {
return status;
}
if (device.GetFormat() == fs_management::kDiskFormatZxcrypt) {
expected_inner_path_ = device.topological_path();
expected_inner_path_.append(kZxcryptSuffix);
}
return ZX_OK;
}
private:
const PartitionMapMatcher& map_;
const PartitionNames partition_names_;
const std::string preferred_name_;
const fuchsia_hardware_block_partition::wire::Guid type_guid_;
const Variant variant_;
const PartitionLimit limit_;
// Once we have matched a zxcrypt partition, this field will be set to the expected topological
// path of the child device, which will then be matched against directly.
std::string expected_inner_path_;
// If we reformat the zxcrypt device, this flag is set so that we know we should reformat the
// minfs device when it appears.
bool reformat_ = false;
};
// Matches the factory partition.
class FactoryfsMatcher : public BlockDeviceManager::Matcher {
public:
static constexpr std::string_view kVerityMutableSuffix = "/verity/mutable/block";
static constexpr std::string_view kVerityVerifiedSuffix = "/verity/verified/block";
explicit FactoryfsMatcher(const PartitionMapMatcher& map) : map_(map) {}
fs_management::DiskFormat Match(const BlockDeviceInterface& device) override {
static constexpr fuchsia_hardware_block_partition::wire::Guid factory_type_guid =
GPT_FACTORY_TYPE_GUID;
if (base_path_.empty()) {
if (map_.IsChild(device) &&
!memcmp(&device.GetTypeGuid(), &factory_type_guid, sizeof(factory_type_guid)) &&
device.partition_name() == "factory") {
return fs_management::kDiskFormatBlockVerity;
}
} else if (!memcmp(&device.GetTypeGuid(), &factory_type_guid, sizeof(factory_type_guid)) &&
(device.topological_path() == std::string(base_path_).append(kVerityMutableSuffix) ||
device.topological_path() ==
std::string(base_path_).append(kVerityVerifiedSuffix))) {
return fs_management::kDiskFormatFactoryfs;
}
return fs_management::kDiskFormatUnknown;
}
zx_status_t Add(BlockDeviceInterface& device) override {
zx_status_t status = device.Add();
if (status != ZX_OK) {
return status;
}
base_path_ = device.topological_path();
return ZX_OK;
}
private:
const PartitionMapMatcher& map_;
std::string base_path_;
};
// Matches devices that report flags with BLOCK_FLAG_BOOTPART set.
class BootpartMatcher : public BlockDeviceManager::Matcher {
public:
fs_management::DiskFormat Match(const BlockDeviceInterface& device) override {
fuchsia_hardware_block_BlockInfo info;
zx_status_t status = device.GetInfo(&info);
if (status != ZX_OK) {
return fs_management::kDiskFormatUnknown;
}
return info.flags & BLOCK_FLAG_BOOTPART ? fs_management::kDiskFormatBootpart
: fs_management::kDiskFormatUnknown;
}
};
DataPartitionMatcher::PartitionNames GetDataPartitionNames(bool include_legacy) {
if (include_legacy) {
return {std::string(kDataPartitionLabel), "minfs", "fuchsia-data"};
}
return {std::string(kDataPartitionLabel)};
}
} // namespace
BlockDeviceManager::BlockDeviceManager(const fshost_config::Config* config) : config_(*config) {
static constexpr fuchsia_hardware_block_partition::wire::Guid data_type_guid = GUID_DATA_VALUE;
if (config_.bootpart()) {
matchers_.push_back(std::make_unique<BootpartMatcher>());
}
if (config_.nand()) {
matchers_.push_back(std::make_unique<NandMatcher>());
}
auto gpt =
std::make_unique<PartitionMapMatcher>(fs_management::kDiskFormatGpt, config_.gpt_all(), "",
/*ramdisk_required=*/false);
auto fvm = std::make_unique<PartitionMapMatcher>(
fs_management::kDiskFormatFvm, /*allow_multiple=*/false, "/fvm", config_.fvm_ramdisk());
bool gpt_required = config_.gpt() || config_.gpt_all();
bool fvm_required = config_.fvm();
// Maximum partition limits. The limits only apply to physical devices (not ramdisks) unless
// apply_limits_to_ramdisk is set.
PartitionLimit blobfs_limit{.apply_to_ramdisk = config_.apply_limits_to_ramdisk(),
.max_bytes = config_.blobfs_max_bytes()};
PartitionLimit data_limit{.apply_to_ramdisk = config_.apply_limits_to_ramdisk(),
.max_bytes = config_.data_max_bytes()};
if (!config_.netboot()) {
// GPT partitions:
if (config_.durable()) {
static constexpr fuchsia_hardware_block_partition::wire::Guid durable_type_guid =
GPT_DURABLE_TYPE_GUID;
matchers_.push_back(std::make_unique<DataPartitionMatcher>(
*gpt, DataPartitionMatcher::PartitionNames{GPT_DURABLE_NAME}, std::string_view(),
durable_type_guid, DataPartitionMatcher::GetVariantFromConfig(config_),
PartitionLimit()));
gpt_required = true;
}
if (config_.factory()) {
matchers_.push_back(std::make_unique<FactoryfsMatcher>(*gpt));
gpt_required = true;
}
// FVM partitions:
if (config_.blobfs()) {
static constexpr fuchsia_hardware_block_partition::wire::Guid blobfs_type_guid =
GUID_BLOB_VALUE;
matchers_.push_back(std::make_unique<SimpleMatcher>(
*fvm, std::string(kBlobfsPartitionLabel), blobfs_type_guid,
fs_management::kDiskFormatBlobfs, blobfs_limit));
fvm_required = true;
}
if (config_.data()) {
if (config_.data_filesystem_format() == "fxfs") {
matchers_.push_back(std::make_unique<FxfsMatcher>(
*fvm, GetDataPartitionNames(config_.allow_legacy_data_partition_names()),
data_type_guid, data_limit, config_.format_data_on_corruption()));
} else {
matchers_.push_back(std::make_unique<DataPartitionMatcher>(
*fvm, GetDataPartitionNames(config_.allow_legacy_data_partition_names()),
kDataPartitionLabel, data_type_guid,
DataPartitionMatcher::GetVariantFromConfig(config_), data_limit));
}
fvm_required = true;
}
}
// The partition map matchers go last because they match on content.
if (fvm_required) {
std::unique_ptr<PartitionMapMatcher> non_ramdisk_fvm;
if (config_.fvm_ramdisk()) {
// Add another matcher for the non-ramdisk version of FVM.
non_ramdisk_fvm = std::make_unique<PartitionMapMatcher>(fs_management::kDiskFormatFvm,
/*allow_multiple=*/false, "/fvm",
/*ramdisk_required=*/false);
if (config_.zxcrypt_non_ramdisk()) {
matchers_.push_back(std::make_unique<DataPartitionMatcher>(
*non_ramdisk_fvm, GetDataPartitionNames(config_.allow_legacy_data_partition_names()),
kDataPartitionLabel, data_type_guid,
DataPartitionMatcher::Variant{.zxcrypt =
DataPartitionMatcher::ZxcryptVariant::kZxcryptOnly},
data_limit));
}
}
matchers_.push_back(std::move(fvm));
if (non_ramdisk_fvm) {
matchers_.push_back(std::move(non_ramdisk_fvm));
}
}
if (gpt_required) {
matchers_.push_back(std::move(gpt));
}
if (config_.mbr()) {
// Default to allowing multiple devices because mbr support is disabled by default and if
// it's enabled, it's likely required for removable devices and so supporting multiple
// devices is probably appropriate.
matchers_.push_back(std::make_unique<PartitionMapMatcher>(fs_management::kDiskFormatMbr,
/*allow_multiple=*/true, "",
/*ramdisk_required=*/false));
}
}
zx_status_t BlockDeviceManager::AddDevice(BlockDeviceInterface& device) {
if (device.topological_path().empty()) {
return ZX_ERR_NOT_SUPPORTED;
}
FX_LOGS(INFO) << "Device " << device.topological_path() << " has content format "
<< fs_management::DiskFormatString(device.content_format());
for (auto& matcher : matchers_) {
fs_management::DiskFormat format = matcher->Match(device);
if (format != fs_management::kDiskFormatUnknown) {
FX_LOGS(INFO) << "Device " << device.topological_path() << " matched format "
<< fs_management::DiskFormatString(format);
device.SetFormat(format);
return matcher->Add(device);
}
}
return ZX_ERR_NOT_SUPPORTED;
}
} // namespace fshost