| // Copyright 2021 The ChromiumOS Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <memory> |
| #include <optional> |
| #include <string> |
| #include <unordered_map> |
| #include <utility> |
| |
| #include <base/files/file.h> |
| #include <base/files/file_enumerator.h> |
| #include <base/files/file_path.h> |
| #include <base/files/file_util.h> |
| #include <base/files/scoped_file.h> |
| #include <base/files/scoped_temp_dir.h> |
| #include <base/json/json_reader.h> |
| #include <base/logging.h> |
| #include <base/strings/string_number_conversions.h> |
| #include <base/strings/stringprintf.h> |
| #include <build/build_config.h> |
| #include <build/buildflag.h> |
| #include <featured/proto_bindings/featured.pb.h> |
| |
| #include "featured/service.h" |
| |
| namespace featured { |
| |
| namespace { |
| constexpr char kPlatformFeaturesPath[] = "/etc/featured/platform-features.json"; |
| constexpr char kSessionStartedState[] = "started"; |
| |
| // Allow featured do write operations to path with these prefixes only. |
| std::vector<std::string> allowedPrefixes = {"/proc", "/sys", |
| "/run/featured_test"}; |
| |
| bool CheckPathPrefix(const base::FilePath& path) { |
| bool valid = false; |
| |
| for (std::string& prefix_str : allowedPrefixes) { |
| if (base::FilePath(prefix_str).IsParent(path)) { |
| valid = true; |
| break; |
| } |
| } |
| |
| return valid; |
| } |
| } // namespace |
| |
| WriteFileCommand::WriteFileCommand(const std::string& file_name, |
| const std::string& value) |
| : FeatureCommand("WriteFile") { |
| file_name_ = file_name; |
| value_ = value; |
| } |
| |
| bool WriteFileCommand::Execute() { |
| base::FilePath fpath = base::FilePath(file_name_); |
| if (!CheckPathPrefix(fpath)) { |
| PLOG(ERROR) << "Unable to write to path, prefix check fail: " << file_name_; |
| PLOG(ERROR) << "Make sure to update the prefix list in sources and the " |
| "selinux config."; |
| return false; |
| } |
| |
| if (!prefix_.empty()) { |
| // Remove the leading "/" in order to be able to append. |
| CHECK(file_name_.starts_with('/')); |
| fpath = prefix_.Append(file_name_.substr(1)); |
| } |
| |
| if (!base::WriteFile(fpath, value_)) { |
| PLOG(ERROR) << "Unable to write to " << file_name_; |
| return false; |
| } |
| return true; |
| } |
| |
| MkdirCommand::MkdirCommand(const std::string& path) : FeatureCommand("Mkdir") { |
| path_ = base::FilePath(path); |
| } |
| |
| bool MkdirCommand::Execute() { |
| if (!CheckPathPrefix(path_)) { |
| PLOG(ERROR) << "Unable to create directory, prefix check fail: " << path_; |
| return false; |
| } |
| |
| base::FilePath path = path_; |
| if (!prefix_.empty()) { |
| // Remove the leading "/" in order to be able to append. |
| CHECK(path_.value().starts_with('/')); |
| path = prefix_.Append(path_.value().substr(1)); |
| } |
| |
| if (!base::CreateDirectory(path)) { |
| PLOG(ERROR) << "Unable to create directory: " << path; |
| return false; |
| } |
| return true; |
| } |
| |
| FileExistsCommand::FileExistsCommand(const std::string& file_name) |
| : SupportCheckCommand("FileExists") { |
| file_name_ = file_name; |
| } |
| |
| bool FileExistsCommand::IsSupported() { |
| return base::PathExists(base::FilePath(file_name_)); |
| } |
| |
| FileNotExistsCommand::FileNotExistsCommand(const std::string& file_name) |
| : SupportCheckCommand("FileNotExists") { |
| file_name_ = file_name; |
| } |
| |
| bool FileNotExistsCommand::IsSupported() { |
| return !base::PathExists(base::FilePath(file_name_)); |
| } |
| |
| bool PlatformFeature::Execute() const { |
| for (auto& cmd : exec_cmds_) { |
| if (!cmd->Execute()) { |
| LOG(ERROR) << "Failed to execute command: " << cmd->name(); |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| bool PlatformFeature::IsSupported() const { |
| for (auto& cmd : support_check_cmds_) { |
| if (!cmd->IsSupported()) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| std::vector<std::string> PlatformFeature::ExecCommandNamesForTesting() const { |
| std::vector<std::string> out; |
| for (const auto& cmd : exec_cmds_) { |
| out.push_back(cmd->name()); |
| } |
| return out; |
| } |
| |
| std::vector<std::string> PlatformFeature::SupportCheckCommandNamesForTesting() |
| const { |
| std::vector<std::string> out; |
| for (const auto& cmd : support_check_cmds_) { |
| out.push_back(cmd->name()); |
| } |
| return out; |
| } |
| |
| bool JsonFeatureParser::ParseFileContents(const std::string& file_contents) { |
| if (features_parsed_) |
| return true; |
| |
| VLOG(1) << "JSON file contents: " << file_contents; |
| |
| auto root = base::JSONReader::ReadAndReturnValueWithError(file_contents); |
| if (!root.has_value()) { |
| LOG(ERROR) << "Failed to parse conf file: " << kPlatformFeaturesPath; |
| return false; |
| } |
| |
| if (!root->is_list() || root->GetList().size() == 0) { |
| LOG(ERROR) << "features list should be non-zero size!"; |
| return false; |
| } |
| |
| for (const auto& item : root->GetList()) { |
| if (!item.is_dict()) { |
| LOG(ERROR) << "features conf not list of dicts!"; |
| return false; |
| } |
| |
| auto feature_obj_optional = MakeFeatureObject(item.GetDict()); |
| if (!feature_obj_optional) { |
| return false; |
| } |
| PlatformFeature feature_obj(std::move(*feature_obj_optional)); |
| |
| auto got = feature_map_.find(feature_obj.name()); |
| if (got != feature_map_.end()) { |
| LOG(ERROR) << "Duplicate feature name found: " << feature_obj.name(); |
| return false; |
| } |
| |
| feature_map_.insert( |
| std::make_pair(feature_obj.name(), std::move(feature_obj))); |
| } |
| |
| features_parsed_ = true; |
| return true; |
| } |
| |
| // PlatformFeature implementation (collect and execute commands). |
| std::optional<PlatformFeature> JsonFeatureParser::MakeFeatureObject( |
| const base::Value::Dict& feature_obj) { |
| const std::string* feat_name = feature_obj.FindString("name"); |
| if (!feat_name) { |
| LOG(ERROR) << "features conf contains empty names"; |
| return std::nullopt; |
| } |
| |
| // Commands for querying if device is supported |
| const base::Value* support_cmd_obj = |
| feature_obj.Find("support_check_commands"); |
| |
| std::vector<std::unique_ptr<SupportCheckCommand>> query_cmds; |
| if (!support_cmd_obj) { |
| // Feature is assumed to be always supported, such as a kernel parameter |
| // that is on all device kernels. |
| query_cmds.push_back(std::make_unique<AlwaysSupportedCommand>()); |
| } else { |
| // Verify that the supplied value is actually a list. |
| const base::Value::List* support_cmd_list_obj = |
| support_cmd_obj->GetIfList(); |
| if (!support_cmd_list_obj || support_cmd_list_obj->size() == 0) { |
| LOG(ERROR) << "Invalid format for support_check_commands commands"; |
| return std::nullopt; |
| } |
| |
| // A support check command was provided, add it to the feature object. |
| for (const auto& item : *support_cmd_list_obj) { |
| if (!item.is_dict()) { |
| LOG(ERROR) << "support_check_commands is not list of dicts."; |
| return std::nullopt; |
| } |
| |
| const std::string* cmd_name = item.GetDict().FindString("name"); |
| |
| if (!cmd_name) { |
| LOG(ERROR) << "Invalid/Empty command name in features config."; |
| return std::nullopt; |
| } |
| |
| if (*cmd_name == "FileExists" || *cmd_name == "FileNotExists") { |
| const std::string* file_name = item.GetDict().FindString("path"); |
| |
| VLOG(1) << "featured: command is " << *cmd_name; |
| if (!file_name) { |
| LOG(ERROR) << "JSON contains invalid path!"; |
| return std::nullopt; |
| } |
| |
| std::unique_ptr<SupportCheckCommand> cmd; |
| if (*cmd_name == "FileExists") { |
| cmd = std::make_unique<FileExistsCommand>(*file_name); |
| } else { |
| cmd = std::make_unique<FileNotExistsCommand>(*file_name); |
| } |
| |
| query_cmds.push_back(std::move(cmd)); |
| } else { |
| LOG(ERROR) << "Invalid support command name in features config: " |
| << *cmd_name; |
| return std::nullopt; |
| } |
| } |
| } |
| |
| // Commands to execute to enable feature |
| const base::Value::List* cmd_list_obj = feature_obj.FindList("commands"); |
| if (!cmd_list_obj || cmd_list_obj->size() == 0) { |
| LOG(ERROR) << "Failed to get commands list in feature."; |
| return std::nullopt; |
| } |
| |
| std::vector<std::unique_ptr<FeatureCommand>> feature_cmds; |
| for (const auto& item : *cmd_list_obj) { |
| if (!item.is_dict()) { |
| LOG(ERROR) << "Invalid command in features config."; |
| return std::nullopt; |
| } |
| const std::string* cmd_name = item.GetDict().FindString("name"); |
| |
| if (!cmd_name) { |
| LOG(ERROR) << "Invalid command in features config."; |
| return std::nullopt; |
| } |
| |
| if (*cmd_name == "WriteFile") { |
| const std::string* file_name = item.GetDict().FindString("path"); |
| |
| VLOG(1) << "featured: command is WriteFile"; |
| if (!file_name) { |
| LOG(ERROR) << "JSON contains invalid path!"; |
| return std::nullopt; |
| } |
| |
| const std::string* value = item.GetDict().FindString("value"); |
| if (!value) { |
| LOG(ERROR) << "JSON contains invalid command value!"; |
| return std::nullopt; |
| } |
| feature_cmds.push_back( |
| std::make_unique<WriteFileCommand>(*file_name, *value)); |
| } else if (*cmd_name == "Mkdir") { |
| const std::string* file_name = item.GetDict().FindString("path"); |
| |
| VLOG(1) << "featured: command is Mkdir"; |
| if (!file_name) { |
| LOG(ERROR) << "JSON contains invalid path!"; |
| return std::nullopt; |
| } |
| |
| feature_cmds.push_back(std::make_unique<MkdirCommand>(*file_name)); |
| } else { |
| LOG(ERROR) << "Invalid command name in features config: " << *cmd_name; |
| return std::nullopt; |
| } |
| } |
| |
| return PlatformFeature(*feat_name, std::move(query_cmds), |
| std::move(feature_cmds)); |
| } |
| |
| bool DbusFeaturedService::ParseFeatureList() { |
| if (parser_->AreFeaturesParsed()) |
| return true; |
| |
| std::string file_contents; |
| if (!ReadFileToString(base::FilePath(kPlatformFeaturesPath), |
| &file_contents)) { |
| PLOG(ERROR) << "Failed to read conf file: " << kPlatformFeaturesPath; |
| return false; |
| } |
| |
| return parser_->ParseFileContents(file_contents); |
| } |
| |
| bool DbusFeaturedService::EnableFeatures() { |
| if (!ParseFeatureList()) { |
| return false; |
| } |
| for (const auto& it : *(parser_->GetFeatureMap())) { |
| if (it.second.IsSupported() && |
| feature::PlatformFeatures::Get()->IsEnabledBlocking( |
| *it.second.feature())) { |
| it.second.Execute(); |
| } |
| } |
| return true; |
| } |
| |
| void DbusFeaturedService::OnSessionStateChanged(const std::string& state) { |
| if (state == kSessionStartedState && !evaluated_platform_features_json_) { |
| if (!EnableFeatures()) { |
| LOG(ERROR) << "failed to enable features"; |
| return; |
| } |
| evaluated_platform_features_json_ = true; |
| } |
| } |
| |
| void OnSignalConnected(const std::string& interface, |
| const std::string& signal, |
| bool success) { |
| if (!success) { |
| LOG(ERROR) << "Could not connect to signal " << signal << " on interface " |
| << interface; |
| } |
| } |
| |
| // Compare two SeedDetails protos for equality. |
| bool SeedsEqual(const SeedDetails& a, const SeedDetails& b) { |
| if (a.compressed_data() != b.compressed_data()) { |
| return false; |
| } |
| if (a.locale() != b.locale()) { |
| return false; |
| } |
| if (a.milestone() != b.milestone()) { |
| return false; |
| } |
| if (a.permanent_consistency_country() != b.permanent_consistency_country()) { |
| return false; |
| } |
| if (a.session_consistency_country() != b.session_consistency_country()) { |
| return false; |
| } |
| if (a.signature() != b.signature()) { |
| return false; |
| } |
| if (a.date() != b.date()) { |
| return false; |
| } |
| if (a.fetch_time() != b.fetch_time()) { |
| return false; |
| } |
| return true; |
| } |
| |
| void DbusFeaturedService::HandleSeedFetched( |
| dbus::MethodCall* method_call, |
| dbus::ExportedObject::ResponseSender sender) { |
| std::unique_ptr<dbus::Response> response; |
| |
| dbus::MessageReader reader(method_call); |
| SeedDetails seed; |
| if (!reader.PopArrayOfBytesAsProto(&seed)) { |
| LOG(ERROR) << "Missing seed argument to HandleSeedFetched"; |
| response = dbus::ErrorResponse::FromMethodCall( |
| method_call, DBUS_ERROR_INVALID_ARGS, "Could not parse seed argument"); |
| std::move(sender).Run(std::move(response)); |
| return; |
| } |
| |
| SeedDetails used_seed; |
| if (tmp_storage_) { |
| used_seed = tmp_storage_->GetUsedSeedDetails(); |
| } |
| // Fall back to assuming null-seed was used if we cannot determine which seed |
| // was used. |
| if (!SeedsEqual(used_seed, seed)) { |
| LOG(WARNING) << "Chrome sent an unexpected seed; ignoring"; |
| response = dbus::Response::FromMethodCall(method_call); |
| std::move(sender).Run(std::move(response)); |
| return; |
| } |
| |
| if (store_) { |
| if (!store_->SetLastGoodSeed(seed)) { |
| LOG(ERROR) << "Failed to write fetched seed to disk"; |
| response = dbus::ErrorResponse::FromMethodCall( |
| method_call, DBUS_ERROR_FAILED, |
| "Failed to write fetched seed to disk"); |
| std::move(sender).Run(std::move(response)); |
| return; |
| } |
| |
| if (!store_->ClearBootAttemptsSinceLastUpdate()) { |
| LOG(ERROR) << "Failed to reset boot attempts counter after fetching seed"; |
| response = dbus::ErrorResponse::FromMethodCall( |
| method_call, DBUS_ERROR_FAILED, |
| "Failed to reset boot attempts counter"); |
| std::move(sender).Run(std::move(response)); |
| return; |
| } |
| } |
| |
| response = dbus::Response::FromMethodCall(method_call); |
| std::move(sender).Run(std::move(response)); |
| } |
| |
| bool DbusFeaturedService::Start(dbus::Bus* bus, |
| std::shared_ptr<DbusFeaturedService> ptr) { |
| if (!bus || !bus->Connect()) { |
| LOG(ERROR) << "Failed to connect to DBus"; |
| return false; |
| } |
| |
| if (!feature::PlatformFeatures::Initialize(bus)) { |
| LOG(ERROR) << "Failed to initialize PlatformFeatures"; |
| return false; |
| } |
| |
| if (store_ && !store_->IncrementBootAttemptsSinceLastUpdate()) { |
| LOG(ERROR) << "Failed to increment boot attempts"; |
| return false; |
| } |
| |
| dbus::ObjectPath path(featured::kFeaturedServicePath); |
| dbus::ExportedObject* object = bus->GetExportedObject(path); |
| if (!object) { |
| LOG(ERROR) << "Failed to get exported object at " << path.value(); |
| return false; |
| } |
| |
| if (!object->ExportMethodAndBlock( |
| featured::kFeaturedServiceName, featured::kHandleSeedFetchedMethod, |
| base::BindRepeating(&DbusFeaturedService::HandleSeedFetched, ptr))) { |
| bus->UnregisterExportedObject(path); |
| LOG(ERROR) << "Failed to export method " |
| << featured::kHandleSeedFetchedMethod; |
| return false; |
| } |
| |
| // Late boot features do not expect to start until after login, so delay them |
| // until then. |
| session_manager_.reset(new org::chromium::SessionManagerInterfaceProxy(bus)); |
| session_manager_->RegisterSessionStateChangedSignalHandler( |
| base::BindRepeating(&DbusFeaturedService::OnSessionStateChanged, |
| weak_ptr_factory_.GetWeakPtr()), |
| base::BindOnce(&OnSignalConnected)); |
| |
| if (!bus->RequestOwnershipAndBlock(featured::kFeaturedServiceName, |
| dbus::Bus::REQUIRE_PRIMARY)) { |
| bus->UnregisterExportedObject(path); |
| LOG(ERROR) << "Failed to get ownership of " |
| << featured::kFeaturedServiceName; |
| return false; |
| } |
| |
| return true; |
| } |
| |
| } // namespace featured |