| // 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. |
| |
| #include "components/named_system_lock/lock.h" |
| |
| #include <mach/mach.h> |
| #include <servers/bootstrap.h> |
| #include <sys/types.h> |
| #include <unistd.h> |
| |
| #include <algorithm> |
| #include <memory> |
| #include <string> |
| #include <utility> |
| |
| #include "base/apple/mach_logging.h" |
| #include "base/apple/scoped_mach_port.h" |
| #include "base/check.h" |
| #include "base/logging.h" |
| #include "base/strings/strcat.h" |
| #include "base/threading/platform_thread.h" |
| #include "base/time/time.h" |
| |
| namespace { |
| |
| // Interval to poll for lock availability if it is not immediately available. |
| // Final interval is truncated to fit the available timeout. |
| constexpr base::TimeDelta kLockPollingInterval = base::Seconds(3); |
| |
| // |
| // Attempts to acquire the receive right to a named Mach service. |
| // Single attempt, no retries. Logs errors other than "permission denied", |
| // since "permission denied" typically means the service receive rights have |
| // already been assigned. |
| // |
| // Returns the receive right if the right was successfully acquired. If the |
| // right cannot be acquired for any reason, returns an invalid right instead. |
| base::apple::ScopedMachReceiveRight TryAcquireReceive( |
| const base::apple::ScopedMachSendRight& bootstrap_right, |
| const std::string& service_name) { |
| VLOG(2) << __func__; |
| base::apple::ScopedMachReceiveRight target_right; |
| kern_return_t check_in_result = bootstrap_check_in( |
| bootstrap_right.get(), service_name.c_str(), |
| base::apple::ScopedMachReceiveRight::Receiver(target_right).get()); |
| if (check_in_result != KERN_SUCCESS) { |
| // Log error reports for all errors other than BOOTSTRAP_NOT_PRIVILEGED. |
| // BOOTSTRAP_NOT_PRIVILEGED is not logged because it just means that another |
| // process has acquired the receive rights for this service. |
| if (check_in_result != BOOTSTRAP_NOT_PRIVILEGED) { |
| BOOTSTRAP_LOG(ERROR, check_in_result) |
| << " bootstrap_check_in to acquire lock: " << service_name; |
| } else { |
| BOOTSTRAP_VLOG(2, check_in_result) |
| << " lock already held: " << service_name; |
| } |
| return base::apple::ScopedMachReceiveRight(); |
| } |
| return target_right; |
| } |
| |
| // Sleeps `wait_time` until the lock should be retried, but no more than |
| // `kLockPollingInterval`. |
| void WaitToRetryLock(base::TimeDelta wait_time) { |
| base::PlatformThread::Sleep(std::min(wait_time, kLockPollingInterval)); |
| } |
| |
| } // anonymous namespace |
| |
| namespace named_system_lock { |
| |
| class ScopedLockImpl { |
| public: |
| // Constructs a ScopedLockImpl from a receive right. |
| explicit ScopedLockImpl(base::apple::ScopedMachReceiveRight receive_right); |
| |
| // Releases the receive right. |
| ~ScopedLockImpl() = default; |
| |
| ScopedLockImpl(const ScopedLockImpl&) = delete; |
| ScopedLockImpl& operator=(const ScopedLockImpl&) = delete; |
| |
| private: |
| // The Mach port representing the held lock itself. We only care about |
| // service ownership; no messages are transferred with this port. |
| base::apple::ScopedMachReceiveRight receive_right_; |
| }; |
| |
| ScopedLockImpl::ScopedLockImpl( |
| base::apple::ScopedMachReceiveRight receive_right) |
| : receive_right_(std::move(receive_right)) { |
| mach_port_type_t port_type = 0; |
| kern_return_t port_check_result = |
| mach_port_type(mach_task_self(), receive_right_.get(), &port_type); |
| MACH_CHECK(port_check_result == KERN_SUCCESS, port_check_result) |
| << "ScopedLockImpl could not verify lock port"; |
| CHECK(port_type & MACH_PORT_TYPE_RECEIVE) |
| << "ScopedLockImpl given port without receive right"; |
| } |
| |
| ScopedLock::ScopedLock(std::unique_ptr<ScopedLockImpl> impl) |
| : impl_(std::move(impl)) {} |
| |
| ScopedLock::~ScopedLock() = default; |
| |
| // static |
| std::unique_ptr<ScopedLock> ScopedLock::Create(const std::string& service_name, |
| base::TimeDelta timeout) { |
| // Find the right namespace for the lock. Non-privileged processes cannot |
| // climb "up" out of their namespace, but user processes run with the right |
| // namespace (the interactive user session) anyway. System processes need the |
| // system namespace, which is the root of macOS' "tree" of Mach bootstrap |
| // namespaces. Daemons (including LaunchDaemons) and system services naturally |
| // run under this namespace, but `sudo` -- a POSIX utility that is not aware |
| // of the Mach portions of the macOS kernel -- runs targets without changing |
| // their bootstrap port (and therefore their namespace). The system namespace |
| // is the only one guaranteed to be shared between users. |
| // |
| // mac/launcher_main.c uses an equivalent algorithm to find this namespace |
| // when launching a privileged process. It's repeated here so processes |
| // launched via sudo (such as the integration test helper) can reach the |
| // system-scope locks. |
| base::apple::ScopedMachSendRight bootstrap_right = |
| base::apple::RetainMachSendRight(bootstrap_port); |
| if (!geteuid()) { |
| // Move the initial bootstrap right into `next_right` so the first loop is |
| // not a special case. base::ScopedGeneric calls `abort()` if you `reset` it |
| // to what it already holds, so this has to be a move, not a retain. |
| base::apple::ScopedMachSendRight next_right(bootstrap_right.release()); |
| while (bootstrap_right.get() != next_right.get()) { |
| bootstrap_right.reset(next_right.release()); |
| kern_return_t bootstrap_err = bootstrap_parent( |
| bootstrap_right.get(), |
| base::apple::ScopedMachSendRight::Receiver(next_right).get()); |
| if (bootstrap_err != KERN_SUCCESS) { |
| BOOTSTRAP_LOG(ERROR, bootstrap_err) |
| << "can't bootstrap_parent in ScopedLock::Create for euid 0"; |
| break; // Use last known bootstrap_right. |
| } |
| CHECK(next_right.is_valid()) |
| << "bootstrap_parent yielded invalid port without error"; |
| } |
| } |
| |
| // Make one try to acquire the lock, even if the timeout is zero or negative. |
| base::apple::ScopedMachReceiveRight receive_right( |
| TryAcquireReceive(bootstrap_right, service_name.c_str())); |
| |
| base::TimeTicks deadline = base::TimeTicks::Now() + timeout; |
| for (base::TimeDelta remain = deadline - base::TimeTicks::Now(); |
| !receive_right.is_valid() && remain.is_positive(); |
| remain = deadline - base::TimeTicks::Now()) { |
| WaitToRetryLock(remain); |
| receive_right = TryAcquireReceive(bootstrap_right, service_name); |
| } |
| |
| if (!receive_right.is_valid()) { |
| return nullptr; |
| } |
| return std::make_unique<ScopedLock>( |
| std::make_unique<ScopedLockImpl>(std::move(receive_right))); |
| } |
| |
| } // namespace named_system_lock |