diff options
author | jschuh@chromium.org <jschuh@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-03-17 02:20:46 +0000 |
---|---|---|
committer | jschuh@chromium.org <jschuh@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-03-17 02:20:46 +0000 |
commit | 5c41e6e1cd446c3b2bc0ed17e66dffbc76f7c650 (patch) | |
tree | 81e2f1a63b2109464dde8efa38b32381e2001829 | |
parent | ab25e338556b9580c54b3f4025009eaaf6ac83d3 (diff) | |
download | chromium_src-5c41e6e1cd446c3b2bc0ed17e66dffbc76f7c650.zip chromium_src-5c41e6e1cd446c3b2bc0ed17e66dffbc76f7c650.tar.gz chromium_src-5c41e6e1cd446c3b2bc0ed17e66dffbc76f7c650.tar.bz2 |
Verify the child process with a secret hello
BUG=117627
TEST=IPCSyncChannelTest.Verified
Review URL: http://codereview.chromium.org/9692035
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@127327 0039d316-1c4b-4281-b951-d872f2087c98
-rw-r--r-- | content/browser/renderer_host/render_process_host_impl.cc | 3 | ||||
-rw-r--r-- | content/common/child_process_host_impl.cc | 17 | ||||
-rw-r--r-- | content/common/child_process_host_impl.h | 6 | ||||
-rw-r--r-- | content/common/gpu/gpu_channel.cc | 9 | ||||
-rw-r--r-- | content/common/gpu/gpu_channel.h | 2 | ||||
-rw-r--r-- | content/common/np_channel_base.cc | 6 | ||||
-rw-r--r-- | content/ppapi_plugin/ppapi_thread.cc | 4 | ||||
-rw-r--r-- | ipc/ipc.gypi | 1 | ||||
-rw-r--r-- | ipc/ipc_channel.cc | 40 | ||||
-rw-r--r-- | ipc/ipc_channel.h | 11 | ||||
-rw-r--r-- | ipc/ipc_channel_posix.cc | 14 | ||||
-rw-r--r-- | ipc/ipc_channel_win.cc | 69 | ||||
-rw-r--r-- | ipc/ipc_channel_win.h | 13 | ||||
-rw-r--r-- | ipc/ipc_sync_channel_unittest.cc | 89 |
14 files changed, 241 insertions, 43 deletions
diff --git a/content/browser/renderer_host/render_process_host_impl.cc b/content/browser/renderer_host/render_process_host_impl.cc index 1cba35f..f2f5ae9 100644 --- a/content/browser/renderer_host/render_process_host_impl.cc +++ b/content/browser/renderer_host/render_process_host_impl.cc @@ -97,6 +97,7 @@ #include "content/public/common/result_codes.h" #include "content/renderer/render_process_impl.h" #include "content/renderer/render_thread_impl.h" +#include "ipc/ipc_channel.h" #include "ipc/ipc_logging.h" #include "ipc/ipc_platform_file.h" #include "ipc/ipc_switches.h" @@ -384,7 +385,7 @@ bool RenderProcessHostImpl::Init(bool is_accessibility_enabled) { // Setup the IPC channel. const std::string channel_id = - ChildProcessHostImpl::GenerateRandomChannelID(this); + IPC::Channel::GenerateVerifiedChannelID(std::string()); channel_.reset(new IPC::ChannelProxy( channel_id, IPC::Channel::MODE_SERVER, this, BrowserThread::GetMessageLoopProxyForThread(BrowserThread::IO))); diff --git a/content/common/child_process_host_impl.cc b/content/common/child_process_host_impl.cc index 26c0b93..f1e330c 100644 --- a/content/common/child_process_host_impl.cc +++ b/content/common/child_process_host_impl.cc @@ -1,4 +1,4 @@ -// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// 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. @@ -20,6 +20,7 @@ #include "content/public/common/child_process_host_delegate.h" #include "content/public/common/content_paths.h" #include "content/public/common/content_switches.h" +#include "ipc/ipc_channel.h" #include "ipc/ipc_logging.h" #if defined(OS_LINUX) @@ -155,7 +156,7 @@ void ChildProcessHostImpl::ForceShutdown() { } std::string ChildProcessHostImpl::CreateChannel() { - channel_id_ = GenerateRandomChannelID(this); + channel_id_ = IPC::Channel::GenerateVerifiedChannelID(std::string()); channel_.reset(new IPC::Channel( channel_id_, IPC::Channel::MODE_SERVER, this)); if (!channel_->Connect()) @@ -205,18 +206,6 @@ void ChildProcessHostImpl::AllocateSharedMemory( shared_buf.GiveToProcess(child_process_handle, shared_memory_handle); } -std::string ChildProcessHostImpl::GenerateRandomChannelID(void* instance) { - // Note: the string must start with the current process id, this is how - // child processes determine the pid of the parent. - // Build the channel ID. This is composed of a unique identifier for the - // parent browser process, an identifier for the child instance, and a random - // component. We use a random component so that a hacked child process can't - // cause denial of service by causing future named pipe creation to fail. - return base::StringPrintf("%d.%p.%d", - base::GetCurrentProcId(), instance, - base::RandInt(0, std::numeric_limits<int>::max())); -} - int ChildProcessHostImpl::GenerateChildProcessUniqueId() { // This function must be threadsafe. static base::subtle::Atomic32 last_unique_child_id = 0; diff --git a/content/common/child_process_host_impl.h b/content/common/child_process_host_impl.h index 32736f3..3168c6c 100644 --- a/content/common/child_process_host_impl.h +++ b/content/common/child_process_host_impl.h @@ -1,4 +1,4 @@ -// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// 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. @@ -36,10 +36,6 @@ class CONTENT_EXPORT ChildProcessHostImpl : public ChildProcessHost, uint32 buffer_size, base::ProcessHandle child_process, base::SharedMemoryHandle* handle); - // Generates a unique channel name for a child process. - // The "instance" pointer value is baked into the channel id. - static std::string GenerateRandomChannelID(void* instance); - // Returns a unique ID to identify a child process. On construction, this // function will be used to generate the id_, but it is also used to generate // IDs for the RenderProcessHost, which doesn't inherit from us, and whose IDs diff --git a/content/common/gpu/gpu_channel.cc b/content/common/gpu/gpu_channel.cc index acf570d..5568b17 100644 --- a/content/common/gpu/gpu_channel.cc +++ b/content/common/gpu/gpu_channel.cc @@ -52,9 +52,7 @@ GpuChannel::GpuChannel(GpuChannelManager* gpu_channel_manager, DCHECK(gpu_channel_manager); DCHECK(client_id); - static int last_channel_id = 0; - channel_id_ = ++last_channel_id; - + channel_id_ = IPC::Channel::GenerateVerifiedChannelID("gpu"); const CommandLine* command_line = CommandLine::ForCurrentProcess(); log_messages_ = command_line->HasSwitch(switches::kLogPluginMessages); disallowed_features_.multisampling = @@ -418,9 +416,8 @@ bool GpuChannel::Init(base::MessageLoopProxy* io_message_loop, DCHECK(!channel_.get()); // Map renderer ID to a (single) channel to that process. - std::string channel_name = GetChannelName(); channel_.reset(new IPC::SyncChannel( - channel_name, + channel_id_, IPC::Channel::MODE_SERVER, this, io_message_loop, @@ -442,7 +439,7 @@ void GpuChannel::DidDestroyCommandBuffer(gfx::GpuPreference gpu_preference) { } std::string GpuChannel::GetChannelName() { - return StringPrintf("gpu.%d.%d", base::GetCurrentProcId(), channel_id_); + return channel_id_; } #if defined(OS_POSIX) diff --git a/content/common/gpu/gpu_channel.h b/content/common/gpu/gpu_channel.h index f2902f0..a92ed87 100644 --- a/content/common/gpu/gpu_channel.h +++ b/content/common/gpu/gpu_channel.h @@ -147,7 +147,7 @@ class GpuChannel : public IPC::Channel::Listener, int client_id_; // Uniquely identifies the channel within this GPU process. - int channel_id_; + std::string channel_id_; // Handle to the renderer process that is on the other side of the channel. base::ProcessHandle renderer_process_; diff --git a/content/common/np_channel_base.cc b/content/common/np_channel_base.cc index 4209b0c..7226c48 100644 --- a/content/common/np_channel_base.cc +++ b/content/common/np_channel_base.cc @@ -24,8 +24,6 @@ typedef std::stack<scoped_refptr<NPChannelBase> > NPChannelRefStack; static base::LazyInstance<NPChannelRefStack>::Leaky g_lazy_channel_stack = LAZY_INSTANCE_INITIALIZER; -static int next_pipe_id = 0; - NPChannelBase* NPChannelBase::GetChannel( const IPC::ChannelHandle& channel_handle, IPC::Channel::Mode mode, ChannelFactory factory, base::MessageLoopProxy* ipc_message_loop, @@ -44,8 +42,8 @@ NPChannelBase* NPChannelBase::GetChannel( if (!channel->channel_valid()) { channel->channel_handle_ = channel_handle; if (mode & IPC::Channel::MODE_SERVER_FLAG) { - channel->channel_handle_.name.append("."); - channel->channel_handle_.name.append(base::IntToString(next_pipe_id++)); + channel->channel_handle_.name = + IPC::Channel::GenerateVerifiedChannelID(channel_key); } channel->mode_ = mode; if (channel->Init(ipc_message_loop, create_pipe_now, shutdown_event)) { diff --git a/content/ppapi_plugin/ppapi_thread.cc b/content/ppapi_plugin/ppapi_thread.cc index 964127b..5b5c2cf 100644 --- a/content/ppapi_plugin/ppapi_thread.cc +++ b/content/ppapi_plugin/ppapi_thread.cc @@ -280,8 +280,8 @@ bool PpapiThread::SetupRendererChannel(base::ProcessHandle host_process_handle, DCHECK(is_broker_ == (connect_instance_func_ != NULL)); DCHECK(is_broker_ == (get_plugin_interface_ == NULL)); IPC::ChannelHandle plugin_handle; - plugin_handle.name = StringPrintf("%d.r%d", base::GetCurrentProcId(), - renderer_id); + plugin_handle.name = IPC::Channel::GenerateVerifiedChannelID( + StringPrintf("%d.r%d", base::GetCurrentProcId(), renderer_id)); ppapi::proxy::ProxyChannel* dispatcher = NULL; bool init_result = false; diff --git a/ipc/ipc.gypi b/ipc/ipc.gypi index d43947a..0684216 100644 --- a/ipc/ipc.gypi +++ b/ipc/ipc.gypi @@ -14,6 +14,7 @@ 'file_descriptor_set_posix.cc', 'file_descriptor_set_posix.h', 'ipc_channel.h', + 'ipc_channel.cc', 'ipc_channel_handle.h', 'ipc_channel_posix.cc', 'ipc_channel_posix.h', diff --git a/ipc/ipc_channel.cc b/ipc/ipc_channel.cc new file mode 100644 index 0000000..5973f72 --- /dev/null +++ b/ipc/ipc_channel.cc @@ -0,0 +1,40 @@ +// 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. + +#include "ipc/ipc_channel.h" + +#include <limits> + +#include "base/atomic_sequence_num.h" +#include "base/process_util.h" +#include "base/rand_util.h" +#include "base/stringprintf.h" + +namespace { + +// Global atomic used to guarantee channel IDs are unique. +base::StaticAtomicSequenceNumber g_last_id; + +} // namespace + +namespace IPC { + +// static +std::string Channel::GenerateUniqueRandomChannelID() { + // Note: the string must start with the current process id, this is how + // some child processes determine the pid of the parent. + // + // This is composed of a unique incremental identifier, the process ID of + // the creator, an identifier for the child instance, and a strong random + // component. The strong random component prevents other processes from + // hijacking or squatting on predictable channel names. + + return base::StringPrintf("%d.%u.%d", + base::GetCurrentProcId(), + g_last_id.GetNext(), + base::RandInt(0, std::numeric_limits<int32>::max())); +} + +} // namespace IPC + diff --git a/ipc/ipc_channel.h b/ipc/ipc_channel.h index 63beeee..4f1e75f 100644 --- a/ipc/ipc_channel.h +++ b/ipc/ipc_channel.h @@ -6,6 +6,8 @@ #define IPC_IPC_CHANNEL_H_ #pragma once +#include <string> + #include "base/compiler_specific.h" #include "ipc/ipc_channel_handle.h" #include "ipc/ipc_message.h" @@ -190,6 +192,15 @@ class IPC_EXPORT Channel : public Message::Sender { // ID. Even if true, the server may have already accepted a connection. static bool IsNamedServerInitialized(const std::string& channel_id); + // Generates a channel ID that's non-predictable and unique. + static std::string GenerateUniqueRandomChannelID(); + + // Generates a channel ID that, if passed to the client as a shared secret, + // will validate that the client's authenticity. On platforms that do not + // require additional this is simply calls GenerateUniqueRandomChannelID(). + // For portability the prefix should not include the \ character. + static std::string GenerateVerifiedChannelID(const std::string& prefix); + #if defined(OS_LINUX) // Sandboxed processes live in a PID namespace, so when sending the IPC hello // message from client to server we need to send the PID from the global diff --git a/ipc/ipc_channel_posix.cc b/ipc/ipc_channel_posix.cc index 39a11be..1a1f77e 100644 --- a/ipc/ipc_channel_posix.cc +++ b/ipc/ipc_channel_posix.cc @@ -29,6 +29,7 @@ #include "base/memory/scoped_ptr.h" #include "base/memory/singleton.h" #include "base/process_util.h" +#include "base/rand_util.h" #include "base/stl_util.h" #include "base/string_util.h" #include "base/synchronization/lock.h" @@ -1160,6 +1161,19 @@ bool Channel::IsNamedServerInitialized(const std::string& channel_id) { return ChannelImpl::IsNamedServerInitialized(channel_id); } +// static +std::string Channel::GenerateVerifiedChannelID(const std::string& prefix) { + // A random name is sufficient validation on posix systems, so we don't need + // an additional shared secret. + + std::string id = prefix; + if (!id.empty()) + id.append("."); + + return id.append(GenerateUniqueRandomChannelID()); +} + + #if defined(OS_LINUX) // static void Channel::SetGlobalPid(int pid) { diff --git a/ipc/ipc_channel_win.cc b/ipc/ipc_channel_win.cc index ef974ee..e2f0535 100644 --- a/ipc/ipc_channel_win.cc +++ b/ipc/ipc_channel_win.cc @@ -10,6 +10,9 @@ #include "base/bind.h" #include "base/compiler_specific.h" #include "base/logging.h" +#include "base/process_util.h" +#include "base/rand_util.h" +#include "base/string_number_conversions.h" #include "base/threading/non_thread_safe.h" #include "base/utf_string_conversions.h" #include "base/win/scoped_handle.h" @@ -36,7 +39,9 @@ Channel::ChannelImpl::ChannelImpl(const IPC::ChannelHandle &channel_handle, pipe_(INVALID_HANDLE_VALUE), waiting_connect_(mode & MODE_SERVER_FLAG), processing_incoming_(false), - ALLOW_THIS_IN_INITIALIZER_LIST(weak_factory_(this)) { + ALLOW_THIS_IN_INITIALIZER_LIST(weak_factory_(this)), + client_secret_(0), + validate_client_(false) { CreatePipe(channel_handle, mode); } @@ -97,14 +102,13 @@ bool Channel::ChannelImpl::Send(Message* message) { // static bool Channel::ChannelImpl::IsNamedServerInitialized( const std::string& channel_id) { - if (WaitNamedPipe(PipeName(channel_id).c_str(), 1)) + if (WaitNamedPipe(PipeName(channel_id, NULL).c_str(), 1)) return true; // If ERROR_SEM_TIMEOUT occurred, the pipe exists but is handling another // connection. return GetLastError() == ERROR_SEM_TIMEOUT; } - Channel::ChannelImpl::ReadState Channel::ChannelImpl::ReadData( char* buffer, int buffer_len, @@ -144,7 +148,16 @@ bool Channel::ChannelImpl::WillDispatchInputMessage(Message* msg) { void Channel::ChannelImpl::HandleHelloMessage(const Message& msg) { // The hello message contains one parameter containing the PID. - listener()->OnChannelConnected(MessageIterator(msg).NextInt()); + MessageIterator it = MessageIterator(msg); + int32 claimed_pid = it.NextInt(); + if (validate_client_ && (it.NextInt() != client_secret_)) { + NOTREACHED(); + // Something went wrong. Abort connection. + Close(); + listener()->OnChannelError(); + return; + } + listener()->OnChannelConnected(claimed_pid); } bool Channel::ChannelImpl::DidEmptyInputBuffers() { @@ -153,9 +166,21 @@ bool Channel::ChannelImpl::DidEmptyInputBuffers() { } // static -const std::wstring Channel::ChannelImpl::PipeName( - const std::string& channel_id) { +const string16 Channel::ChannelImpl::PipeName( + const std::string& channel_id, int32* secret) { std::string name("\\\\.\\pipe\\chrome."); + + // Prevent the shared secret from ending up in the pipe name. + size_t index = channel_id.find_first_of('\\'); + if (index != std::string::npos) { + if (secret) // Retrieve the secret if asked for. + base::StringToInt(channel_id.substr(index + 1), secret); + return ASCIIToWide(name.append(channel_id.substr(0, index - 1))); + } + + // This case is here to support predictable named pipes in tests. + if (secret) + *secret = 0; return ASCIIToWide(name.append(channel_id)); } @@ -192,7 +217,8 @@ bool Channel::ChannelImpl::CreatePipe(const IPC::ChannelHandle &channel_handle, DCHECK(!channel_handle.pipe.handle); const DWORD open_mode = PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED | FILE_FLAG_FIRST_PIPE_INSTANCE; - pipe_name = PipeName(channel_handle.name); + pipe_name = PipeName(channel_handle.name, &client_secret_); + validate_client_ = !!client_secret_; pipe_ = CreateNamedPipeW(pipe_name.c_str(), open_mode, PIPE_TYPE_BYTE | PIPE_READMODE_BYTE, @@ -203,7 +229,7 @@ bool Channel::ChannelImpl::CreatePipe(const IPC::ChannelHandle &channel_handle, NULL); } else if (mode & MODE_CLIENT_FLAG) { DCHECK(!channel_handle.pipe.handle); - pipe_name = PipeName(channel_handle.name); + pipe_name = PipeName(channel_handle.name, &client_secret_); pipe_ = CreateFileW(pipe_name.c_str(), GENERIC_READ | GENERIC_WRITE, 0, @@ -228,7 +254,12 @@ bool Channel::ChannelImpl::CreatePipe(const IPC::ChannelHandle &channel_handle, scoped_ptr<Message> m(new Message(MSG_ROUTING_NONE, HELLO_MESSAGE_TYPE, IPC::Message::PRIORITY_NORMAL)); - if (!m->WriteInt(GetCurrentProcessId())) { + + // Don't send the secret to the untrusted process, and don't send a secret + // if the value is zero (for IPC backwards compatability). + int32 secret = validate_client_ ? 0 : client_secret_; + if (!m->WriteInt(GetCurrentProcessId()) || + (secret && !m->WriteUInt32(secret))) { CloseHandle(pipe_); pipe_ = INVALID_HANDLE_VALUE; return false; @@ -441,4 +472,24 @@ bool Channel::IsNamedServerInitialized(const std::string& channel_id) { return ChannelImpl::IsNamedServerInitialized(channel_id); } +// static +std::string Channel::GenerateVerifiedChannelID(const std::string& prefix) { + // Windows pipes can be enumerated by low-privileged processes. So, we + // append a strong random value after the \ character. This value is not + // included in the pipe name, but sent as part of the client hello, to + // hijacking the pipe name to spoof the client. + + std::string id = prefix; + if (!id.empty()) + id.append("."); + + int secret; + do { // Guarantee we get a non-zero value. + secret = base::RandInt(0, std::numeric_limits<int>::max()); + } while (secret == 0); + + id.append(GenerateUniqueRandomChannelID()); + return id.append(base::StringPrintf("\\%d", secret)); +} + } // namespace IPC diff --git a/ipc/ipc_channel_win.h b/ipc/ipc_channel_win.h index 7a48695..d680fb7 100644 --- a/ipc/ipc_channel_win.h +++ b/ipc/ipc_channel_win.h @@ -43,7 +43,8 @@ class Channel::ChannelImpl : public internal::ChannelReader, bool DidEmptyInputBuffers() OVERRIDE; virtual void HandleHelloMessage(const Message& msg) OVERRIDE; - static const std::wstring PipeName(const std::string& channel_id); + static const string16 PipeName(const std::string& channel_id, + int32* secret); bool CreatePipe(const IPC::ChannelHandle &channel_handle, Mode mode); bool ProcessConnection(); @@ -79,6 +80,16 @@ class Channel::ChannelImpl : public internal::ChannelReader, // problems. TODO(darin): make this unnecessary bool processing_incoming_; + // Determines if we should validate a client's secret on connection. + bool validate_client_; + + // This is a unique per-channel value used to authenticate the client end of + // a connection. If the value is non-zero, the client passes it in the hello + // and the host validates. (We don't send the zero value fto preserve IPC + // compatability with existing clients that don't validate the channel.) + int32 client_secret_; + + base::WeakPtrFactory<ChannelImpl> weak_factory_; scoped_ptr<base::NonThreadSafe> thread_check_; diff --git a/ipc/ipc_sync_channel_unittest.cc b/ipc/ipc_sync_channel_unittest.cc index 2b74ff4..6cb7457 100644 --- a/ipc/ipc_sync_channel_unittest.cc +++ b/ipc/ipc_sync_channel_unittest.cc @@ -1594,4 +1594,93 @@ TEST_F(IPCSyncChannelTest, RestrictedDispatchDeadlock) { RunTest(workers); } +//----------------------------------------------------------------------------- + +// Generate a validated channel ID using Channel::GenerateVerifiedChannelID(). +namespace { + +class VerifiedServer : public Worker { + public: + VerifiedServer(base::Thread* listener_thread, + const std::string& channel_name, + const std::string& reply_text) + : Worker(channel_name, Channel::MODE_SERVER), + reply_text_(reply_text) { + Worker::OverrideThread(listener_thread); + } + + virtual void OnNestedTestMsg(Message* reply_msg) { + VLOG(1) << __FUNCTION__ << " Sending reply: " << reply_text_; + SyncChannelNestedTestMsg_String::WriteReplyParams(reply_msg, reply_text_); + Send(reply_msg); + Done(); + } + + private: + std::string reply_text_; +}; + +class VerifiedClient : public Worker { + public: + VerifiedClient(base::Thread* listener_thread, + const std::string& channel_name, + const std::string& expected_text) + : Worker(channel_name, Channel::MODE_CLIENT), + expected_text_(expected_text) { + Worker::OverrideThread(listener_thread); + } + + virtual void Run() { + std::string response; + SyncMessage* msg = new SyncChannelNestedTestMsg_String(&response); + bool result = Send(msg); + DCHECK(result); + DCHECK_EQ(response, expected_text_); + + VLOG(1) << __FUNCTION__ << " Received reply: " << response; + Done(); + } + + private: + bool pump_during_send_; + std::string expected_text_; +}; + +void Verified() { + std::vector<Worker*> workers; + + // A shared worker thread for servers + base::Thread server_worker_thread("Verified_ServerListener"); + ASSERT_TRUE(server_worker_thread.Start()); + + base::Thread client_worker_thread("Verified_ClientListener"); + ASSERT_TRUE(client_worker_thread.Start()); + + std::string channel_id = Channel::GenerateVerifiedChannelID("Verified"); + Worker* worker; + + worker = new VerifiedServer(&server_worker_thread, + channel_id, + "Got first message"); + workers.push_back(worker); + + worker = new VerifiedClient(&client_worker_thread, + channel_id, + "Got first message"); + workers.push_back(worker); + + RunTest(workers); + +#if defined(OS_WIN) +#endif +} + +} // namespace + +// Windows needs to send an out-of-band secret to verify the client end of the +// channel. Test that we still connect correctly in that case. +TEST_F(IPCSyncChannelTest, Verified) { + Verified(); +} + } // namespace IPC |