blob: 75a7052040a80582323b11fb9a9eab47f93d14ff [file] [log] [blame]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CHROME_BROWSER_MAC_CODE_SIGN_CLONE_MANAGER_H_
#define CHROME_BROWSER_MAC_CODE_SIGN_CLONE_MANAGER_H_
#include "base/feature_list.h"
#include "base/files/file_path.h"
#include "base/functional/callback_helpers.h"
#include "base/timer/timer.h"
#include "content/public/common/main_function_params.h"
namespace code_sign_clone_manager {
BASE_DECLARE_FEATURE(kMacAppCodeSignClone);
BASE_DECLARE_FEATURE(kMacAppCodeSignCloneRenameAsBundle);
//
// Manages a temporary copy-on-write clone of an app bundle. The temporary clone
// crucially has its main executable replaced with a hard link to the source's
// main executable. This is intended for use by the browser app to keep in-use
// files available on the filesystem after a staged update. This is in service
// of keeping the app's code signature statically valid and in agreement with
// its dynamic code signature after a staged update. See
// https://crbug.com/338582873 for more details.
//
// During initialization, the app bundle in `src_path` will be APFS
// copy-on-write cloned to a temporary directory. The main executable in the
// clone is then replaced with a hard link to the `src_path` main executable.
// The hard linked main executable is crucial in passing dynamic code signature
// validation after an update is staged. For dynamic validation, we need to keep
// a reachable path to the main executable alive on the filesystem, which is why
// it is hard linked. To support support static code signature validation, the
// hard linked main executable needs to be surrounded by a bundle structure that
// validates. The rest of the cloned files make up this bundle structure and
// support static code signature validation.
//
// Creation of this clone is best effort, there are no guarantees of success.
// Some failure cases include running the source app bundle from:
// * not-APFS (because filesystem clones are only supported on APFS)
// * a read-only filesystem (because this needs to write to the same
// filesystem that the app is running from)
// * a non-root filesystem (because the clone is built in
// `/private/var/folders/...`, which is on the root filesystem)
//
// Upon destruction, the temporary directory hosting the clone will be deleted.
//
// Clone creation and deletion will take place in the background and will not
// block. Background clone creation will take place in the calling process on a
// background thread to prevent blocking startup. Care has been taken to
// optimize clone creation to avoid resource contention on startup. Background
// clone deletion will take place from a helper process. This is to prevent
// blocking browser shutdown, with the added benefit of keeping in-use files
// available on the filesystem until the very end of the browser process.
//
// Cloning and hard linking the main executable was chosen as it is the least
// expensive out of the explored options (tested with Google Chrome M125 on an
// M1 Max Mac).
//
// `clonefile` + `link` the main executable: ~10ms
// Hard linking the whole app tree: best approach ~60ms
// * `-[NSFileManager linkItemAtURL:toURL:error:]`: ~120ms
// * `base::FileEnumerator`: ~80ms
// * `readdir`, `getattrlistbulk`, `fts` only `stat`ing dirs: ~60ms
//
// In the common case, both options (`clonefile` vs. hard link-based) take up no
// extra disk space, aside from directory entries, while `clonefile` involves
// the least amount of touching of the disk. The only disadvantage of
// `clonefile` is that it’s currently APFS-only. The purely hard link-based
// approaches would be more broadly applicable (on HFS+ for example). This is
// not a major concern given how ubiquitous APFS on macOS has become.
//
// Each instance of Chrome will create a new temporary clone of itself at
// startup even if one already exists. In this way each instance of Chrome
// maintains a reference to its specific in-use files, keeping them accessible
// on the filesystem until exit. Once the last instance of Chrome that maintains
// a reference to the in-use files exits and its cleanup helper runs, the files
// will become inaccessible on the filesystem and their disk blocks will be
// freed.
//
// The clone is given a ".bundle" extension to avoid Launch Services issues; see
// https://crbug.com/381199182 for more details.
//
// Example path to the cloned app bundle:
// /private/var/folders/c4/ygf_t4gn0tx0k1y1hm32hh6w00b_4p/X/org.chromium.Chromium.code_sign_clone/code_sign_clone.tKdILk/Chromium.app.bundle
//
// Each clone contains an instance-specific snapshot of an on-disk
// representation of Chrome. The bundles are verifiable by both dynamic and
// static code signature checks.
//
class CodeSignCloneManager {
public:
using CloneCallback = base::OnceCallback<void(base::FilePath)>;
//
// `src_path` is the path to the app bundle to be cloned.
//
// `main_executable_name` is the name of the main executable to be hard
// linked. The name must be an entry in the "Contents/MacOS" directory within
// the `src_path`.
//
// `callback` is called after the clone has been created or if an error
// occurs.
//
CodeSignCloneManager(const base::FilePath& src_path,
const base::FilePath& main_executable_name,
CloneCallback callback = base::DoNothing());
CodeSignCloneManager(const CodeSignCloneManager&) = delete;
CodeSignCloneManager& operator=(const CodeSignCloneManager&) = delete;
~CodeSignCloneManager();
static void SetTemporaryDirectoryPathForTesting(const base::FilePath& path);
static void ClearTemporaryDirectoryPathForTesting();
static void SetDirhelperPathForTesting(const base::FilePath& path);
static void ClearDirhelperPathForTesting();
static base::FilePath GetCloneTemporaryDirectoryForTesting();
bool get_needs_cleanup_for_testing() { return needs_cleanup_; }
private:
void Clone(const base::FilePath& src_path,
const base::FilePath& main_executable_name,
CloneCallback callback);
void StartCloneExistsTimer(const base::FilePath& clone_app_path,
const base::FilePath& main_executable_name);
void StopCloneExistsTimer();
void CloneExistsTimerFire(const base::FilePath& clone_app_path,
const base::FilePath& main_executable_name);
std::string unique_temp_dir_suffix_;
base::RepeatingTimer clone_exists_timer_;
scoped_refptr<base::SequencedTaskRunner> task_runner_;
bool needs_cleanup_ = false;
};
namespace internal {
// The entry point into the background clone cleanup process. This is not a user
// API.
int ChromeCodeSignCloneCleanupMain(content::MainFunctionParams main_parameters);
//
// Checks if a file is open more than once, globally (across all processes on a
// host), including the calling process.
//
// Usage of this check is fairly niche. It does not provide any information
// about which process has a reference to an open file. For that see
// `proc_listpidspath` (The linked header is from macOS 14.5).
// https://github.com/apple-oss-distributions/xnu/blob/xnu-10063.121.3/libsyscall/wrappers/libproc/libproc.h
//
// This function simply checks if a file is exclusively opened. Performance
// concerns may be a reason to use this function over `proc_listpidspath`, which
// needs to loop over the process tree.
//
// Note: File descriptors that have been `dup`ed or inherited only count as
// being opened once for this check’s purpose.
//
enum class FileOpenMoreThanOnce {
kNo = 0,
kYes = 1,
kError = 2,
};
FileOpenMoreThanOnce IsFileOpenMoreThanOnce(const base::FilePath& file_path);
FileOpenMoreThanOnce IsFileOpenMoreThanOnce(int file_descriptor);
} // namespace internal
} // namespace code_sign_clone_manager
#endif // CHROME_BROWSER_MAC_CODE_SIGN_CLONE_MANAGER_H_