SymbianOS
supports kernel-mediated communication between running processes.
Thanks to the protected memory model, a process cannot directly modify
the memory space of another running process. Interprocess communication
is still a desirable activity—for example, dividing a single executable
into two executables that communicate to provide a separation of
concerns.
Client/Server Sessions
A client/server IPC model is
used pervasively throughout SymbianOS. A number of the OS-provided
services are implemented in two components: a server process and a
client DLL that creates a session with the server. The server is an
independent, long-running process with its own SecureID. The client DLL
is loaded by client processes and maintains a mapping between function
names and ordinal function numbers. Note that the client DLL is not
strictly necessary, but the developer must interact with the server in
the same fashion.
Part of implementing a
client/server interface includes the ability to enforce a particular
security policy as to which clients can connect to a server and which
servers a client will connect to. A security policy is defined using one
of fifteen _LIT_SECURITY_POLICY macros. These macros can be grouped in
three categories: Capability enforcement, SecureID enforcement, and
VendorID enforcement. A listing of these macros can be seen in Table 1.
A server can and should validate the capabilities of clients that
connect to it in order to prevent capability leakage where a privileged
server performs sensitive actions on behalf of an unprivileged client. A
server can also enforce that only clients with a particular SecureID
can connect to it. This can be useful when implementing an application
model where sensitive actions are performed in a small second process.
Finally, a server can enforce that only clients from a particular vendor
can connect to it when the vendor factors out sensitive actions into a
common process that each of its applications should be able to access.
Table 1. Security Policy Macros
Macro | Purpose |
---|
_LIT_SECURITY_POLICY_C1 | Enforce one capability |
_LIT_SECURITY_POLICY_C2 | Enforce two capabilities |
_LIT_SECURITY_POLICY_C3 | Enforce three capabilities |
_LIT_SECURITY_POLICY_C4 | Enforce four capabilities |
_LIT_SECURITY_POLICY_C5 | Enforce five capabilities |
_LIT_SECURITY_POLICY_C6 | Enforce six capabilities |
_LIT_SECURITY_POLICY_C7 | Enforce seven capabilities |
_LIT_SECURITY_POLICY_S0 | Enforce a SecureID |
_LIT_SECURITY_POLICY_S1 | Enforce a SecureID and one capability |
_LIT_SECURITY_POLICY_S2 | Enforce a SecureID and two capabilities |
_LIT_SECURITY_POLICY_S3 | Enforce a SecureID and three capabilities |
_LIT_SECURITY_POLICY_V0 | Enforce a VendorID |
_LIT_SECURITY_POLICY_V1 | Enforce a VendorID and one capability |
_LIT_SECURITY_POLICY_V2 | Enforce a VendorID and two capabilities |
_LIT_SECURITY_POLICY_V3 | Enforce a VendorID and three capabilities |
Each of these macros is
used in the same general way. The first parameter specifies the name of a
new policy object. For SecureID macros, the second parameter specifies
the targeted SecureID. For VendorID macros, the second parameter
specifies the targeted VendorID. The rest of the parameters are one of
the enumerated capabilities, the number of which is specified in the
macro name.
_LIT_SECURITY_POLICY_S0(KCustomServerSID, 0xE0000001);
_LIT_SECURITY_POLICY_V1(KClientVIDOneCap, 0xE0000001, ECapabilityDiskAdmin);
_LIT_SECURITY_POLICY_C2(KEnforceTwoCaps, ECapabilityReadUserData,
ECapabilityWriteUserData);
Client
sessions are created via an RSessionBase object and a call to the
CreateSession() method. Most client DLLs will derive their own subclass
that calls this method with the appropriate parameters. During the 9.x
series, another overload of CreateSession() was added that takes a
pointer to a TSecurityPolicy object. This allows a client to validate
the SecureID or VendorID of the named server. Messages to the server are
delivered and responses obtained through the SendReceive() method. Most
client DLLs will provide wrapper functions for calls to SendReceive()
to provide a more natural interface. The code below shows the basic
pattern behind writing a client proxy object. This class will reside
within a client DLL and hide the interprocess communication details.
_LIT(KCustomServerName, "com_isecpartners_custom");
enum TCustomServerMessages {
EDoStuff
};
class RCustomSession : public RSessionBase {
public:
IMPORT_C TInt Connect();
IMPORT_C TInt DoStuff(const LString& str);
};
EXPORT_C TInt RCustomSession::Connect() {
return CreateSession(KCustomServerName,
TVersion(),
KServerDefaultMessageSlots,
EIpcSession_Unsharable,
&KCustomServerSID());
}
EXPORT_C TInt RCustomSession::DoStuff(const LString& str) {
return SendReceive(EDoStuff, TIpcArgs(&str));
}
Servers
are created by deriving two of three classes: CSession2 and either
CServer2 or CPolicyServer. CServer2 works well in many cases—namely,
where the security policy to be enforced is straightforward (for
example, restricting potential clients to those with a particular
SecureID upon session connection or requiring a particular capability to
call a certain method). In order to enforce a policy with a CSession2
or CServer2 class, call the CheckPolicy() method of a SecurityPolicy
object with RMessage& as the first parameter. The result of this
method is a boolean indicating whether the message was delivered by a
process that conforms to the policy. Wrapping calls to this method in an
if statement allows for corrective action to be taken. In the following
example, the corrective action is to “leave” with a permission-denied
error:
class CCustomSession : public CSession2 {
private:
void ServiceL(const RMessage2& msg);
TInt doStuff(const LString& str);
TInt getStuff (LString& str);
};
void CCustomSession::ServiceL(const RMessage2& msg) {
TInt status = KErrNotSupported;
switch (aMessage.Function()) {
case EDoStuff: {
LString param(msg.GetDesLengthL(0));
msg.ReadL(0, param);
status = doStuff(param);
break;
}
case EGetStuff :{
if(!KEnforceTwoCaps().CheckPolicy (msg,
__PLATSEC_DIAGNOSTIC_STRING("CCustomSession::ServiceL"))) {
User::Leave(KErrPermissionDenied);
}
LString result;
status = getStuff(result);
__ASSERT_ALWAYS(result.Length() <= msg.GetDesMaxLengthL(0),
User::Leave(KErrBadDescriptor));
msg.WriteL(0, result);
break;
}
default: {
_LIT(KErrMsg, "Unknown function call!");
msg.Panic(KErrMsg, KErrNotSupported);
}
}
aMessage.Complete(status);
}
class CCustomServer : public CServer2 {
public:
CCustomServer(TInt priority = EPriorityNormal);
private:
CSession2* NewSessionL(const TVersion& version,
const RMessage2& msg) const;
};
CCustomServer::CCustomServer(TInt priority) : CServer2(priority,
ESharableSessions) {
Start(KCustomServerName);
}
CSession2* CCustomServer::NewSessionL(const TVersion&,
const RMessage2&) const {
if(!KClientVIDOneCap().CheckPolicy(msg,
__PLATSEC_DIAGNOSTIC_STRING("CCustomServer::NewSessionL"))) {
User::Leave(KErrPermissionDenied);
}
return new (ELeave) CCustomSession();
}
CPolicyServer
should be chosen when the policy to be enforced is very complex.
Although substantially more complex in simple cases, it can be much
simpler in complex cases. The framework automatically handles checking a
prospective client’s policy conformance upon session initiation and for
each message. The first step is to create an array of message numbers
in sorted increasing order. Each number need not be represented, just
the lower bound of a range that shares the same policy. That is, if
functions 0 through 3 share a policy and function 4 has its own policy,
then the array should have two elements: 0 and 4. Next, a second array
is created that must be the same size as the previous one to contain
indices into a third array. This third array contains the separate
policy-enforcement objects. When a message is sent to the server, its
function number (or the closest number less than it) is found in the
first array and the index is noted. This index is used to reference into
the second array in order to obtain the index into the third array,
where the actual policy object is found. Conceptually this can be
imagined as a dictionary that maps a function ordinal to the policy to
be applied, where the first array holds the dictionary key and the
second array holds a reference to the policy. The code below
demonstrates the basics behind using the CPolicyServer class to reduce
the developer effort required to enforce complex security policies.
const TUInt rangesCount = 3;
const TInt msgNumRanges[rangesCount] = {
0, // EDoStuff
1, // EGetStuff
2 // Non-existent functions
};
const TUInt8 msgPolicyIndices[] = {
CPolicyServer::EAlwaysPass, // EDoStuff
1, // EGetStuff
CPolicyServer::EBadMessageNumber // Non-existent functions
};
const CPolicyServer::TPolicyElement msgPolicies[] = {
{
_INIT_SECURITY_POLICY_V1(0xE0000001, ECapabilityDiskAdmin),
CPolicyServer::EFailClient
},
{
_INIT_SECURITY_POLICY_C2(ECapabilityReadUserData,
ECapabilityWriteUserData),
CPolicyServer::EFailClient
}
};
const CPolicyServer::TPolicy customPolicy = {
0,
rangesCount,
msgNumberRanges,
msgPolicyIndices,
msgPolicies
};
class CCustomPolicyServer : public CPolicyServer {
public:
CCustomPolicyServer(TInt priority = EPriorityNormal);
private:
CSession2* NewSessionL(const TVersion& version,
const RMessage2& msg) const;
};
CCustomPolicyServer::CCustomServer(TInt priority) :
CPolicyServer(priority, CustomPolicy, ESharableSessions) {
Start(KCustomServerName);
}
CSession2* CCustomPolicyServer::NewSessionL(const TVersion&,
const RMessage2&) const {
return new (ELeave) CCustomSession();
}
Shared Sessions
Sessions can be created as
unsharable, sharable between threads within the same process, or
sharable between processes. This is defined on both the client and
server sides. When CreateSession() is called on the client, one of the
parameters specifies the desired session sharing. The CServer2() constructor specifies the type of sessions that can be created.
Use caution when
allowing globally sharable sessions and implementing a policy where
clients are checked at session initialization. A client that does not
meet the policy could obtain a session handle from a process that did.
This is especially true when implementing subsessions. Subsessions
maintain a reference to the parent session (which maintains a list of
open subsessions). When sharing a subsession, be aware that all of the
other active subsessions are also exposed.
Shared Handles
Another form of IPC
available to SymbianOS processes involves sharing handles to kernel-side
objects. A number of these objects share a common interface because
they derive from the same base class: RHandleBase. Examples of these
objects include RChunk, RSemaphore, RMutex, and RSessionBase.
RChunk references a chunk of
memory and is useful for transferring large amounts of data between
processes. In previous versions of SymbianOS, this could not be used
securely. An RChunk object could either be created locally, accessible
only to the current process, or created as a named global object,
accessible to any process that knew the name. With the introduction of
9.x,
anonymous global chunks were introduced. These objects were accessible
between processes, but the creating process had to pass the handle to
the consuming process through the client/server interface. This greatly
limited the possibility of unintentionally leaking data through a global
handle; however, one still needs to take care because a shared handle
can be shared further to other processes.
This same interface is
exposed for the other RHandleBase-derived classes. They can be created
locally, named globally, or anonymous globally. This allows a process to
carefully control access to these kernel-side handles. In fact, it is
this interface exposed by the RSessionBase that allows for separate
processes to share file handles. A session handle to the file server is
shared between two processes.