blob: 45d81cf7b2ca2a5826097dc0c7d93344cf48b3c0 [file] [log] [blame]
// 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.
//
// This file implements the Windows service controlling Me2Me host processes
// running within user sessions.
#include "remoting/host/wts_session_process_launcher_win.h"
#include <windows.h>
#include <sddl.h>
#include <limits>
#include "base/logging.h"
#include "base/process_util.h"
#include "base/rand_util.h"
#include "base/string16.h"
#include "base/stringprintf.h"
#include "base/threading/thread.h"
#include "base/utf_string_conversions.h"
#include "base/win/scoped_handle.h"
#include "ipc/ipc_channel_proxy.h"
#include "ipc/ipc_message.h"
#include "ipc/ipc_message_macros.h"
#include "remoting/host/chromoting_messages.h"
#include "remoting/host/sas_injector.h"
#include "remoting/host/wts_console_monitor_win.h"
using base::win::ScopedHandle;
using base::TimeDelta;
namespace {
// The minimum and maximum delays between attempts to inject host process into
// a session.
const int kMaxLaunchDelaySeconds = 60;
const int kMinLaunchDelaySeconds = 1;
// Name of the default session desktop.
const char kDefaultDesktopName[] = "winsta0\\default";
// Match the pipe name prefix used by Chrome IPC channels.
const char kChromePipeNamePrefix[] = "\\\\.\\pipe\\chrome.";
// Generates the command line of the host process.
const char kHostProcessCommandLineFormat[] = "\"%ls\" --chromoting-ipc=%ls";
// The security descriptor of the Chromoting IPC channel. It gives full access
// to LocalSystem and denies access by anyone else.
const char kChromotingChannelSecurityDescriptor[] =
"O:SY" "G:SY" "D:(A;;GA;;;SY)";
// Takes the process token and makes a copy of it. The returned handle will have
// |desired_access| rights.
bool CopyProcessToken(DWORD desired_access,
ScopedHandle* token_out) {
HANDLE handle;
if (!OpenProcessToken(GetCurrentProcess(),
TOKEN_DUPLICATE | desired_access,
&handle)) {
LOG_GETLASTERROR(ERROR) << "Failed to open process token";
return false;
}
ScopedHandle process_token(handle);
if (!DuplicateTokenEx(process_token,
desired_access,
NULL,
SecurityImpersonation,
TokenPrimary,
&handle)) {
LOG_GETLASTERROR(ERROR) << "Failed to duplicate the process token";
return false;
}
token_out->Set(handle);
return true;
}
// Creates a copy of the current process with SE_TCB_NAME privilege enabled.
bool CreatePrivilegedToken(ScopedHandle* token_out) {
ScopedHandle privileged_token;
DWORD desired_access = TOKEN_ADJUST_PRIVILEGES | TOKEN_IMPERSONATE |
TOKEN_DUPLICATE | TOKEN_QUERY;
if (!CopyProcessToken(desired_access, &privileged_token)) {
return false;
}
// Get the LUID for the SE_TCB_NAME privilege.
TOKEN_PRIVILEGES state;
state.PrivilegeCount = 1;
state.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
if (!LookupPrivilegeValue(NULL, SE_TCB_NAME, &state.Privileges[0].Luid)) {
LOG_GETLASTERROR(ERROR) <<
"Failed to lookup the LUID for the SE_TCB_NAME privilege";
return false;
}
// Enable the SE_TCB_NAME privilege.
if (!AdjustTokenPrivileges(privileged_token, FALSE, &state, 0, NULL, 0)) {
LOG_GETLASTERROR(ERROR) <<
"Failed to enable SE_TCB_NAME privilege in a token";
return false;
}
token_out->Set(privileged_token.Take());
return true;
}
// Creates a copy of the current process token for the given |session_id| so
// it can be used to launch a process in that session.
bool CreateSessionToken(uint32 session_id,
ScopedHandle* token_out) {
ScopedHandle session_token;
DWORD desired_access = TOKEN_ADJUST_DEFAULT | TOKEN_ADJUST_SESSIONID |
TOKEN_ASSIGN_PRIMARY | TOKEN_QUERY;
if (!CopyProcessToken(desired_access, &session_token)) {
return false;
}
// Change the session ID of the token.
DWORD new_session_id = session_id;
if (!SetTokenInformation(session_token,
TokenSessionId,
&new_session_id,
sizeof(new_session_id))) {
LOG_GETLASTERROR(ERROR) <<
"Failed to change session ID of a token";
return false;
}
token_out->Set(session_token.Take());
return true;
}
// Generates random channel ID.
// N.B. Stolen from src/content/common/child_process_host_impl.cc
string16 GenerateRandomChannelId(void* instance) {
return base::StringPrintf(ASCIIToUTF16("%d.%p.%d").c_str(),
base::GetCurrentProcId(), instance,
base::RandInt(0, std::numeric_limits<int>::max()));
}
// Creates the server end of the Chromoting IPC channel.
// N.B. This code is based on IPC::Channel's implementation.
bool CreatePipeForIpcChannel(void* instance,
string16* channel_name_out,
ScopedHandle* pipe_out) {
// Create security descriptor for the channel.
SECURITY_ATTRIBUTES security_attributes;
security_attributes.nLength = sizeof(security_attributes);
security_attributes.bInheritHandle = FALSE;
ULONG security_descriptor_length = 0;
if (!ConvertStringSecurityDescriptorToSecurityDescriptorA(
kChromotingChannelSecurityDescriptor,
SDDL_REVISION_1,
reinterpret_cast<PSECURITY_DESCRIPTOR*>(
&security_attributes.lpSecurityDescriptor),
&security_descriptor_length)) {
LOG_GETLASTERROR(ERROR) <<
"Failed to create a security descriptor for the Chromoting IPC channel";
return false;
}
// Generate a random channel name.
string16 channel_name(GenerateRandomChannelId(instance));
// Convert it to the pipe name.
string16 pipe_name(ASCIIToUTF16(kChromePipeNamePrefix));
pipe_name.append(channel_name);
// Create the server end of the pipe. This code should match the code in
// IPC::Channel with exception of passing a non-default security descriptor.
HANDLE pipe = CreateNamedPipeW(pipe_name.c_str(),
PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED |
FILE_FLAG_FIRST_PIPE_INSTANCE,
PIPE_TYPE_BYTE | PIPE_READMODE_BYTE,
1,
IPC::Channel::kReadBufferSize,
IPC::Channel::kReadBufferSize,
5000,
&security_attributes);
if (pipe == INVALID_HANDLE_VALUE) {
LOG_GETLASTERROR(ERROR) <<
"Failed to create the server end of the Chromoting IPC channel";
LocalFree(security_attributes.lpSecurityDescriptor);
return false;
}
LocalFree(security_attributes.lpSecurityDescriptor);
*channel_name_out = channel_name;
pipe_out->Set(pipe);
return true;
}
// Launches |binary| in the security context of the supplied |user_token|.
bool LaunchProcessAsUser(const FilePath& binary,
const string16& command_line,
HANDLE user_token,
base::Process* process_out) {
string16 application_name = binary.value();
string16 desktop = ASCIIToUTF16(kDefaultDesktopName);
PROCESS_INFORMATION process_info;
STARTUPINFOW startup_info;
memset(&startup_info, 0, sizeof(startup_info));
startup_info.cb = sizeof(startup_info);
startup_info.lpDesktop = const_cast<LPWSTR>(desktop.c_str());
if (!CreateProcessAsUserW(user_token,
application_name.c_str(),
const_cast<LPWSTR>(command_line.c_str()),
NULL,
NULL,
FALSE,
0,
NULL,
NULL,
&startup_info,
&process_info)) {
LOG_GETLASTERROR(ERROR) <<
"Failed to launch a process with a user token";
return false;
}
CloseHandle(process_info.hThread);
process_out->set_handle(process_info.hProcess);
return true;
}
} // namespace
namespace remoting {
WtsSessionProcessLauncher::WtsSessionProcessLauncher(
WtsConsoleMonitor* monitor,
const FilePath& host_binary,
base::Thread* io_thread)
: host_binary_(host_binary),
io_thread_(io_thread),
monitor_(monitor),
state_(StateDetached) {
monitor_->AddWtsConsoleObserver(this);
}
WtsSessionProcessLauncher::~WtsSessionProcessLauncher() {
DCHECK(state_ == StateDetached);
DCHECK(!timer_.IsRunning());
DCHECK(process_.handle() == NULL);
DCHECK(process_watcher_.GetWatchedObject() == NULL);
DCHECK(chromoting_channel_.get() == NULL);
monitor_->RemoveWtsConsoleObserver(this);
}
void WtsSessionProcessLauncher::LaunchProcess() {
DCHECK(state_ == StateStarting);
DCHECK(!timer_.IsRunning());
DCHECK(process_.handle() == NULL);
DCHECK(process_watcher_.GetWatchedObject() == NULL);
DCHECK(chromoting_channel_.get() == NULL);
launch_time_ = base::Time::Now();
string16 channel_name;
ScopedHandle pipe;
if (CreatePipeForIpcChannel(this, &channel_name, &pipe)) {
// Wrap the pipe into an IPC channel.
chromoting_channel_.reset(new IPC::ChannelProxy(
IPC::ChannelHandle(pipe.Get()),
IPC::Channel::MODE_SERVER,
this,
io_thread_->message_loop_proxy().get()));
string16 command_line =
base::StringPrintf(ASCIIToUTF16(kHostProcessCommandLineFormat).c_str(),
host_binary_.value().c_str(),
channel_name.c_str());
// Try to launch the process and attach an object watcher to the returned
// handle so that we get notified when the process terminates.
if (LaunchProcessAsUser(host_binary_, command_line, session_token_,
&process_)) {
if (process_watcher_.StartWatching(process_.handle(), this)) {
state_ = StateAttached;
return;
} else {
LOG(ERROR) << "Failed to arm the process watcher.";
process_.Terminate(0);
process_.Close();
}
}
chromoting_channel_.reset();
}
// Something went wrong. Try to launch the host again later. The attempts rate
// is limited by exponential backoff.
launch_backoff_ = std::max(launch_backoff_ * 2,
TimeDelta::FromSeconds(kMinLaunchDelaySeconds));
launch_backoff_ = std::min(launch_backoff_,
TimeDelta::FromSeconds(kMaxLaunchDelaySeconds));
timer_.Start(FROM_HERE, launch_backoff_,
this, &WtsSessionProcessLauncher::LaunchProcess);
}
void WtsSessionProcessLauncher::OnObjectSignaled(HANDLE object) {
DCHECK(state_ == StateAttached);
DCHECK(!timer_.IsRunning());
DCHECK(process_.handle() != NULL);
DCHECK(process_watcher_.GetWatchedObject() == NULL);
DCHECK(chromoting_channel_.get() != NULL);
// The host process has been terminated for some reason. The handle can now be
// closed.
process_.Close();
chromoting_channel_.reset();
// Expand the backoff interval if the process has died quickly or reset it if
// it was up longer than the maximum backoff delay.
base::TimeDelta delta = base::Time::Now() - launch_time_;
if (delta < base::TimeDelta() ||
delta >= base::TimeDelta::FromSeconds(kMaxLaunchDelaySeconds)) {
launch_backoff_ = base::TimeDelta();
} else {
launch_backoff_ = std::max(launch_backoff_ * 2,
TimeDelta::FromSeconds(kMinLaunchDelaySeconds));
launch_backoff_ = std::min(launch_backoff_,
TimeDelta::FromSeconds(kMaxLaunchDelaySeconds));
}
// Try to restart the host.
state_ = StateStarting;
timer_.Start(FROM_HERE, launch_backoff_,
this, &WtsSessionProcessLauncher::LaunchProcess);
}
bool WtsSessionProcessLauncher::OnMessageReceived(const IPC::Message& message) {
bool handled = true;
IPC_BEGIN_MESSAGE_MAP(WtsSessionProcessLauncher, message)
IPC_MESSAGE_HANDLER(ChromotingHostMsg_SendSasToConsole,
OnSendSasToConsole)
IPC_MESSAGE_UNHANDLED(handled = false)
IPC_END_MESSAGE_MAP()
return handled;
}
void WtsSessionProcessLauncher::OnSendSasToConsole() {
if (state_ == StateAttached) {
if (sas_injector_.get() == NULL) {
sas_injector_ = SasInjector::Create();
}
if (sas_injector_.get() != NULL) {
sas_injector_->InjectSas();
}
}
}
void WtsSessionProcessLauncher::OnSessionAttached(uint32 session_id) {
DCHECK(state_ == StateDetached);
DCHECK(!timer_.IsRunning());
DCHECK(process_.handle() == NULL);
DCHECK(process_watcher_.GetWatchedObject() == NULL);
DCHECK(chromoting_channel_.get() == NULL);
// Temporarily enable the SE_TCB_NAME privilege. The privileged token is
// created as needed and kept for later reuse.
if (privileged_token_.Get() == NULL) {
if (!CreatePrivilegedToken(&privileged_token_)) {
return;
}
}
if (!ImpersonateLoggedOnUser(privileged_token_)) {
LOG_GETLASTERROR(ERROR) <<
"Failed to impersonate the privileged token";
return;
}
// While the SE_TCB_NAME privilege is enabled, create a session token for
// the launched process.
bool result = CreateSessionToken(session_id, &session_token_);
// Revert to the default token. The default token is sufficient to call
// CreateProcessAsUser() successfully.
CHECK(RevertToSelf());
if (!result)
return;
// Now try to launch the host.
state_ = StateStarting;
LaunchProcess();
}
void WtsSessionProcessLauncher::OnSessionDetached() {
DCHECK(state_ == StateDetached ||
state_ == StateStarting ||
state_ == StateAttached);
switch (state_) {
case StateDetached:
DCHECK(!timer_.IsRunning());
DCHECK(process_.handle() == NULL);
DCHECK(process_watcher_.GetWatchedObject() == NULL);
DCHECK(chromoting_channel_.get() == NULL);
break;
case StateStarting:
DCHECK(timer_.IsRunning());
DCHECK(process_.handle() == NULL);
DCHECK(process_watcher_.GetWatchedObject() == NULL);
DCHECK(chromoting_channel_.get() == NULL);
timer_.Stop();
launch_backoff_ = base::TimeDelta();
state_ = StateDetached;
break;
case StateAttached:
DCHECK(!timer_.IsRunning());
DCHECK(process_.handle() != NULL);
DCHECK(process_watcher_.GetWatchedObject() != NULL);
DCHECK(chromoting_channel_.get() != NULL);
process_watcher_.StopWatching();
process_.Terminate(0);
process_.Close();
chromoting_channel_.reset();
state_ = StateDetached;
break;
}
}
} // namespace remoting