// Copyright (c) 2012 The Chromium 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 "sandbox/win/src/broker_services.h" #include #include #include #include "base/logging.h" #include "base/macros.h" #include "base/memory/ptr_util.h" #include "base/threading/platform_thread.h" #include "base/win/scoped_handle.h" #include "base/win/scoped_process_information.h" #include "base/win/startup_information.h" #include "base/win/windows_version.h" #include "sandbox/win/src/app_container_profile.h" #include "sandbox/win/src/process_mitigations.h" #include "sandbox/win/src/sandbox.h" #include "sandbox/win/src/sandbox_policy_base.h" #include "sandbox/win/src/sandbox_policy_diagnostic.h" #include "sandbox/win/src/target_process.h" #include "sandbox/win/src/win2k_threadpool.h" #include "sandbox/win/src/win_utils.h" namespace { // Utility function to associate a completion port to a job object. bool AssociateCompletionPort(HANDLE job, HANDLE port, void* key) { JOBOBJECT_ASSOCIATE_COMPLETION_PORT job_acp = {key, port}; return ::SetInformationJobObject(job, JobObjectAssociateCompletionPortInformation, &job_acp, sizeof(job_acp)) ? true : false; } // Utility function to do the cleanup necessary when something goes wrong // while in SpawnTarget and we must terminate the target process. sandbox::ResultCode SpawnCleanup(sandbox::TargetProcess* target) { target->Terminate(); delete target; return sandbox::SBOX_ERROR_GENERIC; } // the different commands that you can send to the worker thread that // executes TargetEventsThread(). enum { THREAD_CTRL_NONE, THREAD_CTRL_NEW_JOB_TRACKER, THREAD_CTRL_NEW_PROCESS_TRACKER, THREAD_CTRL_PROCESS_SIGNALLED, THREAD_CTRL_GET_POLICY_INFO, THREAD_CTRL_QUIT, THREAD_CTRL_LAST, }; // Helper structure that allows the Broker to associate a job notification // with a job object and with a policy. struct JobTracker { JobTracker(base::win::ScopedHandle job, scoped_refptr policy, DWORD process_id) : job(std::move(job)), policy(policy), process_id(process_id) {} ~JobTracker() { FreeResources(); } // Releases the Job and notifies the associated Policy object to release its // resources as well. void FreeResources(); base::win::ScopedHandle job; scoped_refptr policy; DWORD process_id; }; void JobTracker::FreeResources() { if (policy) { bool res = ::TerminateJobObject(job.Get(), sandbox::SBOX_ALL_OK); DCHECK(res); // Closing the job causes the target process to be destroyed so this needs // to happen before calling OnJobEmpty(). HANDLE stale_job_handle = job.Get(); job.Close(); // In OnJobEmpty() we don't actually use the job handle directly. policy->OnJobEmpty(stale_job_handle); policy = nullptr; } } // tracks processes that are not in jobs struct ProcessTracker { ProcessTracker(scoped_refptr policy, DWORD process_id, base::win::ScopedHandle process) : policy(policy), process_id(process_id), process(std::move(process)) {} ~ProcessTracker() { FreeResources(); } void FreeResources(); scoped_refptr policy; DWORD process_id; base::win::ScopedHandle process; // Used to UnregisterWait. Not a real handle so cannot CloseHandle(). HANDLE wait_handle; // IOCP that is tracking this non-job process HANDLE iocp; }; void ProcessTracker::FreeResources() { if (policy) { policy->OnJobEmpty(nullptr); policy = nullptr; } } // Helper redispatches process events to tracker thread. void WINAPI ProcessEventCallback(PVOID param, BOOLEAN ignored) { // This callback should do very little, and must be threadpool safe. ProcessTracker* tracker = reinterpret_cast(param); // If this fails we can do nothing... we will leak the policy. ::PostQueuedCompletionStatus(tracker->iocp, 0, THREAD_CTRL_PROCESS_SIGNALLED, reinterpret_cast(tracker)); } // Helper class to send policy lists class PolicyDiagnosticList final : public sandbox::PolicyList { public: PolicyDiagnosticList() {} ~PolicyDiagnosticList() override {} void push_back(std::unique_ptr info) { internal_list_.push_back(std::move(info)); } std::vector>::iterator begin() override { return internal_list_.begin(); } std::vector>::iterator end() override { return internal_list_.end(); } size_t size() const override { return internal_list_.size(); } private: std::vector> internal_list_; }; } // namespace namespace sandbox { BrokerServicesBase::BrokerServicesBase() {} // The broker uses a dedicated worker thread that services the job completion // port to perform policy notifications and associated cleanup tasks. ResultCode BrokerServicesBase::Init() { if (job_port_.IsValid() || thread_pool_) return SBOX_ERROR_UNEXPECTED_CALL; job_port_.Set(::CreateIoCompletionPort(INVALID_HANDLE_VALUE, nullptr, 0, 0)); if (!job_port_.IsValid()) return SBOX_ERROR_CANNOT_INIT_BROKERSERVICES; no_targets_.Set(::CreateEventW(nullptr, true, false, nullptr)); job_thread_.Set(::CreateThread(nullptr, 0, // Default security and stack. TargetEventsThread, this, 0, nullptr)); if (!job_thread_.IsValid()) return SBOX_ERROR_CANNOT_INIT_BROKERSERVICES; if (!SharedMemIPCServer::CreateBrokerAliveMutex()) return SBOX_ERROR_CANNOT_INIT_BROKERSERVICES; return SBOX_ALL_OK; } // The destructor should only be called when the Broker process is terminating. // Since BrokerServicesBase is a singleton, this is called from the CRT // termination handlers, if this code lives on a DLL it is called during // DLL_PROCESS_DETACH in other words, holding the loader lock, so we cannot // wait for threads here. BrokerServicesBase::~BrokerServicesBase() { // If there is no port Init() was never called successfully. if (!job_port_.IsValid()) return; // Closing the port causes, that no more Job notifications are delivered to // the worker thread and also causes the thread to exit. This is what we // want to do since we are going to close all outstanding Jobs and notifying // the policy objects ourselves. ::PostQueuedCompletionStatus(job_port_.Get(), 0, THREAD_CTRL_QUIT, nullptr); if (job_thread_.IsValid() && WAIT_TIMEOUT == ::WaitForSingleObject(job_thread_.Get(), 1000)) { // Cannot clean broker services. NOTREACHED(); return; } thread_pool_.reset(); } scoped_refptr BrokerServicesBase::CreatePolicy() { // If you change the type of the object being created here you must also // change the downcast to it in SpawnTarget(). scoped_refptr policy(new PolicyBase); // PolicyBase starts with refcount 1. policy->Release(); return policy; } // The worker thread stays in a loop waiting for asynchronous notifications // from the job objects. Right now we only care about knowing when the last // process on a job terminates, but in general this is the place to tell // the policy about events. DWORD WINAPI BrokerServicesBase::TargetEventsThread(PVOID param) { if (!param) return 1; base::PlatformThread::SetName("BrokerEvent"); BrokerServicesBase* broker = reinterpret_cast(param); HANDLE port = broker->job_port_.Get(); HANDLE no_targets = broker->no_targets_.Get(); std::set child_process_ids; std::list> jobs; std::list> processes; int target_counter = 0; int untracked_target_counter = 0; ::ResetEvent(no_targets); while (true) { DWORD events = 0; ULONG_PTR key = 0; LPOVERLAPPED ovl = nullptr; if (!::GetQueuedCompletionStatus(port, &events, &key, &ovl, INFINITE)) { // this call fails if the port has been closed before we have a // chance to service the last packet which is 'exit' anyway so // this is not an error. return 1; } if (key > THREAD_CTRL_LAST) { // The notification comes from a job object. There are nine notifications // that jobs can send and some of them depend on the job attributes set. JobTracker* tracker = reinterpret_cast(key); // Processes may be added to a job after the process count has // reached zero, leading us to manipulate a freed JobTracker // object or job handle (as the key is no longer valid). We // therefore check if the tracker has already been deleted. if (std::find_if(jobs.begin(), jobs.end(), [&](auto&& p) -> bool { return p.get() == tracker; }) == jobs.end()) { CHECK(false); } switch (events) { case JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO: { // The job object has signaled that the last process associated // with it has terminated. It is safe to free the tracker // and release its reference to the associated policy object // which will Close the job handle. HANDLE job_handle = tracker->job.Get(); // Erase by comparing with the job handle. jobs.erase(std::remove_if(jobs.begin(), jobs.end(), [&](auto&& p) -> bool { return p->job.Get() == job_handle; }), jobs.end()); break; } case JOB_OBJECT_MSG_NEW_PROCESS: { // Child process created from sandboxed process. DWORD process_id = static_cast(reinterpret_cast(ovl)); size_t count = child_process_ids.count(process_id); if (count == 0) untracked_target_counter++; ++target_counter; if (1 == target_counter) { ::ResetEvent(no_targets); } break; } case JOB_OBJECT_MSG_EXIT_PROCESS: case JOB_OBJECT_MSG_ABNORMAL_EXIT_PROCESS: { size_t erase_result = child_process_ids.erase( static_cast(reinterpret_cast(ovl))); if (erase_result != 1U) { // The process was untracked e.g. a child process of the target. --untracked_target_counter; DCHECK(untracked_target_counter >= 0); } --target_counter; if (0 == target_counter) ::SetEvent(no_targets); DCHECK(target_counter >= 0); break; } case JOB_OBJECT_MSG_ACTIVE_PROCESS_LIMIT: { // A child process attempted and failed to create a child process. // Windows does not reveal the process id. untracked_target_counter++; target_counter++; break; } case JOB_OBJECT_MSG_PROCESS_MEMORY_LIMIT: { bool res = ::TerminateJobObject(tracker->job.Get(), SBOX_FATAL_MEMORY_EXCEEDED); DCHECK(res); break; } default: { NOTREACHED(); break; } } } else if (THREAD_CTRL_NEW_JOB_TRACKER == key) { std::unique_ptr tracker; tracker.reset(reinterpret_cast(ovl)); DCHECK(tracker->job.IsValid()); child_process_ids.insert(tracker->process_id); jobs.push_back(std::move(tracker)); } else if (THREAD_CTRL_NEW_PROCESS_TRACKER == key) { std::unique_ptr tracker; tracker.reset(reinterpret_cast(ovl)); if (child_process_ids.empty()) { ::SetEvent(broker->no_targets_.Get()); } tracker->iocp = port; if (!::RegisterWaitForSingleObject(&(tracker->wait_handle), tracker->process.Get(), ProcessEventCallback, tracker.get(), INFINITE, WT_EXECUTEONLYONCE)) { // Failed. Invalidate the wait_handle and store anyway. tracker->wait_handle = INVALID_HANDLE_VALUE; } processes.push_back(std::move(tracker)); } else if (THREAD_CTRL_PROCESS_SIGNALLED == key) { ProcessTracker* tracker = static_cast(reinterpret_cast(ovl)); ::UnregisterWait(tracker->wait_handle); tracker->wait_handle = INVALID_HANDLE_VALUE; // Copy process_id so that we can legally reference it even after we have // found the ProcessTracker object to delete. const DWORD process_id = tracker->process_id; // PID is unique until the process handle is closed in dtor. processes.erase(std::remove_if(processes.begin(), processes.end(), [&](auto&& p) -> bool { return p->process_id == process_id; }), processes.end()); } else if (THREAD_CTRL_GET_POLICY_INFO == key) { // Clone the policies for sandbox diagnostics. std::unique_ptr receiver; receiver.reset(static_cast( reinterpret_cast(ovl))); // The PollicyInfo ctor copies essential information from the trackers. auto policy_list = std::make_unique(); for (auto&& process_tracker : processes) { if (process_tracker->policy) { policy_list->push_back(std::make_unique( process_tracker->policy.get())); } } for (auto&& job_tracker : jobs) { if (job_tracker->policy) { policy_list->push_back( std::make_unique(job_tracker->policy.get())); } } // Receiver should return quickly. receiver->ReceiveDiagnostics(std::move(policy_list)); } else if (THREAD_CTRL_QUIT == key) { // The broker object is being destroyed so the thread needs to exit. for (auto&& tracker : processes) { ::UnregisterWait(tracker->wait_handle); tracker->wait_handle = INVALID_HANDLE_VALUE; } // After this point, so further calls to ProcessEventCallback can // occur. Other tracked objects are destroyed as this thread ends. return 0; } else { // We have not implemented more commands. NOTREACHED(); } } NOTREACHED(); return 0; } // SpawnTarget does all the interesting sandbox setup and creates the target // process inside the sandbox. ResultCode BrokerServicesBase::SpawnTarget(const wchar_t* exe_path, const wchar_t* command_line, base::EnvironmentMap& env_map, scoped_refptr policy, ResultCode* last_warning, DWORD* last_error, PROCESS_INFORMATION* target_info) { if (!exe_path) return SBOX_ERROR_BAD_PARAMS; if (!policy) return SBOX_ERROR_BAD_PARAMS; // Even though the resources touched by SpawnTarget can be accessed in // multiple threads, the method itself cannot be called from more than one // thread. This is to protect the global variables used while setting up the // child process, and to make sure launcher thread mitigations are applied // correctly. static DWORD thread_id = ::GetCurrentThreadId(); DCHECK(thread_id == ::GetCurrentThreadId()); *last_warning = SBOX_ALL_OK; // Launcher thread only needs to be opted out of ACG once. Do this on the // first child process being spawned. static bool launcher_thread_opted_out = false; if (!launcher_thread_opted_out) { // Soft fail this call. It will fail if ACG is not enabled for this process. sandbox::ApplyMitigationsToCurrentThread( sandbox::MITIGATION_DYNAMIC_CODE_OPT_OUT_THIS_THREAD); launcher_thread_opted_out = true; } // This downcast is safe as long as we control CreatePolicy() scoped_refptr policy_base(static_cast(policy.get())); // Construct the tokens and the job object that we are going to associate // with the soon to be created target process. base::win::ScopedHandle initial_token; base::win::ScopedHandle lockdown_token; base::win::ScopedHandle lowbox_token; ResultCode result = SBOX_ALL_OK; result = policy_base->MakeTokens(&initial_token, &lockdown_token, &lowbox_token); if (SBOX_ALL_OK != result) return result; if (lowbox_token.IsValid() && base::win::GetVersion() < base::win::Version::WIN8) { // We don't allow lowbox_token below Windows 8. return SBOX_ERROR_BAD_PARAMS; } base::win::ScopedHandle job; result = policy_base->MakeJobObject(&job); if (SBOX_ALL_OK != result) return result; // Initialize the startup information from the policy. base::win::StartupInformation startup_info; // We don't want any child processes causing the IDC_APPSTARTING cursor. startup_info.startup_info()->dwFlags |= STARTF_FORCEOFFFEEDBACK; // The liftime of |mitigations|, |inherit_handle_list| and // |child_process_creation| have to be at least as long as // |startup_info| because |UpdateProcThreadAttribute| requires that // its |lpValue| parameter persist until |DeleteProcThreadAttributeList| is // called; StartupInformation's destructor makes such a call. DWORD64 mitigations[2]; std::vector inherited_handle_list; DWORD child_process_creation = PROCESS_CREATION_CHILD_PROCESS_RESTRICTED; std::wstring desktop = policy_base->GetAlternateDesktop(); if (!desktop.empty()) { startup_info.startup_info()->lpDesktop = const_cast(desktop.c_str()); } bool inherit_handles = false; int attribute_count = 0; size_t mitigations_size; ConvertProcessMitigationsToPolicy(policy_base->GetProcessMitigations(), &mitigations[0], &mitigations_size); if (mitigations[0] || mitigations[1]) ++attribute_count; bool restrict_child_process_creation = false; if (base::win::GetVersion() >= base::win::Version::WIN10_TH2 && policy_base->GetJobLevel() <= JOB_LIMITED_USER) { restrict_child_process_creation = true; ++attribute_count; } HANDLE stdout_handle = policy_base->GetStdoutHandle(); HANDLE stderr_handle = policy_base->GetStderrHandle(); if (stdout_handle != INVALID_HANDLE_VALUE) inherited_handle_list.push_back(stdout_handle); // Handles in the list must be unique. if (stderr_handle != stdout_handle && stderr_handle != INVALID_HANDLE_VALUE) inherited_handle_list.push_back(stderr_handle); const auto& policy_handle_list = policy_base->GetHandlesBeingShared(); for (HANDLE handle : policy_handle_list) inherited_handle_list.push_back(handle); if (inherited_handle_list.size()) ++attribute_count; scoped_refptr profile = policy_base->GetAppContainerProfileBase(); if (profile) { if (base::win::GetVersion() < base::win::Version::WIN8) return SBOX_ERROR_BAD_PARAMS; ++attribute_count; if (profile->GetEnableLowPrivilegeAppContainer()) { // LPAC first supported in RS1. if (base::win::GetVersion() < base::win::Version::WIN10_RS1) return SBOX_ERROR_BAD_PARAMS; ++attribute_count; } } if (!startup_info.InitializeProcThreadAttributeList(attribute_count)) return SBOX_ERROR_PROC_THREAD_ATTRIBUTES; if (mitigations[0] || mitigations[1]) { if (!startup_info.UpdateProcThreadAttribute( PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY, &mitigations[0], mitigations_size)) { return SBOX_ERROR_PROC_THREAD_ATTRIBUTES; } } if (restrict_child_process_creation) { if (!startup_info.UpdateProcThreadAttribute( PROC_THREAD_ATTRIBUTE_CHILD_PROCESS_POLICY, &child_process_creation, sizeof(child_process_creation))) { return SBOX_ERROR_PROC_THREAD_ATTRIBUTES; } } if (inherited_handle_list.size()) { if (!startup_info.UpdateProcThreadAttribute( PROC_THREAD_ATTRIBUTE_HANDLE_LIST, &inherited_handle_list[0], sizeof(HANDLE) * inherited_handle_list.size())) { return SBOX_ERROR_PROC_THREAD_ATTRIBUTES; } startup_info.startup_info()->dwFlags |= STARTF_USESTDHANDLES; startup_info.startup_info()->hStdInput = INVALID_HANDLE_VALUE; startup_info.startup_info()->hStdOutput = stdout_handle; startup_info.startup_info()->hStdError = stderr_handle; // Allowing inheritance of handles is only secure now that we // have limited which handles will be inherited. inherit_handles = true; } // Declared here to ensure they stay in scope until after process creation. std::unique_ptr security_capabilities; DWORD all_applications_package_policy = PROCESS_CREATION_ALL_APPLICATION_PACKAGES_OPT_OUT; if (profile) { security_capabilities = profile->GetSecurityCapabilities(); if (!startup_info.UpdateProcThreadAttribute( PROC_THREAD_ATTRIBUTE_SECURITY_CAPABILITIES, security_capabilities.get(), sizeof(SECURITY_CAPABILITIES))) { return SBOX_ERROR_PROC_THREAD_ATTRIBUTES; } if (profile->GetEnableLowPrivilegeAppContainer()) { if (!startup_info.UpdateProcThreadAttribute( PROC_THREAD_ATTRIBUTE_ALL_APPLICATION_PACKAGES_POLICY, &all_applications_package_policy, sizeof(all_applications_package_policy))) { return SBOX_ERROR_PROC_THREAD_ATTRIBUTES; } } } // Construct the thread pool here in case it is expensive. // The thread pool is shared by all the targets if (!thread_pool_) thread_pool_ = std::make_unique(); // Create the TargetProcess object and spawn the target suspended. Note that // Brokerservices does not own the target object. It is owned by the Policy. base::win::ScopedProcessInformation process_info; TargetProcess* target = new TargetProcess( std::move(initial_token), std::move(lockdown_token), job.Get(), thread_pool_.get(), profile ? profile->GetImpersonationCapabilities() : std::vector()); result = target->Create(exe_path, command_line, inherit_handles, startup_info, &process_info, env_map, last_error); if (result != SBOX_ALL_OK) { SpawnCleanup(target); return result; } if (lowbox_token.IsValid()) { *last_warning = target->AssignLowBoxToken(lowbox_token); // If this fails we continue, but report the error as a warning. // This is due to certain configurations causing the setting of the // token to fail post creation, and we'd rather continue if possible. if (*last_warning != SBOX_ALL_OK) *last_error = ::GetLastError(); } // Now the policy is the owner of the target. result = policy_base->AddTarget(target); if (result != SBOX_ALL_OK) { *last_error = ::GetLastError(); SpawnCleanup(target); return result; } if (job.IsValid()) { JobTracker* tracker = new JobTracker(std::move(job), policy_base, process_info.process_id()); // Post the tracker to the tracking thread, then associate the job with // the tracker. The worker thread takes ownership of these objects. CHECK(::PostQueuedCompletionStatus( job_port_.Get(), 0, THREAD_CTRL_NEW_JOB_TRACKER, reinterpret_cast(tracker))); // There is no obvious recovery after failure here. Previous version with // SpawnCleanup() caused deletion of TargetProcess twice. crbug.com/480639 CHECK( AssociateCompletionPort(tracker->job.Get(), job_port_.Get(), tracker)); } else { // Duplicate the process handle to give the tracking machinery // something valid to wait on in the tracking thread. HANDLE tmp_process_handle = INVALID_HANDLE_VALUE; if (!::DuplicateHandle(::GetCurrentProcess(), process_info.process_handle(), ::GetCurrentProcess(), &tmp_process_handle, SYNCHRONIZE, false, 0 /*no options*/)) { *last_error = ::GetLastError(); // This may fail in the same way as Job associated processes. // crbug.com/480639. target->Terminate(); return SBOX_ERROR_CANNOT_DUPLICATE_PROCESS_HANDLE; } base::win::ScopedHandle dup_process_handle(tmp_process_handle); ProcessTracker* tracker = new ProcessTracker( policy_base, process_info.process_id(), std::move(dup_process_handle)); // The tracker and policy will leak if this call fails. ::PostQueuedCompletionStatus(job_port_.Get(), 0, THREAD_CTRL_NEW_PROCESS_TRACKER, reinterpret_cast(tracker)); } *target_info = process_info.Take(); return result; } ResultCode BrokerServicesBase::WaitForAllTargets() { ::WaitForSingleObject(no_targets_.Get(), INFINITE); return SBOX_ALL_OK; } ResultCode BrokerServicesBase::GetPolicyDiagnostics( std::unique_ptr receiver) { CHECK(job_thread_.IsValid()); // Post to the job thread. if (!::PostQueuedCompletionStatus( job_port_.Get(), 0, THREAD_CTRL_GET_POLICY_INFO, reinterpret_cast(receiver.get()))) { receiver->OnError(SBOX_ERROR_GENERIC); return SBOX_ERROR_GENERIC; } // Ownership has passed to tracker thread. receiver.release(); return SBOX_ALL_OK; } bool BrokerServicesBase::DeriveCapabilitySidFromName(const wchar_t* name, PSID derived_sid, DWORD sid_buffer_length) { return ::CopySid(sid_buffer_length, derived_sid, Sid::FromNamedCapability(name).GetPSID()); } } // namespace sandbox