| // 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_ |