OLD | NEW |
| (Empty) |
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | |
2 // Use of this source code is governed by a BSD-style license that can be | |
3 // found in the LICENSE file. | |
4 | |
5 #include "ipc/ipc_channel_win.h" | |
6 | |
7 #include <windows.h> | |
8 #include <stddef.h> | |
9 #include <stdint.h> | |
10 | |
11 #include "base/auto_reset.h" | |
12 #include "base/bind.h" | |
13 #include "base/compiler_specific.h" | |
14 #include "base/logging.h" | |
15 #include "base/memory/ptr_util.h" | |
16 #include "base/pickle.h" | |
17 #include "base/process/process_handle.h" | |
18 #include "base/rand_util.h" | |
19 #include "base/single_thread_task_runner.h" | |
20 #include "base/strings/string_number_conversions.h" | |
21 #include "base/strings/utf_string_conversions.h" | |
22 #include "base/threading/thread_checker.h" | |
23 #include "base/threading/thread_task_runner_handle.h" | |
24 #include "base/win/scoped_handle.h" | |
25 #include "ipc/attachment_broker.h" | |
26 #include "ipc/ipc_listener.h" | |
27 #include "ipc/ipc_logging.h" | |
28 #include "ipc/ipc_message_attachment_set.h" | |
29 #include "ipc/ipc_message_utils.h" | |
30 | |
31 namespace IPC { | |
32 | |
33 ChannelWin::State::State() = default; | |
34 | |
35 ChannelWin::State::~State() { | |
36 static_assert(offsetof(ChannelWin::State, context) == 0, | |
37 "ChannelWin::State should have context as its first data" | |
38 "member."); | |
39 } | |
40 | |
41 ChannelWin::ChannelWin(const IPC::ChannelHandle& channel_handle, | |
42 Mode mode, | |
43 Listener* listener) | |
44 : ChannelReader(listener), | |
45 peer_pid_(base::kNullProcessId), | |
46 waiting_connect_(mode & MODE_SERVER_FLAG), | |
47 processing_incoming_(false), | |
48 validate_client_(false), | |
49 client_secret_(0), | |
50 weak_factory_(this) { | |
51 CreatePipe(channel_handle, mode); | |
52 } | |
53 | |
54 ChannelWin::~ChannelWin() { | |
55 CleanUp(); | |
56 Close(); | |
57 } | |
58 | |
59 void ChannelWin::Close() { | |
60 if (thread_check_.get()) | |
61 DCHECK(thread_check_->CalledOnValidThread()); | |
62 | |
63 if (input_state_.is_pending || output_state_.is_pending) | |
64 CancelIo(pipe_.Get()); | |
65 | |
66 // Closing the handle at this point prevents us from issuing more requests | |
67 // form OnIOCompleted(). | |
68 if (pipe_.IsValid()) | |
69 pipe_.Close(); | |
70 | |
71 // Make sure all IO has completed. | |
72 while (input_state_.is_pending || output_state_.is_pending) { | |
73 base::MessageLoopForIO::current()->WaitForIOCompletion(INFINITE, this); | |
74 } | |
75 | |
76 while (!output_queue_.empty()) { | |
77 OutputElement* element = output_queue_.front(); | |
78 output_queue_.pop(); | |
79 delete element; | |
80 } | |
81 } | |
82 | |
83 bool ChannelWin::Send(Message* message) { | |
84 DCHECK(thread_check_->CalledOnValidThread()); | |
85 DVLOG(2) << "sending message @" << message << " on channel @" << this | |
86 << " with type " << message->type() | |
87 << " (" << output_queue_.size() << " in queue)"; | |
88 | |
89 if (!prelim_queue_.empty()) { | |
90 prelim_queue_.push(message); | |
91 return true; | |
92 } | |
93 | |
94 if (message->HasBrokerableAttachments() && | |
95 peer_pid_ == base::kNullProcessId) { | |
96 prelim_queue_.push(message); | |
97 return true; | |
98 } | |
99 | |
100 return ProcessMessageForDelivery(message); | |
101 } | |
102 | |
103 bool ChannelWin::ProcessMessageForDelivery(Message* message) { | |
104 // Sending a brokerable attachment requires a call to Channel::Send(), so | |
105 // both Send() and ProcessMessageForDelivery() may be re-entrant. | |
106 if (message->HasBrokerableAttachments()) { | |
107 DCHECK(GetAttachmentBroker()); | |
108 DCHECK(peer_pid_ != base::kNullProcessId); | |
109 for (const scoped_refptr<IPC::BrokerableAttachment>& attachment : | |
110 message->attachment_set()->GetBrokerableAttachments()) { | |
111 if (!GetAttachmentBroker()->SendAttachmentToProcess(attachment, | |
112 peer_pid_)) { | |
113 delete message; | |
114 return false; | |
115 } | |
116 } | |
117 } | |
118 | |
119 #ifdef IPC_MESSAGE_LOG_ENABLED | |
120 Logging::GetInstance()->OnSendMessage(message, ""); | |
121 #endif | |
122 | |
123 TRACE_EVENT_WITH_FLOW0(TRACE_DISABLED_BY_DEFAULT("ipc.flow"), | |
124 "ChannelWin::ProcessMessageForDelivery", | |
125 message->flags(), | |
126 TRACE_EVENT_FLAG_FLOW_OUT); | |
127 | |
128 // |output_queue_| takes ownership of |message|. | |
129 OutputElement* element = new OutputElement(message); | |
130 output_queue_.push(element); | |
131 | |
132 #if USE_ATTACHMENT_BROKER | |
133 if (message->HasBrokerableAttachments()) { | |
134 // |output_queue_| takes ownership of |ids.buffer|. | |
135 Message::SerializedAttachmentIds ids = | |
136 message->SerializedIdsOfBrokerableAttachments(); | |
137 output_queue_.push(new OutputElement(ids.buffer, ids.size)); | |
138 } | |
139 #endif | |
140 | |
141 // ensure waiting to write | |
142 if (!waiting_connect_) { | |
143 if (!output_state_.is_pending) { | |
144 if (!ProcessOutgoingMessages(NULL, 0)) | |
145 return false; | |
146 } | |
147 } | |
148 | |
149 return true; | |
150 } | |
151 | |
152 void ChannelWin::FlushPrelimQueue() { | |
153 DCHECK_NE(peer_pid_, base::kNullProcessId); | |
154 | |
155 // Due to the possibly re-entrant nature of ProcessMessageForDelivery(), it | |
156 // is critical that |prelim_queue_| appears empty. | |
157 std::queue<Message*> prelim_queue; | |
158 prelim_queue_.swap(prelim_queue); | |
159 | |
160 while (!prelim_queue.empty()) { | |
161 Message* m = prelim_queue.front(); | |
162 bool success = ProcessMessageForDelivery(m); | |
163 prelim_queue.pop(); | |
164 | |
165 if (!success) | |
166 break; | |
167 } | |
168 | |
169 // Delete any unprocessed messages. | |
170 while (!prelim_queue.empty()) { | |
171 Message* m = prelim_queue.front(); | |
172 delete m; | |
173 prelim_queue.pop(); | |
174 } | |
175 } | |
176 | |
177 AttachmentBroker* ChannelWin::GetAttachmentBroker() { | |
178 return AttachmentBroker::GetGlobal(); | |
179 } | |
180 | |
181 base::ProcessId ChannelWin::GetPeerPID() const { | |
182 return peer_pid_; | |
183 } | |
184 | |
185 base::ProcessId ChannelWin::GetSelfPID() const { | |
186 return GetCurrentProcessId(); | |
187 } | |
188 | |
189 // static | |
190 bool ChannelWin::IsNamedServerInitialized( | |
191 const std::string& channel_id) { | |
192 if (WaitNamedPipe(PipeName(channel_id, NULL).c_str(), 1)) | |
193 return true; | |
194 // If ERROR_SEM_TIMEOUT occurred, the pipe exists but is handling another | |
195 // connection. | |
196 return GetLastError() == ERROR_SEM_TIMEOUT; | |
197 } | |
198 | |
199 ChannelWin::ReadState ChannelWin::ReadData( | |
200 char* buffer, | |
201 int buffer_len, | |
202 int* /* bytes_read */) { | |
203 if (!pipe_.IsValid()) | |
204 return READ_FAILED; | |
205 | |
206 DWORD bytes_read = 0; | |
207 BOOL ok = ReadFile(pipe_.Get(), buffer, buffer_len, | |
208 &bytes_read, &input_state_.context.overlapped); | |
209 if (!ok) { | |
210 DWORD err = GetLastError(); | |
211 if (err == ERROR_IO_PENDING) { | |
212 input_state_.is_pending = true; | |
213 return READ_PENDING; | |
214 } | |
215 LOG(ERROR) << "pipe error: " << err; | |
216 return READ_FAILED; | |
217 } | |
218 | |
219 // We could return READ_SUCCEEDED here. But the way that this code is | |
220 // structured we instead go back to the message loop. Our completion port | |
221 // will be signalled even in the "synchronously completed" state. | |
222 // | |
223 // This allows us to potentially process some outgoing messages and | |
224 // interleave other work on this thread when we're getting hammered with | |
225 // input messages. Potentially, this could be tuned to be more efficient | |
226 // with some testing. | |
227 input_state_.is_pending = true; | |
228 return READ_PENDING; | |
229 } | |
230 | |
231 bool ChannelWin::ShouldDispatchInputMessage(Message* msg) { | |
232 // Make sure we get a hello when client validation is required. | |
233 if (validate_client_) | |
234 return IsHelloMessage(*msg); | |
235 return true; | |
236 } | |
237 | |
238 bool ChannelWin::GetNonBrokeredAttachments(Message* msg) { | |
239 return true; | |
240 } | |
241 | |
242 void ChannelWin::HandleInternalMessage(const Message& msg) { | |
243 DCHECK_EQ(msg.type(), static_cast<unsigned>(Channel::HELLO_MESSAGE_TYPE)); | |
244 // The hello message contains one parameter containing the PID. | |
245 base::PickleIterator it(msg); | |
246 int32_t claimed_pid; | |
247 bool failed = !it.ReadInt(&claimed_pid); | |
248 | |
249 if (!failed && validate_client_) { | |
250 int32_t secret; | |
251 failed = it.ReadInt(&secret) ? (secret != client_secret_) : true; | |
252 } | |
253 | |
254 if (failed) { | |
255 NOTREACHED(); | |
256 Close(); | |
257 listener()->OnChannelError(); | |
258 return; | |
259 } | |
260 | |
261 peer_pid_ = claimed_pid; | |
262 // Validation completed. | |
263 validate_client_ = false; | |
264 | |
265 listener()->OnChannelConnected(claimed_pid); | |
266 | |
267 FlushPrelimQueue(); | |
268 | |
269 if (IsAttachmentBrokerEndpoint() && | |
270 AttachmentBroker::GetGlobal()->IsPrivilegedBroker()) { | |
271 AttachmentBroker::GetGlobal()->ReceivedPeerPid(claimed_pid); | |
272 } | |
273 } | |
274 | |
275 base::ProcessId ChannelWin::GetSenderPID() { | |
276 return GetPeerPID(); | |
277 } | |
278 | |
279 bool ChannelWin::IsAttachmentBrokerEndpoint() { | |
280 return is_attachment_broker_endpoint(); | |
281 } | |
282 | |
283 bool ChannelWin::DidEmptyInputBuffers() { | |
284 // We don't need to do anything here. | |
285 return true; | |
286 } | |
287 | |
288 // static | |
289 const base::string16 ChannelWin::PipeName(const std::string& channel_id, | |
290 int32_t* secret) { | |
291 std::string name("\\\\.\\pipe\\chrome."); | |
292 | |
293 // Prevent the shared secret from ending up in the pipe name. | |
294 size_t index = channel_id.find_first_of('\\'); | |
295 if (index != std::string::npos) { | |
296 if (secret) // Retrieve the secret if asked for. | |
297 base::StringToInt(channel_id.substr(index + 1), secret); | |
298 return base::ASCIIToUTF16(name.append(channel_id.substr(0, index - 1))); | |
299 } | |
300 | |
301 // This case is here to support predictable named pipes in tests. | |
302 if (secret) | |
303 *secret = 0; | |
304 return base::ASCIIToUTF16(name.append(channel_id)); | |
305 } | |
306 | |
307 bool ChannelWin::CreatePipe(const IPC::ChannelHandle &channel_handle, | |
308 Mode mode) { | |
309 DCHECK(!pipe_.IsValid()); | |
310 base::string16 pipe_name; | |
311 // If we already have a valid pipe for channel just copy it. | |
312 if (channel_handle.pipe.handle) { | |
313 // TODO(rvargas) crbug.com/415294: ChannelHandle should either go away in | |
314 // favor of two independent entities (name/file), or it should be a move- | |
315 // only type with a base::File member. In any case, this code should not | |
316 // call DuplicateHandle. | |
317 DCHECK(channel_handle.name.empty()); | |
318 pipe_name = L"Not Available"; // Just used for LOG | |
319 // Check that the given pipe confirms to the specified mode. We can | |
320 // only check for PIPE_TYPE_MESSAGE & PIPE_SERVER_END flags since the | |
321 // other flags (PIPE_TYPE_BYTE, and PIPE_CLIENT_END) are defined as 0. | |
322 DWORD flags = 0; | |
323 GetNamedPipeInfo(channel_handle.pipe.handle, &flags, NULL, NULL, NULL); | |
324 DCHECK(!(flags & PIPE_TYPE_MESSAGE)); | |
325 if (((mode & MODE_SERVER_FLAG) && !(flags & PIPE_SERVER_END)) || | |
326 ((mode & MODE_CLIENT_FLAG) && (flags & PIPE_SERVER_END))) { | |
327 LOG(WARNING) << "Inconsistent open mode. Mode :" << mode; | |
328 return false; | |
329 } | |
330 HANDLE local_handle; | |
331 if (!DuplicateHandle(GetCurrentProcess(), | |
332 channel_handle.pipe.handle, | |
333 GetCurrentProcess(), | |
334 &local_handle, | |
335 0, | |
336 FALSE, | |
337 DUPLICATE_SAME_ACCESS)) { | |
338 LOG(WARNING) << "DuplicateHandle failed. Error :" << GetLastError(); | |
339 return false; | |
340 } | |
341 pipe_.Set(local_handle); | |
342 } else if (mode & MODE_SERVER_FLAG) { | |
343 DCHECK(!channel_handle.pipe.handle); | |
344 const DWORD open_mode = PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED | | |
345 FILE_FLAG_FIRST_PIPE_INSTANCE; | |
346 pipe_name = PipeName(channel_handle.name, &client_secret_); | |
347 validate_client_ = !!client_secret_; | |
348 pipe_.Set(CreateNamedPipeW(pipe_name.c_str(), | |
349 open_mode, | |
350 PIPE_TYPE_BYTE | PIPE_READMODE_BYTE, | |
351 1, | |
352 Channel::kReadBufferSize, | |
353 Channel::kReadBufferSize, | |
354 5000, | |
355 NULL)); | |
356 } else if (mode & MODE_CLIENT_FLAG) { | |
357 DCHECK(!channel_handle.pipe.handle); | |
358 pipe_name = PipeName(channel_handle.name, &client_secret_); | |
359 pipe_.Set(CreateFileW(pipe_name.c_str(), | |
360 GENERIC_READ | GENERIC_WRITE, | |
361 0, | |
362 NULL, | |
363 OPEN_EXISTING, | |
364 SECURITY_SQOS_PRESENT | SECURITY_ANONYMOUS | | |
365 FILE_FLAG_OVERLAPPED, | |
366 NULL)); | |
367 } else { | |
368 NOTREACHED(); | |
369 } | |
370 | |
371 if (!pipe_.IsValid()) { | |
372 // If this process is being closed, the pipe may be gone already. | |
373 PLOG(WARNING) << "Unable to create pipe \"" << pipe_name << "\" in " | |
374 << (mode & MODE_SERVER_FLAG ? "server" : "client") << " mode"; | |
375 return false; | |
376 } | |
377 | |
378 // Create the Hello message to be sent when Connect is called | |
379 std::unique_ptr<Message> m(new Message(MSG_ROUTING_NONE, HELLO_MESSAGE_TYPE, | |
380 IPC::Message::PRIORITY_NORMAL)); | |
381 | |
382 // Don't send the secret to the untrusted process, and don't send a secret | |
383 // if the value is zero (for IPC backwards compatability). | |
384 int32_t secret = validate_client_ ? 0 : client_secret_; | |
385 if (!m->WriteInt(GetCurrentProcessId()) || | |
386 (secret && !m->WriteUInt32(secret))) { | |
387 pipe_.Close(); | |
388 return false; | |
389 } | |
390 | |
391 OutputElement* element = new OutputElement(m.release()); | |
392 output_queue_.push(element); | |
393 return true; | |
394 } | |
395 | |
396 bool ChannelWin::Connect() { | |
397 DCHECK(base::MessageLoopForIO::IsCurrent()); | |
398 | |
399 WillConnect(); | |
400 | |
401 DLOG_IF(WARNING, thread_check_.get()) << "Connect called more than once"; | |
402 | |
403 if (!thread_check_.get()) | |
404 thread_check_.reset(new base::ThreadChecker()); | |
405 | |
406 if (!pipe_.IsValid()) | |
407 return false; | |
408 | |
409 base::MessageLoopForIO::current()->RegisterIOHandler(pipe_.Get(), this); | |
410 | |
411 // Check to see if there is a client connected to our pipe... | |
412 if (waiting_connect_) | |
413 ProcessConnection(); | |
414 | |
415 if (!input_state_.is_pending) { | |
416 // Complete setup asynchronously. By not setting input_state_.is_pending | |
417 // to true, we indicate to OnIOCompleted that this is the special | |
418 // initialization signal. | |
419 base::ThreadTaskRunnerHandle::Get()->PostTask( | |
420 FROM_HERE, | |
421 base::Bind(&ChannelWin::OnIOCompleted, weak_factory_.GetWeakPtr(), | |
422 &input_state_.context, 0, 0)); | |
423 } | |
424 | |
425 if (!waiting_connect_) | |
426 ProcessOutgoingMessages(NULL, 0); | |
427 return true; | |
428 } | |
429 | |
430 bool ChannelWin::ProcessConnection() { | |
431 DCHECK(thread_check_->CalledOnValidThread()); | |
432 if (input_state_.is_pending) | |
433 input_state_.is_pending = false; | |
434 | |
435 // Do we have a client connected to our pipe? | |
436 if (!pipe_.IsValid()) | |
437 return false; | |
438 | |
439 BOOL ok = ConnectNamedPipe(pipe_.Get(), &input_state_.context.overlapped); | |
440 DWORD err = GetLastError(); | |
441 if (ok) { | |
442 // Uhm, the API documentation says that this function should never | |
443 // return success when used in overlapped mode. | |
444 NOTREACHED(); | |
445 return false; | |
446 } | |
447 | |
448 switch (err) { | |
449 case ERROR_IO_PENDING: | |
450 input_state_.is_pending = true; | |
451 break; | |
452 case ERROR_PIPE_CONNECTED: | |
453 waiting_connect_ = false; | |
454 break; | |
455 case ERROR_NO_DATA: | |
456 // The pipe is being closed. | |
457 return false; | |
458 default: | |
459 NOTREACHED(); | |
460 return false; | |
461 } | |
462 | |
463 return true; | |
464 } | |
465 | |
466 bool ChannelWin::ProcessOutgoingMessages( | |
467 base::MessageLoopForIO::IOContext* context, | |
468 DWORD bytes_written) { | |
469 DCHECK(!waiting_connect_); // Why are we trying to send messages if there's | |
470 // no connection? | |
471 DCHECK(thread_check_->CalledOnValidThread()); | |
472 | |
473 if (output_state_.is_pending) { | |
474 DCHECK(context); | |
475 output_state_.is_pending = false; | |
476 if (!context || bytes_written == 0) { | |
477 DWORD err = GetLastError(); | |
478 LOG(ERROR) << "pipe error: " << err; | |
479 return false; | |
480 } | |
481 // Message was sent. | |
482 CHECK(!output_queue_.empty()); | |
483 OutputElement* element = output_queue_.front(); | |
484 output_queue_.pop(); | |
485 delete element; | |
486 } | |
487 | |
488 if (output_queue_.empty()) | |
489 return true; | |
490 | |
491 if (!pipe_.IsValid()) | |
492 return false; | |
493 | |
494 // Write to pipe... | |
495 OutputElement* element = output_queue_.front(); | |
496 DCHECK(element->size() <= INT_MAX); | |
497 BOOL ok = WriteFile(pipe_.Get(), | |
498 element->data(), | |
499 static_cast<uint32_t>(element->size()), | |
500 NULL, | |
501 &output_state_.context.overlapped); | |
502 if (!ok) { | |
503 DWORD write_error = GetLastError(); | |
504 if (write_error == ERROR_IO_PENDING) { | |
505 output_state_.is_pending = true; | |
506 | |
507 const Message* m = element->get_message(); | |
508 if (m) { | |
509 DVLOG(2) << "sent pending message @" << m << " on channel @" << this | |
510 << " with type " << m->type(); | |
511 } | |
512 | |
513 return true; | |
514 } | |
515 LOG(ERROR) << "pipe error: " << write_error; | |
516 return false; | |
517 } | |
518 | |
519 const Message* m = element->get_message(); | |
520 if (m) { | |
521 DVLOG(2) << "sent message @" << m << " on channel @" << this | |
522 << " with type " << m->type(); | |
523 } | |
524 | |
525 output_state_.is_pending = true; | |
526 return true; | |
527 } | |
528 | |
529 void ChannelWin::OnIOCompleted( | |
530 base::MessageLoopForIO::IOContext* context, | |
531 DWORD bytes_transfered, | |
532 DWORD error) { | |
533 bool ok = true; | |
534 DCHECK(thread_check_->CalledOnValidThread()); | |
535 if (context == &input_state_.context) { | |
536 if (waiting_connect_) { | |
537 if (!ProcessConnection()) | |
538 return; | |
539 // We may have some messages queued up to send... | |
540 if (!output_queue_.empty() && !output_state_.is_pending) | |
541 ProcessOutgoingMessages(NULL, 0); | |
542 if (input_state_.is_pending) | |
543 return; | |
544 // else, fall-through and look for incoming messages... | |
545 } | |
546 | |
547 // We don't support recursion through OnMessageReceived yet! | |
548 DCHECK(!processing_incoming_); | |
549 base::AutoReset<bool> auto_reset_processing_incoming( | |
550 &processing_incoming_, true); | |
551 | |
552 // Process the new data. | |
553 if (input_state_.is_pending) { | |
554 // This is the normal case for everything except the initialization step. | |
555 input_state_.is_pending = false; | |
556 if (!bytes_transfered) { | |
557 ok = false; | |
558 } else if (pipe_.IsValid()) { | |
559 ok = (AsyncReadComplete(bytes_transfered) != DISPATCH_ERROR); | |
560 } | |
561 } else { | |
562 DCHECK(!bytes_transfered); | |
563 } | |
564 | |
565 // Request more data. | |
566 if (ok) | |
567 ok = (ProcessIncomingMessages() != DISPATCH_ERROR); | |
568 } else { | |
569 DCHECK(context == &output_state_.context); | |
570 CHECK(output_state_.is_pending); | |
571 ok = ProcessOutgoingMessages(context, bytes_transfered); | |
572 } | |
573 if (!ok && pipe_.IsValid()) { | |
574 // We don't want to re-enter Close(). | |
575 Close(); | |
576 listener()->OnChannelError(); | |
577 } | |
578 } | |
579 | |
580 //------------------------------------------------------------------------------ | |
581 // Channel's methods | |
582 | |
583 // static | |
584 std::unique_ptr<Channel> Channel::Create( | |
585 const IPC::ChannelHandle& channel_handle, | |
586 Mode mode, | |
587 Listener* listener) { | |
588 return base::WrapUnique(new ChannelWin(channel_handle, mode, listener)); | |
589 } | |
590 | |
591 // static | |
592 bool Channel::IsNamedServerInitialized(const std::string& channel_id) { | |
593 return ChannelWin::IsNamedServerInitialized(channel_id); | |
594 } | |
595 | |
596 // static | |
597 std::string Channel::GenerateVerifiedChannelID(const std::string& prefix) { | |
598 // Windows pipes can be enumerated by low-privileged processes. So, we | |
599 // append a strong random value after the \ character. This value is not | |
600 // included in the pipe name, but sent as part of the client hello, to | |
601 // hijacking the pipe name to spoof the client. | |
602 | |
603 std::string id = prefix; | |
604 if (!id.empty()) | |
605 id.append("."); | |
606 | |
607 int secret; | |
608 do { // Guarantee we get a non-zero value. | |
609 secret = base::RandInt(0, std::numeric_limits<int>::max()); | |
610 } while (secret == 0); | |
611 | |
612 id.append(GenerateUniqueRandomChannelID()); | |
613 return id.append(base::StringPrintf("\\%d", secret)); | |
614 } | |
615 | |
616 } // namespace IPC | |
OLD | NEW |