michael@0: // Copyright (c) 2012 The Chromium Authors. All rights reserved. michael@0: // Use of this source code is governed by a BSD-style license that can be michael@0: // found in the LICENSE file. michael@0: michael@0: #include "sandbox/win/src/broker_services.h" michael@0: michael@0: #include "base/logging.h" michael@0: #include "base/memory/scoped_ptr.h" michael@0: #include "base/threading/platform_thread.h" michael@0: #include "base/win/scoped_handle.h" michael@0: #include "base/win/scoped_process_information.h" michael@0: #include "base/win/startup_information.h" michael@0: #include "base/win/windows_version.h" michael@0: #include "sandbox/win/src/app_container.h" michael@0: #include "sandbox/win/src/process_mitigations.h" michael@0: #include "sandbox/win/src/sandbox_policy_base.h" michael@0: #include "sandbox/win/src/sandbox.h" michael@0: #include "sandbox/win/src/target_process.h" michael@0: #include "sandbox/win/src/win2k_threadpool.h" michael@0: #include "sandbox/win/src/win_utils.h" michael@0: michael@0: namespace { michael@0: michael@0: // Utility function to associate a completion port to a job object. michael@0: bool AssociateCompletionPort(HANDLE job, HANDLE port, void* key) { michael@0: JOBOBJECT_ASSOCIATE_COMPLETION_PORT job_acp = { key, port }; michael@0: return ::SetInformationJobObject(job, michael@0: JobObjectAssociateCompletionPortInformation, michael@0: &job_acp, sizeof(job_acp))? true : false; michael@0: } michael@0: michael@0: // Utility function to do the cleanup necessary when something goes wrong michael@0: // while in SpawnTarget and we must terminate the target process. michael@0: sandbox::ResultCode SpawnCleanup(sandbox::TargetProcess* target, DWORD error) { michael@0: if (0 == error) michael@0: error = ::GetLastError(); michael@0: michael@0: target->Terminate(); michael@0: delete target; michael@0: ::SetLastError(error); michael@0: return sandbox::SBOX_ERROR_GENERIC; michael@0: } michael@0: michael@0: // the different commands that you can send to the worker thread that michael@0: // executes TargetEventsThread(). michael@0: enum { michael@0: THREAD_CTRL_NONE, michael@0: THREAD_CTRL_REMOVE_PEER, michael@0: THREAD_CTRL_QUIT, michael@0: THREAD_CTRL_LAST, michael@0: }; michael@0: michael@0: // Helper structure that allows the Broker to associate a job notification michael@0: // with a job object and with a policy. michael@0: struct JobTracker { michael@0: HANDLE job; michael@0: sandbox::PolicyBase* policy; michael@0: JobTracker(HANDLE cjob, sandbox::PolicyBase* cpolicy) michael@0: : job(cjob), policy(cpolicy) { michael@0: } michael@0: }; michael@0: michael@0: // Helper structure that allows the broker to track peer processes michael@0: struct PeerTracker { michael@0: HANDLE wait_object; michael@0: base::win::ScopedHandle process; michael@0: DWORD id; michael@0: HANDLE job_port; michael@0: PeerTracker(DWORD process_id, HANDLE broker_job_port) michael@0: : wait_object(NULL), id(process_id), job_port(broker_job_port) { michael@0: } michael@0: }; michael@0: michael@0: void DeregisterPeerTracker(PeerTracker* peer) { michael@0: // Deregistration shouldn't fail, but we leak rather than crash if it does. michael@0: if (::UnregisterWaitEx(peer->wait_object, INVALID_HANDLE_VALUE)) { michael@0: delete peer; michael@0: } else { michael@0: NOTREACHED(); michael@0: } michael@0: } michael@0: michael@0: } // namespace michael@0: michael@0: namespace sandbox { michael@0: michael@0: BrokerServicesBase::BrokerServicesBase() michael@0: : thread_pool_(NULL), job_port_(NULL), no_targets_(NULL), michael@0: job_thread_(NULL) { michael@0: } michael@0: michael@0: // The broker uses a dedicated worker thread that services the job completion michael@0: // port to perform policy notifications and associated cleanup tasks. michael@0: ResultCode BrokerServicesBase::Init() { michael@0: if ((NULL != job_port_) || (NULL != thread_pool_)) michael@0: return SBOX_ERROR_UNEXPECTED_CALL; michael@0: michael@0: ::InitializeCriticalSection(&lock_); michael@0: michael@0: job_port_ = ::CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0); michael@0: if (NULL == job_port_) michael@0: return SBOX_ERROR_GENERIC; michael@0: michael@0: no_targets_ = ::CreateEventW(NULL, TRUE, FALSE, NULL); michael@0: michael@0: job_thread_ = ::CreateThread(NULL, 0, // Default security and stack. michael@0: TargetEventsThread, this, NULL, NULL); michael@0: if (NULL == job_thread_) michael@0: return SBOX_ERROR_GENERIC; michael@0: michael@0: return SBOX_ALL_OK; michael@0: } michael@0: michael@0: // The destructor should only be called when the Broker process is terminating. michael@0: // Since BrokerServicesBase is a singleton, this is called from the CRT michael@0: // termination handlers, if this code lives on a DLL it is called during michael@0: // DLL_PROCESS_DETACH in other words, holding the loader lock, so we cannot michael@0: // wait for threads here. michael@0: BrokerServicesBase::~BrokerServicesBase() { michael@0: // If there is no port Init() was never called successfully. michael@0: if (!job_port_) michael@0: return; michael@0: michael@0: // Closing the port causes, that no more Job notifications are delivered to michael@0: // the worker thread and also causes the thread to exit. This is what we michael@0: // want to do since we are going to close all outstanding Jobs and notifying michael@0: // the policy objects ourselves. michael@0: ::PostQueuedCompletionStatus(job_port_, 0, THREAD_CTRL_QUIT, FALSE); michael@0: ::CloseHandle(job_port_); michael@0: michael@0: if (WAIT_TIMEOUT == ::WaitForSingleObject(job_thread_, 1000)) { michael@0: // Cannot clean broker services. michael@0: NOTREACHED(); michael@0: return; michael@0: } michael@0: michael@0: JobTrackerList::iterator it; michael@0: for (it = tracker_list_.begin(); it != tracker_list_.end(); ++it) { michael@0: JobTracker* tracker = (*it); michael@0: FreeResources(tracker); michael@0: delete tracker; michael@0: } michael@0: ::CloseHandle(job_thread_); michael@0: delete thread_pool_; michael@0: ::CloseHandle(no_targets_); michael@0: michael@0: // Cancel the wait events and delete remaining peer trackers. michael@0: for (PeerTrackerMap::iterator it = peer_map_.begin(); michael@0: it != peer_map_.end(); ++it) { michael@0: DeregisterPeerTracker(it->second); michael@0: } michael@0: michael@0: // If job_port_ isn't NULL, assumes that the lock has been initialized. michael@0: if (job_port_) michael@0: ::DeleteCriticalSection(&lock_); michael@0: } michael@0: michael@0: TargetPolicy* BrokerServicesBase::CreatePolicy() { michael@0: // If you change the type of the object being created here you must also michael@0: // change the downcast to it in SpawnTarget(). michael@0: return new PolicyBase; michael@0: } michael@0: michael@0: void BrokerServicesBase::FreeResources(JobTracker* tracker) { michael@0: if (NULL != tracker->policy) { michael@0: BOOL res = ::TerminateJobObject(tracker->job, SBOX_ALL_OK); michael@0: DCHECK(res); michael@0: // Closing the job causes the target process to be destroyed so this michael@0: // needs to happen before calling OnJobEmpty(). michael@0: res = ::CloseHandle(tracker->job); michael@0: DCHECK(res); michael@0: // In OnJobEmpty() we don't actually use the job handle directly. michael@0: tracker->policy->OnJobEmpty(tracker->job); michael@0: tracker->policy->Release(); michael@0: tracker->policy = NULL; michael@0: } michael@0: } michael@0: michael@0: // The worker thread stays in a loop waiting for asynchronous notifications michael@0: // from the job objects. Right now we only care about knowing when the last michael@0: // process on a job terminates, but in general this is the place to tell michael@0: // the policy about events. michael@0: DWORD WINAPI BrokerServicesBase::TargetEventsThread(PVOID param) { michael@0: if (NULL == param) michael@0: return 1; michael@0: michael@0: base::PlatformThread::SetName("BrokerEvent"); michael@0: michael@0: BrokerServicesBase* broker = reinterpret_cast(param); michael@0: HANDLE port = broker->job_port_; michael@0: HANDLE no_targets = broker->no_targets_; michael@0: michael@0: int target_counter = 0; michael@0: ::ResetEvent(no_targets); michael@0: michael@0: while (true) { michael@0: DWORD events = 0; michael@0: ULONG_PTR key = 0; michael@0: LPOVERLAPPED ovl = NULL; michael@0: michael@0: if (!::GetQueuedCompletionStatus(port, &events, &key, &ovl, INFINITE)) michael@0: // this call fails if the port has been closed before we have a michael@0: // chance to service the last packet which is 'exit' anyway so michael@0: // this is not an error. michael@0: return 1; michael@0: michael@0: if (key > THREAD_CTRL_LAST) { michael@0: // The notification comes from a job object. There are nine notifications michael@0: // that jobs can send and some of them depend on the job attributes set. michael@0: JobTracker* tracker = reinterpret_cast(key); michael@0: michael@0: switch (events) { michael@0: case JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO: { michael@0: // The job object has signaled that the last process associated michael@0: // with it has terminated. Assuming there is no way for a process michael@0: // to appear out of thin air in this job, it safe to assume that michael@0: // we can tell the policy to destroy the target object, and for michael@0: // us to release our reference to the policy object. michael@0: FreeResources(tracker); michael@0: break; michael@0: } michael@0: michael@0: case JOB_OBJECT_MSG_NEW_PROCESS: { michael@0: ++target_counter; michael@0: if (1 == target_counter) { michael@0: ::ResetEvent(no_targets); michael@0: } michael@0: break; michael@0: } michael@0: michael@0: case JOB_OBJECT_MSG_EXIT_PROCESS: michael@0: case JOB_OBJECT_MSG_ABNORMAL_EXIT_PROCESS: { michael@0: { michael@0: AutoLock lock(&broker->lock_); michael@0: broker->child_process_ids_.erase(reinterpret_cast(ovl)); michael@0: } michael@0: --target_counter; michael@0: if (0 == target_counter) michael@0: ::SetEvent(no_targets); michael@0: michael@0: DCHECK(target_counter >= 0); michael@0: break; michael@0: } michael@0: michael@0: case JOB_OBJECT_MSG_ACTIVE_PROCESS_LIMIT: { michael@0: break; michael@0: } michael@0: michael@0: default: { michael@0: NOTREACHED(); michael@0: break; michael@0: } michael@0: } michael@0: } else if (THREAD_CTRL_REMOVE_PEER == key) { michael@0: // Remove a process from our list of peers. michael@0: AutoLock lock(&broker->lock_); michael@0: PeerTrackerMap::iterator it = michael@0: broker->peer_map_.find(reinterpret_cast(ovl)); michael@0: DeregisterPeerTracker(it->second); michael@0: broker->peer_map_.erase(it); michael@0: } else if (THREAD_CTRL_QUIT == key) { michael@0: // The broker object is being destroyed so the thread needs to exit. michael@0: return 0; michael@0: } else { michael@0: // We have not implemented more commands. michael@0: NOTREACHED(); michael@0: } michael@0: } michael@0: michael@0: NOTREACHED(); michael@0: return 0; michael@0: } michael@0: michael@0: // SpawnTarget does all the interesting sandbox setup and creates the target michael@0: // process inside the sandbox. michael@0: ResultCode BrokerServicesBase::SpawnTarget(const wchar_t* exe_path, michael@0: const wchar_t* command_line, michael@0: TargetPolicy* policy, michael@0: PROCESS_INFORMATION* target_info) { michael@0: if (!exe_path) michael@0: return SBOX_ERROR_BAD_PARAMS; michael@0: michael@0: if (!policy) michael@0: return SBOX_ERROR_BAD_PARAMS; michael@0: michael@0: // Even though the resources touched by SpawnTarget can be accessed in michael@0: // multiple threads, the method itself cannot be called from more than michael@0: // 1 thread. This is to protect the global variables used while setting up michael@0: // the child process. michael@0: static DWORD thread_id = ::GetCurrentThreadId(); michael@0: DCHECK(thread_id == ::GetCurrentThreadId()); michael@0: michael@0: AutoLock lock(&lock_); michael@0: michael@0: // This downcast is safe as long as we control CreatePolicy() michael@0: PolicyBase* policy_base = static_cast(policy); michael@0: michael@0: // Construct the tokens and the job object that we are going to associate michael@0: // with the soon to be created target process. michael@0: HANDLE initial_token_temp; michael@0: HANDLE lockdown_token_temp; michael@0: ResultCode result = policy_base->MakeTokens(&initial_token_temp, michael@0: &lockdown_token_temp); michael@0: if (SBOX_ALL_OK != result) michael@0: return result; michael@0: michael@0: base::win::ScopedHandle initial_token(initial_token_temp); michael@0: base::win::ScopedHandle lockdown_token(lockdown_token_temp); michael@0: michael@0: HANDLE job_temp; michael@0: result = policy_base->MakeJobObject(&job_temp); michael@0: if (SBOX_ALL_OK != result) michael@0: return result; michael@0: michael@0: base::win::ScopedHandle job(job_temp); michael@0: michael@0: // Initialize the startup information from the policy. michael@0: base::win::StartupInformation startup_info; michael@0: string16 desktop = policy_base->GetAlternateDesktop(); michael@0: if (!desktop.empty()) { michael@0: startup_info.startup_info()->lpDesktop = michael@0: const_cast(desktop.c_str()); michael@0: } michael@0: michael@0: bool inherit_handles = false; michael@0: if (base::win::GetVersion() >= base::win::VERSION_VISTA) { michael@0: int attribute_count = 0; michael@0: const AppContainerAttributes* app_container = michael@0: policy_base->GetAppContainer(); michael@0: if (app_container) michael@0: ++attribute_count; michael@0: michael@0: DWORD64 mitigations; michael@0: size_t mitigations_size; michael@0: ConvertProcessMitigationsToPolicy(policy->GetProcessMitigations(), michael@0: &mitigations, &mitigations_size); michael@0: if (mitigations) michael@0: ++attribute_count; michael@0: michael@0: HANDLE stdout_handle = policy_base->GetStdoutHandle(); michael@0: HANDLE stderr_handle = policy_base->GetStderrHandle(); michael@0: HANDLE inherit_handle_list[2]; michael@0: int inherit_handle_count = 0; michael@0: if (stdout_handle != INVALID_HANDLE_VALUE) michael@0: inherit_handle_list[inherit_handle_count++] = stdout_handle; michael@0: // Handles in the list must be unique. michael@0: if (stderr_handle != stdout_handle && stderr_handle != INVALID_HANDLE_VALUE) michael@0: inherit_handle_list[inherit_handle_count++] = stderr_handle; michael@0: if (inherit_handle_count) michael@0: ++attribute_count; michael@0: michael@0: if (!startup_info.InitializeProcThreadAttributeList(attribute_count)) michael@0: return SBOX_ERROR_PROC_THREAD_ATTRIBUTES; michael@0: michael@0: if (app_container) { michael@0: result = app_container->ShareForStartup(&startup_info); michael@0: if (SBOX_ALL_OK != result) michael@0: return result; michael@0: } michael@0: michael@0: if (mitigations) { michael@0: if (!startup_info.UpdateProcThreadAttribute( michael@0: PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY, &mitigations, michael@0: mitigations_size)) { michael@0: return SBOX_ERROR_PROC_THREAD_ATTRIBUTES; michael@0: } michael@0: } michael@0: michael@0: if (inherit_handle_count) { michael@0: if (!startup_info.UpdateProcThreadAttribute( michael@0: PROC_THREAD_ATTRIBUTE_HANDLE_LIST, michael@0: inherit_handle_list, michael@0: sizeof(inherit_handle_list[0]) * inherit_handle_count)) { michael@0: return SBOX_ERROR_PROC_THREAD_ATTRIBUTES; michael@0: } michael@0: startup_info.startup_info()->dwFlags |= STARTF_USESTDHANDLES; michael@0: startup_info.startup_info()->hStdInput = INVALID_HANDLE_VALUE; michael@0: startup_info.startup_info()->hStdOutput = stdout_handle; michael@0: startup_info.startup_info()->hStdError = stderr_handle; michael@0: // Allowing inheritance of handles is only secure now that we michael@0: // have limited which handles will be inherited. michael@0: inherit_handles = true; michael@0: } michael@0: } michael@0: michael@0: // Construct the thread pool here in case it is expensive. michael@0: // The thread pool is shared by all the targets michael@0: if (NULL == thread_pool_) michael@0: thread_pool_ = new Win2kThreadPool(); michael@0: michael@0: // Create the TargetProces object and spawn the target suspended. Note that michael@0: // Brokerservices does not own the target object. It is owned by the Policy. michael@0: base::win::ScopedProcessInformation process_info; michael@0: TargetProcess* target = new TargetProcess(initial_token.Take(), michael@0: lockdown_token.Take(), michael@0: job, michael@0: thread_pool_); michael@0: michael@0: DWORD win_result = target->Create(exe_path, command_line, inherit_handles, michael@0: startup_info, &process_info); michael@0: if (ERROR_SUCCESS != win_result) michael@0: return SpawnCleanup(target, win_result); michael@0: michael@0: // Now the policy is the owner of the target. michael@0: if (!policy_base->AddTarget(target)) { michael@0: return SpawnCleanup(target, 0); michael@0: } michael@0: michael@0: // We are going to keep a pointer to the policy because we'll call it when michael@0: // the job object generates notifications using the completion port. michael@0: policy_base->AddRef(); michael@0: if (job.IsValid()) { michael@0: scoped_ptr tracker(new JobTracker(job.Take(), policy_base)); michael@0: if (!AssociateCompletionPort(tracker->job, job_port_, tracker.get())) michael@0: return SpawnCleanup(target, 0); michael@0: // Save the tracker because in cleanup we might need to force closing michael@0: // the Jobs. michael@0: tracker_list_.push_back(tracker.release()); michael@0: child_process_ids_.insert(process_info.process_id()); michael@0: } else { michael@0: // We have to signal the event once here because the completion port will michael@0: // never get a message that this target is being terminated thus we should michael@0: // not block WaitForAllTargets until we have at least one target with job. michael@0: if (child_process_ids_.empty()) michael@0: ::SetEvent(no_targets_); michael@0: // We can not track the life time of such processes and it is responsibility michael@0: // of the host application to make sure that spawned targets without jobs michael@0: // are terminated when the main application don't need them anymore. michael@0: } michael@0: michael@0: *target_info = process_info.Take(); michael@0: return SBOX_ALL_OK; michael@0: } michael@0: michael@0: michael@0: ResultCode BrokerServicesBase::WaitForAllTargets() { michael@0: ::WaitForSingleObject(no_targets_, INFINITE); michael@0: return SBOX_ALL_OK; michael@0: } michael@0: michael@0: bool BrokerServicesBase::IsActiveTarget(DWORD process_id) { michael@0: AutoLock lock(&lock_); michael@0: return child_process_ids_.find(process_id) != child_process_ids_.end() || michael@0: peer_map_.find(process_id) != peer_map_.end(); michael@0: } michael@0: michael@0: VOID CALLBACK BrokerServicesBase::RemovePeer(PVOID parameter, BOOLEAN timeout) { michael@0: PeerTracker* peer = reinterpret_cast(parameter); michael@0: // Don't check the return code because we this may fail (safely) at shutdown. michael@0: ::PostQueuedCompletionStatus(peer->job_port, 0, THREAD_CTRL_REMOVE_PEER, michael@0: reinterpret_cast(peer->id)); michael@0: } michael@0: michael@0: ResultCode BrokerServicesBase::AddTargetPeer(HANDLE peer_process) { michael@0: scoped_ptr peer(new PeerTracker(::GetProcessId(peer_process), michael@0: job_port_)); michael@0: if (!peer->id) michael@0: return SBOX_ERROR_GENERIC; michael@0: michael@0: HANDLE process_handle; michael@0: if (!::DuplicateHandle(::GetCurrentProcess(), peer_process, michael@0: ::GetCurrentProcess(), &process_handle, michael@0: SYNCHRONIZE, FALSE, 0)) { michael@0: return SBOX_ERROR_GENERIC; michael@0: } michael@0: peer->process.Set(process_handle); michael@0: michael@0: AutoLock lock(&lock_); michael@0: if (!peer_map_.insert(std::make_pair(peer->id, peer.get())).second) michael@0: return SBOX_ERROR_BAD_PARAMS; michael@0: michael@0: if (!::RegisterWaitForSingleObject( michael@0: &peer->wait_object, peer->process, RemovePeer, peer.get(), INFINITE, michael@0: WT_EXECUTEONLYONCE | WT_EXECUTEINWAITTHREAD)) { michael@0: peer_map_.erase(peer->id); michael@0: return SBOX_ERROR_GENERIC; michael@0: } michael@0: michael@0: // Release the pointer since it will be cleaned up by the callback. michael@0: peer.release(); michael@0: return SBOX_ALL_OK; michael@0: } michael@0: michael@0: ResultCode BrokerServicesBase::InstallAppContainer(const wchar_t* sid, michael@0: const wchar_t* name) { michael@0: if (base::win::OSInfo::GetInstance()->version() < base::win::VERSION_WIN8) michael@0: return SBOX_ERROR_UNSUPPORTED; michael@0: michael@0: string16 old_name = LookupAppContainer(sid); michael@0: if (old_name.empty()) michael@0: return CreateAppContainer(sid, name); michael@0: michael@0: if (old_name != name) michael@0: return SBOX_ERROR_INVALID_APP_CONTAINER; michael@0: michael@0: return SBOX_ALL_OK; michael@0: } michael@0: michael@0: ResultCode BrokerServicesBase::UninstallAppContainer(const wchar_t* sid) { michael@0: if (base::win::OSInfo::GetInstance()->version() < base::win::VERSION_WIN8) michael@0: return SBOX_ERROR_UNSUPPORTED; michael@0: michael@0: string16 name = LookupAppContainer(sid); michael@0: if (name.empty()) michael@0: return SBOX_ERROR_INVALID_APP_CONTAINER; michael@0: michael@0: return DeleteAppContainer(sid); michael@0: } michael@0: michael@0: } // namespace sandbox