Source code
Revision control
Copy as Markdown
Other Tools
// Copyright 2022 The Chromium Authors.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <windows.h>
#include <winternl.h>
#include <cstring>
#include <memory>
#include <utility>
#include <vector>
#include "common/utils_win.h"
#include "client_win.h"
namespace content_analysis {
namespace sdk {
const DWORD kBufferSize = 4096;
// Use the same default timeout value (50ms) as CreateNamedPipeA(), expressed
// in 100ns intervals.
constexpr LONGLONG kDefaultTimeout = 500000;
// The following #defines and struct are copied from the official Microsoft
// Windows Driver Kit headers because they are not available in the official
// Microsoft Windows user mode SDK headers.
#define FSCTL_PIPE_WAIT 0x110018
#define STATUS_SUCCESS ((NTSTATUS)0)
#define STATUS_PIPE_NOT_AVAILABLE ((NTSTATUS)0xc00000ac)
#define STATUS_IO_TIMEOUT ((NTSTATUS)0xc00000b5)
typedef struct _FILE_PIPE_WAIT_FOR_BUFFER {
LARGE_INTEGER Timeout;
ULONG NameLength;
BOOLEAN TimeoutSpecified;
WCHAR Name[1];
} FILE_PIPE_WAIT_FOR_BUFFER, *PFILE_PIPE_WAIT_FOR_BUFFER;
namespace {
using NtCreateFileFn = decltype(&::NtCreateFile);
NtCreateFileFn GetNtCreateFileFn() {
static NtCreateFileFn fnNtCreateFile = []() {
NtCreateFileFn fn = nullptr;
HMODULE h = LoadLibraryA("NtDll.dll");
if (h != nullptr) {
fn = reinterpret_cast<NtCreateFileFn>(GetProcAddress(h, "NtCreateFile"));
FreeLibrary(h);
}
return fn;
}();
return fnNtCreateFile;
}
using NtFsControlFileFn = NTSTATUS (NTAPI *)(
HANDLE FileHandle,
HANDLE Event,
PIO_APC_ROUTINE ApcRoutine,
PVOID ApcContext,
PIO_STATUS_BLOCK IoStatusBlock,
ULONG IoControlCode,
PVOID InputBuffer,
ULONG InputBufferLength,
PVOID OutputBuffer,
ULONG OutputBufferLength);
NtFsControlFileFn GetNtFsControlFileFn() {
static NtFsControlFileFn fnNtFsControlFile = []() {
NtFsControlFileFn fn = nullptr;
HMODULE h = LoadLibraryA("NtDll.dll");
if (h != nullptr) {
fn = reinterpret_cast<NtFsControlFileFn>(GetProcAddress(h, "NtFsControlFile"));
FreeLibrary(h);
}
return fn;
}();
return fnNtFsControlFile;
}
NTSTATUS WaitForPipeAvailability(const UNICODE_STRING& path) {
NtCreateFileFn fnNtCreateFile = GetNtCreateFileFn();
if (fnNtCreateFile == nullptr) {
return false;
}
NtFsControlFileFn fnNtFsControlFile = GetNtFsControlFileFn();
if (fnNtFsControlFile == nullptr) {
return false;
}
// Build the device name. This is the initial part of `path` which is
// assumed to start with the string `kPipePrefixForClient`. The `Length`
// field is measured in bytes, not characters, and does not include the null
// terminator. It's important that the device name ends with a trailing
// backslash.
size_t device_name_char_length = std::strlen(internal::kPipePrefixForClient);
UNICODE_STRING device_name;
device_name.Buffer = path.Buffer;
device_name.Length = device_name_char_length * sizeof(wchar_t);
device_name.MaximumLength = device_name.Length;
// Build the pipe name. This is the remaining part of `path` after the device
// name.
UNICODE_STRING pipe_name;
pipe_name.Buffer = path.Buffer + device_name_char_length;
pipe_name.Length = path.Length - device_name.Length;
pipe_name.MaximumLength = pipe_name.Length;
// Build the ioctl input buffer. This buffer is the size of
// FILE_PIPE_WAIT_FOR_BUFFER plus the length of the pipe name. Since
// FILE_PIPE_WAIT_FOR_BUFFER includes one WCHAR this includes space for
// the terminating null character of the name which wcsncpy() copies.
size_t buffer_size = sizeof(FILE_PIPE_WAIT_FOR_BUFFER) + pipe_name.Length;
std::vector<char> buffer(buffer_size);
FILE_PIPE_WAIT_FOR_BUFFER* wait_buffer =
reinterpret_cast<FILE_PIPE_WAIT_FOR_BUFFER*>(buffer.data());
wait_buffer->Timeout.QuadPart = kDefaultTimeout;
wait_buffer->NameLength = pipe_name.Length;
wait_buffer->TimeoutSpecified = TRUE;
std::wcsncpy(wait_buffer->Name, pipe_name.Buffer, wait_buffer->NameLength /
sizeof(wchar_t));
OBJECT_ATTRIBUTES attr;
InitializeObjectAttributes(&attr, &device_name, OBJ_CASE_INSENSITIVE, nullptr,
nullptr);
IO_STATUS_BLOCK io;
HANDLE h = INVALID_HANDLE_VALUE;
NTSTATUS sts = fnNtCreateFile(&h, GENERIC_READ | GENERIC_WRITE | SYNCHRONIZE,
&attr, &io, /*AllocationSize=*/nullptr, FILE_ATTRIBUTE_NORMAL,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, FILE_OPEN,
FILE_SYNCHRONOUS_IO_NONALERT, /*EaBuffer=*/nullptr, /*EaLength=*/0);
if (sts != STATUS_SUCCESS) {
return false;
}
IO_STATUS_BLOCK io2;
sts = fnNtFsControlFile(h, /*Event=*/nullptr, /*ApcRoutine=*/nullptr,
/*ApcContext*/nullptr, &io2, FSCTL_PIPE_WAIT, buffer.data(),
buffer.size(), nullptr, 0);
CloseHandle(h);
return sts;
}
// Reads the next message from the pipe and returns a buffer of chars.
// This function is synchronous.
std::vector<char> ReadNextMessageFromPipe(
HANDLE pipe,
OVERLAPPED* overlapped) {
DWORD err = ERROR_SUCCESS;
std::vector<char> buffer(kBufferSize);
char* p = buffer.data();
int final_size = 0;
while (true) {
DWORD read;
// Even though the pipe is opened for overlapped IO, the read operation
// could still completely synchronously. For example, a server's response
// message could already be available in the pipe's internal buffer.
// If ReadFile() does complete synchronously, TRUE is returned. In this
// case update the final size and exit the loop.
if (ReadFile(pipe, p, kBufferSize, &read, overlapped)) {
final_size += read;
break;
} else {
// Reaching here means that ReadFile() will either complete async or
// an error has occurred. The former case is detected if the error code
// is "IO pending", in which case GetOverlappedResult() is called to wait
// for the IO to complete. If that function returns TRUE then the read
// operation completed successfully and the code simply updates the final
// size and exits the loop.
err = GetLastError();
if (err == ERROR_IO_PENDING) {
if (GetOverlappedResult(pipe, overlapped, &read, /*wait=*/TRUE)) {
final_size += read;
break;
} else {
err = GetLastError();
}
}
// Reaching here means an error has occurred. One error is recoverable:
// "more data". For any other type of error break out of the loop.
if (err != ERROR_MORE_DATA) {
final_size = 0;
break;
}
// Reaching here means the error is "more data", that is, the buffer
// specified in ReadFile() was too small to contain the entire response
// message from the server. ReadFile() has placed the start of the
// message in the specified buffer but ReadFile() needs to be called
// again to read the remaining part.
//
// The buffer size is increased and the current pointer into the buffer
// `p` is adjusted so that when the loop re-runs, it calls ReadFile()
// with the correct point in the buffer. It's possible that this loop
// might have to run many times if the response message is rather large.
buffer.resize(buffer.size() + kBufferSize);
p = buffer.data() + buffer.size() - kBufferSize;
}
}
buffer.resize(final_size);
return buffer;
}
// Writes a string to the pipe. Returns true if successful, false otherwise.
// This function is synchronous.
bool WriteMessageToPipe(
HANDLE pipe,
const std::string& message,
OVERLAPPED* overlapped) {
if (message.empty())
return false;
// Even though the pipe is opened for overlapped IO, the write operation
// could still completely synchronously. If it does, TRUE is returned.
// In this case the function is done.
bool ok = WriteFile(pipe, message.data(), message.size(), nullptr, overlapped);
if (!ok) {
// Reaching here means that WriteFile() will either complete async or
// an error has occurred. The former case is detected if the error code
// is "IO pending", in which case GetOverlappedResult() is called to wait
// for the IO to complete. Whether the operation completes sync or async,
// return true if the operation succeeded and false otherwise.
DWORD err = GetLastError();
if (err == ERROR_IO_PENDING) {
DWORD written;
ok = GetOverlappedResult(pipe, overlapped, &written, /*wait=*/TRUE);
}
}
return ok;
}
} // namespace
// static
std::unique_ptr<Client> Client::Create(Config config) {
int rc;
auto client = std::make_unique<ClientWin>(std::move(config), &rc);
return rc == 0 ? std::move(client) : nullptr;
}
ClientWin::ClientWin(Config config, int* rc) : ClientBase(std::move(config)) {
*rc = -1;
std::string pipename =
internal::GetPipeNameForClient(configuration().name,
configuration().user_specific);
if (!pipename.empty()) {
unsigned long pid = 0;
if (ConnectToPipe(pipename, &hPipe_) == ERROR_SUCCESS &&
GetNamedPipeServerProcessId(hPipe_, &pid)) {
agent_info().pid = pid;
// Getting the process path is best effort.
*rc = 0;
std::string binary_path;
if (internal::GetProcessPath(pid, &binary_path)) {
agent_info().binary_path = std::move(binary_path);
}
}
}
if (*rc != 0) {
Shutdown();
}
}
ClientWin::~ClientWin() {
Shutdown();
}
int ClientWin::Send(ContentAnalysisRequest request,
ContentAnalysisResponse* response) {
ChromeToAgent chrome_to_agent;
*chrome_to_agent.mutable_request() = std::move(request);
internal::ScopedOverlapped overlapped;
if (!overlapped.is_valid()) {
return -1;
}
bool success = WriteMessageToPipe(hPipe_,
chrome_to_agent.SerializeAsString(),
overlapped);
if (success) {
std::vector<char> buffer = ReadNextMessageFromPipe(hPipe_, overlapped);
AgentToChrome agent_to_chrome;
success = buffer.size() > 0 &&
agent_to_chrome.ParseFromArray(buffer.data(), buffer.size());
if (success) {
*response = std::move(*agent_to_chrome.mutable_response());
}
}
return success ? 0 : -1;
}
int ClientWin::Acknowledge(const ContentAnalysisAcknowledgement& ack) {
// TODO: could avoid a copy by changing argument to be
// `ContentAnalysisAcknowledgement ack` and then using std::move() below and
// at call site.
ChromeToAgent chrome_to_agent;
*chrome_to_agent.mutable_ack() = ack;
internal::ScopedOverlapped overlapped;
if (!overlapped.is_valid()) {
return -1;
}
return WriteMessageToPipe(hPipe_, chrome_to_agent.SerializeAsString(), overlapped)
? 0 : -1;
}
int ClientWin::CancelRequests(const ContentAnalysisCancelRequests& cancel) {
// TODO: could avoid a copy by changing argument to be
// `ContentAnalysisCancelRequests cancel` and then using std::move() below and
// at call site.
ChromeToAgent chrome_to_agent;
*chrome_to_agent.mutable_cancel() = cancel;
internal::ScopedOverlapped overlapped;
if (!overlapped.is_valid()) {
return -1;
}
return WriteMessageToPipe(hPipe_, chrome_to_agent.SerializeAsString(), overlapped)
? 0 : -1;
}
// static
DWORD ClientWin::ConnectToPipe(const std::string& pipename, HANDLE* handle) {
// Get pointers to the Ntxxx functions. This is required to use absolute
// pipe names from the Windows NT Object Manager's namespace. This protects
// against the "\\.\pipe" symlink being redirected.
NtCreateFileFn fnNtCreateFile = GetNtCreateFileFn();
if (fnNtCreateFile == nullptr) {
return ERROR_INVALID_FUNCTION;
}
// Convert the path to a wchar_t string. Pass pipename.size() as the
// `cbMultiByte` argument instead of -1 since the terminating null should not
// be counted. NtCreateFile() does not expect the object name to be
// terminated. Note that `buffer` and hence `name` created from it are both
// unterminated strings.
int wlen = MultiByteToWideChar(CP_ACP, 0, pipename.c_str(), pipename.size(),
nullptr, 0);
if (wlen == 0) {
return GetLastError();
}
std::vector<wchar_t> buffer(wlen);
MultiByteToWideChar(CP_ACP, 0, pipename.c_str(), pipename.size(),
buffer.data(), wlen);
UNICODE_STRING name;
name.Buffer = buffer.data();
name.Length = wlen * sizeof(wchar_t); // Length in bytes, not characters.
name.MaximumLength = name.Length;
OBJECT_ATTRIBUTES attr;
InitializeObjectAttributes(&attr, &name, OBJ_CASE_INSENSITIVE, nullptr,
nullptr);
// Open the named pipe for overlapped IO, i.e. do not specify either of the
// FILE_SYNCHRONOUS_IO_xxxALERT in the creation option flags. If the pipe
// is not opened for overlapped IO, then the Send() method will block if
// called from different threads since only one read or write operation would
// be allowed at a time.
IO_STATUS_BLOCK io;
HANDLE h = INVALID_HANDLE_VALUE;
NTSTATUS sts = STATUS_IO_TIMEOUT;
while (sts == STATUS_IO_TIMEOUT) {
sts = fnNtCreateFile(&h, GENERIC_READ | GENERIC_WRITE |
SYNCHRONIZE, &attr, &io, /*AllocationSize=*/nullptr,
FILE_ATTRIBUTE_NORMAL, /*ShareAccess=*/0, FILE_OPEN,
FILE_NON_DIRECTORY_FILE,
/*EaBuffer=*/nullptr, /*EaLength=*/0);
if (sts != STATUS_SUCCESS) {
if (sts != STATUS_PIPE_NOT_AVAILABLE) {
break;
}
sts = WaitForPipeAvailability(name);
if (sts != STATUS_SUCCESS && sts != STATUS_IO_TIMEOUT) {
break;
}
}
}
if (sts != STATUS_SUCCESS) {
return ERROR_PIPE_NOT_CONNECTED;
}
// Change to message read mode to match server side. Max connection count
// and timeout must be null if client and server are on the same machine.
DWORD mode = PIPE_READMODE_MESSAGE;
if (!SetNamedPipeHandleState(h, &mode,
/*maxCollectionCount=*/nullptr,
/*connectionTimeout=*/nullptr)) {
DWORD err = GetLastError();
CloseHandle(h);
return err;
}
*handle = h;
return ERROR_SUCCESS;
}
void ClientWin::Shutdown() {
if (hPipe_ != INVALID_HANDLE_VALUE) {
// TODO: This trips the LateWriteObserver. We could move this earlier
// (before the LateWriteObserver is created) or just remove it, although
// the later could mean an ACK message is not processed by the agent
// in time.
// FlushFileBuffers(hPipe_);
CloseHandle(hPipe_);
hPipe_ = INVALID_HANDLE_VALUE;
}
}
} // namespace sdk
} // namespace content_analysis