1f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)// Copyright 2014 The Chromium Authors. All rights reserved.
2f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)// Use of this source code is governed by a BSD-style license that can be
3f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)// found in the LICENSE file.
4f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)
5f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)#include "sandbox/mac/mach_message_server.h"
6f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)
7f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)#include <bsm/libbsm.h>
8f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)#include <servers/bootstrap.h>
9f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)
10f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)#include <string>
11f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)
12f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)#include "base/logging.h"
13f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)#include "base/mac/mach_logging.h"
14f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)#include "base/strings/stringprintf.h"
15116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch#include "sandbox/mac/dispatch_source_mach.h"
16f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)
17f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)namespace sandbox {
18f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)
19f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)MachMessageServer::MachMessageServer(
20f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)    MessageDemuxer* demuxer,
216d86b77056ed63eb6871182f42a9fd5f07550f90Torne (Richard Coles)    mach_port_t server_receive_right,
22f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)    mach_msg_size_t buffer_size)
23f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)    : demuxer_(demuxer),
246d86b77056ed63eb6871182f42a9fd5f07550f90Torne (Richard Coles)      server_port_(server_receive_right),
25f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)      buffer_size_(
26f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)          mach_vm_round_page(buffer_size + sizeof(mach_msg_audit_trailer_t))),
27f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)      did_forward_message_(false) {
28f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  DCHECK(demuxer_);
29f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)}
30f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)
31f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)MachMessageServer::~MachMessageServer() {
32f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)}
33f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)
34f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)bool MachMessageServer::Initialize() {
35f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  mach_port_t task = mach_task_self();
36f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  kern_return_t kr;
37f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)
386d86b77056ed63eb6871182f42a9fd5f07550f90Torne (Richard Coles)  // Allocate a port for use as a new server port if one was not passed to the
396d86b77056ed63eb6871182f42a9fd5f07550f90Torne (Richard Coles)  // constructor.
406d86b77056ed63eb6871182f42a9fd5f07550f90Torne (Richard Coles)  if (!server_port_.is_valid()) {
416d86b77056ed63eb6871182f42a9fd5f07550f90Torne (Richard Coles)    mach_port_t port;
426d86b77056ed63eb6871182f42a9fd5f07550f90Torne (Richard Coles)    if ((kr = mach_port_allocate(task, MACH_PORT_RIGHT_RECEIVE, &port)) !=
436d86b77056ed63eb6871182f42a9fd5f07550f90Torne (Richard Coles)            KERN_SUCCESS) {
446d86b77056ed63eb6871182f42a9fd5f07550f90Torne (Richard Coles)      MACH_LOG(ERROR, kr) << "Failed to allocate new server port.";
456d86b77056ed63eb6871182f42a9fd5f07550f90Torne (Richard Coles)      return false;
466d86b77056ed63eb6871182f42a9fd5f07550f90Torne (Richard Coles)    }
476d86b77056ed63eb6871182f42a9fd5f07550f90Torne (Richard Coles)    server_port_.reset(port);
48f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  }
49f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)
50f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  // Allocate the message request and reply buffers.
51f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  const int kMachMsgMemoryFlags = VM_MAKE_TAG(VM_MEMORY_MACH_MSG) |
52f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)                                  VM_FLAGS_ANYWHERE;
53f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  vm_address_t buffer = 0;
54f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)
55f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  kr = vm_allocate(task, &buffer, buffer_size_, kMachMsgMemoryFlags);
56f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  if (kr != KERN_SUCCESS) {
57f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)    MACH_LOG(ERROR, kr) << "Failed to allocate request buffer.";
58f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)    return false;
59f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  }
60f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  request_buffer_.reset(buffer, buffer_size_);
61f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)
62f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  kr = vm_allocate(task, &buffer, buffer_size_, kMachMsgMemoryFlags);
63f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  if (kr != KERN_SUCCESS) {
64f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)    MACH_LOG(ERROR, kr) << "Failed to allocate reply buffer.";
65f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)    return false;
66f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  }
67f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  reply_buffer_.reset(buffer, buffer_size_);
68f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)
69f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  // Set up the dispatch queue to service the bootstrap port.
70f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  std::string label = base::StringPrintf(
71f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)      "org.chromium.sandbox.MachMessageServer.%p", demuxer_);
72116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch  dispatch_source_.reset(new DispatchSourceMach(
73116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch      label.c_str(), server_port_.get(), ^{ ReceiveMessage(); }));
74116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch  dispatch_source_->Resume();
75f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)
76f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  return true;
77f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)}
78f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)
79116680a4aac90f2aa7413d9095a592090648e557Ben Murdochpid_t MachMessageServer::GetMessageSenderPID(IPCMessage request) {
80f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  // Get the PID of the task that sent this request. This requires getting at
81f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  // the trailer of the message, from the header.
82f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  mach_msg_audit_trailer_t* trailer =
83f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)      reinterpret_cast<mach_msg_audit_trailer_t*>(
84116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch          reinterpret_cast<vm_address_t>(request.mach) +
85116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch              round_msg(request.mach->msgh_size));
86f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  // TODO(rsesek): In the 10.7 SDK, there's audit_token_to_pid().
87f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  pid_t sender_pid;
88f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  audit_token_to_au32(trailer->msgh_audit,
89f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)      NULL, NULL, NULL, NULL, NULL, &sender_pid, NULL, NULL);
90f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  return sender_pid;
91f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)}
92f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)
93116680a4aac90f2aa7413d9095a592090648e557Ben MurdochIPCMessage MachMessageServer::CreateReply(IPCMessage request_message) {
94116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch  mach_msg_header_t* request = request_message.mach;
95116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch
96116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch  IPCMessage reply_message;
97116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch  mach_msg_header_t* reply = reply_message.mach =
98116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch      reinterpret_cast<mach_msg_header_t*>(reply_buffer_.address());
99116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch  bzero(reply, buffer_size_);
100116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch
101116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch  reply->msgh_bits = MACH_MSGH_BITS_REMOTE(reply->msgh_bits);
102116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch  // Since mach_msg will automatically swap the request and reply ports,
103116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch  // undo that.
104116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch  reply->msgh_remote_port = request->msgh_remote_port;
105116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch  reply->msgh_local_port = MACH_PORT_NULL;
106116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch  // MIG servers simply add 100 to the request ID to generate the reply ID.
107116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch  reply->msgh_id = request->msgh_id + 100;
108116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch
109116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch  return reply_message;
110116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch}
111116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch
112116680a4aac90f2aa7413d9095a592090648e557Ben Murdochbool MachMessageServer::SendReply(IPCMessage reply) {
113116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch  kern_return_t kr = mach_msg(reply.mach, MACH_SEND_MSG,
114116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch      reply.mach->msgh_size, 0, MACH_PORT_NULL, MACH_MSG_TIMEOUT_NONE,
115116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch      MACH_PORT_NULL);
116f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  MACH_LOG_IF(ERROR, kr != KERN_SUCCESS, kr)
117f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)      << "Unable to send intercepted reply message.";
118f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  return kr == KERN_SUCCESS;
119f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)}
120f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)
121116680a4aac90f2aa7413d9095a592090648e557Ben Murdochvoid MachMessageServer::ForwardMessage(IPCMessage message,
122f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)                                       mach_port_t destination) {
123116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch  mach_msg_header_t* request = message.mach;
124f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  request->msgh_local_port = request->msgh_remote_port;
125f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  request->msgh_remote_port = destination;
126f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  // Preserve the msgh_bits that do not deal with the local and remote ports.
127f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  request->msgh_bits = (request->msgh_bits & ~MACH_MSGH_BITS_PORTS_MASK) |
128f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)      MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, MACH_MSG_TYPE_MOVE_SEND_ONCE);
129f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  kern_return_t kr = mach_msg_send(request);
130f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  if (kr == KERN_SUCCESS) {
131f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)    did_forward_message_ = true;
132f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  } else {
133f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)    MACH_LOG(ERROR, kr) << "Unable to forward message to the real launchd.";
134f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  }
135f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)}
136f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)
137116680a4aac90f2aa7413d9095a592090648e557Ben Murdochvoid MachMessageServer::RejectMessage(IPCMessage request, int error_code) {
138116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch  IPCMessage reply = CreateReply(request);
139116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch  mig_reply_error_t* error_reply =
140116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch      reinterpret_cast<mig_reply_error_t*>(reply.mach);
141f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  error_reply->Head.msgh_size = sizeof(mig_reply_error_t);
142f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  error_reply->Head.msgh_bits =
143f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)      MACH_MSGH_BITS_REMOTE(MACH_MSG_TYPE_MOVE_SEND_ONCE);
144f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  error_reply->NDR = NDR_record;
145f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  error_reply->RetCode = error_code;
146116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch  SendReply(reply);
147116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch}
148116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch
149116680a4aac90f2aa7413d9095a592090648e557Ben Murdochmach_port_t MachMessageServer::GetServerPort() const {
150116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch  return server_port_.get();
151f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)}
152f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)
153f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)void MachMessageServer::ReceiveMessage() {
154f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  const mach_msg_options_t kRcvOptions = MACH_RCV_MSG |
155f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)      MACH_RCV_TRAILER_TYPE(MACH_MSG_TRAILER_FORMAT_0) |
156f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)      MACH_RCV_TRAILER_ELEMENTS(MACH_RCV_TRAILER_AUDIT);
157f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)
158f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  mach_msg_header_t* request =
159f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)      reinterpret_cast<mach_msg_header_t*>(request_buffer_.address());
160f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  mach_msg_header_t* reply =
161f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)      reinterpret_cast<mach_msg_header_t*>(reply_buffer_.address());
162f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)
163f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  // Zero out the buffers from handling any previous message.
164f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  bzero(request, buffer_size_);
165f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  bzero(reply, buffer_size_);
166f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  did_forward_message_ = false;
167f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)
168f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  // A Mach message server-once. The system library to run a message server
169f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  // cannot be used here, because some requests are conditionally forwarded
170f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  // to another server.
171f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  kern_return_t kr = mach_msg(request, kRcvOptions, 0, buffer_size_,
172f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)      server_port_.get(), MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
173f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  if (kr != KERN_SUCCESS) {
174f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)    MACH_LOG(ERROR, kr) << "Unable to receive message.";
175f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)    return;
176f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  }
177f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)
178f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  // Process the message.
179116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch  IPCMessage request_message = { request };
180116680a4aac90f2aa7413d9095a592090648e557Ben Murdoch  demuxer_->DemuxMessage(request_message);
181f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)
182f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  // Free any descriptors in the message body. If the message was forwarded,
183f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  // any descriptors would have been moved out of the process on send. If the
184f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  // forwarded message was sent from the process hosting this sandbox server,
185f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  // destroying the message could also destroy rights held outside the scope of
186f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  // this message server.
187f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  if (!did_forward_message_) {
188f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)    mach_msg_destroy(request);
189f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)    mach_msg_destroy(reply);
190f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)  }
191f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)}
192f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)
193f8ee788a64d60abd8f2d742a5fdedde054ecd910Torne (Richard Coles)}  // namespace sandbox
194