The
primary development language for SymbianOS is a modified dialect of C++
known as Symbian C++; however, additional language runtimes are
available. Developers may opt to write applications with P.I.P.S
(P.I.P.S Is Posix on SymbianOS), Open C (an extension of P.I.P.S.),
Python, Java, or other languages. In
general, additional runtimes do not come preinstalled, and the runtime
installer should be embedded within the installers of relying
applications.
Symbian C++
With Symbian C++,
code executes directly on the hardware of the phone, without a virtual
machine or interpreter, allowing a developer to take full advantage of
the available resources. Unfortunately, such native code does not
provide protections against many common memory corruption
vulnerabilities. This includes stack overflows, heap overflows, and
integer overflows. In essence, the programmer is entirely responsible
for preventing these vulnerabilities through appropriate secure coding
practices. Otherwise, an attacker could potentially cause their
malicious code to execute within an exploited process.
Descriptors
Symbian provides
technologies to reduce the chances of exploitable buffer overflow
conditions, but the programmer must take advantage of them. For example,
buffer overflows regularly result from mishandling data when copying,
formatting, or concatenating strings and byte arrays. To protect against
such errors, Symbian C++ provides the descriptor framework to replace
all C-style strings and the related string-handling operations. They are
called descriptors
because each instance stores its type, length, and data, thus
describing everything needed to safely manipulate the stored data.
Mutable descriptors (those whose name does not end with the letter C) also include the maximum length. Figure 1 presents the relationships among the varied descriptor classes.
Descriptors come in both
8-bit-wide and 16-bit-wide varieties and can be identified by the number
appended to the name (for example, TDesC8 and TBuf16). When a
descriptor is used that includes neither a 16 nor an 8, the 16-bit
version will be used. (Technically a determination is made based on
whether the _UNICODE macro is defined, which is the platform default.)
It is important to note that
the type and the length are stored within a single 32-bit Tint field.
The four most significant bits indicate the type of the descriptor, and
the remaining 28 bits represent the size of the referenced data. This
has several important consequences. First, descriptors are limited to a
maximum size of 256MB (228
bytes). Second, developers must be exceptionally careful deriving
custom descriptor subclasses with regard to the type field. The TDesC
base class uses this field to determine the memory layout of its
subclasses. This means that custom subclasses cannot have an arbitrary
memory layout, but must share a type and memory layout of a built-in
descriptor. It is not recommended to derive custom descriptor classes.
For
example, accessing the descriptor data of each subclass is performed
with the non-virtual Ptr() method defined in the TDesC base class. (In
fact, there are no virtual methods within the descriptor hierarchy.)
This method uses the type data stored in each descriptor instance as the
control variable for a switch statement that then returns the proper
address referring to the beginning of the data.
By preserving the length
(and maximum length) of a descriptor, its method calls are able to
perform appropriate validation and prevent the unintentional access to
out-of-bounds memory. When a method call would read or write beyond the
data boundaries, the process will panic and immediately terminate. This
means that with traditional descriptors, the burden of correct memory
management falls solely on the shoulders of the developer. They must
verify that the descriptor can accommodate the amount of incoming data
before calling a method that would modify the contents of the
descriptor.
Recognizing these
difficulties, Symbian has developed and released a new library. The
EUserHL Core Idioms Library provides a pair of descriptors (LString for
Unicode-aware text and LData for simple byte buffers) that manage their
own memory. These descriptors automatically reallocate themselves to
increase their capacity and then release the resources when they go out
of scope, similar to the behavior of standard C++ strings. Because these
two classes have been derived from their respective TDesC class (TDesC8
and TDesC16), they can be used as parameters directly when accessing the preexisting platform API. LString and LData should be used instead of any of the other descriptor classes.
Caution
Because
descriptor methods are not virtual, preexisting methods in the class
hierarchy will still cause a process panic when the buffer is not large
enough to hold the fresh data. For working with LString and LData
directly, these methods have been restricted with the private access
modifier and public replacements have been introduced. These can be
identified by the “L” suffix common to all API functions that may leave
(throw an exception). However, LString or LData objects passed to a
function taking TDes16 or TDes8 parameters will not have the new methods
called. You should ensure that the LString/LData object is large enough
to hold any resultant data when calling such a method. The
ReserveFreeCapacity method should be called before calling any such
method.
Arrays
Memory management issues can
also arise when manipulating bare arrays. After all, a C-style string
is a null-terminated array of chars. Rather than use a C-style array
(int a[10]; int * b = new int[10];), Symbian provides many classes for
safe array management. In fact, the number of separate classes available
can be daunting.
For the predominant
use cases, the RArray and RPointerArray template classes are the
recommended choices. These two classes manage their own memory and
implement a traditional array interface (that is, indices can be
accessed through the [] operator, and objects can be inserted, deleted,
and appended). Attempts to access indices outside the bounds of the
current size (either negative or greater than the current size) will
cause the process to panic.
Using the
RArray class has some additional restrictions that RPointerArray is not
subject to. Due to an implementation detail, objects stored in an RArray
class must be word-aligned on a four-byte boundary and cannot be larger
than 640 bytes. Attempts to create an RArray of objects larger than 640
bytes will cause the process to panic. In these cases, consider using
an RPointerArray instead. Here’s an example:
LString ex1 (_L ("example1")), ex2(_L ("example2"));
LCleanedupHandle< RArray< LString > > strArray;
strArray->ReserveL(10);
strArray->AppendL(ex1);
strArray->InsertL(ex2, 0);
strArray->Remove(0);
strArray->Compress();
Integer Overflows
Symbian
does not provide for automatic protections against integer overflows.
An integer overflow occurs when the memory representation of an integer
variable lacks the capacity to hold the result of an arithmetic
operation. The behavior when such a condition arises is left as an
implementation-specific detail. Integer overflows can cause security
vulnerabilities in many instances—for example, when allocating memory or
accessing an array. Developers must consciously take strides to
eliminate the risk associated with such errors. As such, each arithmetic
operation should be checked for overflow conditions.
When a type is
available whose representation is twice as large as the type that is to
be checked—for example, a 64-bit integer and a 32-bit integer—the
overflow tests are straightforward. This is because the larger type can
accurately hold the result of any arithmetic operation on variables of
the smaller type. Symbian provides both a signed and an unsigned 64-bit
integer type that can be used to perform such testing:
TInt32 safe_add(TInt32 a, TInt32 b) {
TInt64 c = static_cast< TInt64 >(a) + b;
if((c > INT_MAX) || (c < INT_MIN)) {
User::Leave(KErrOverflow);
}
return static_cast< TInt32 >(c);
}
If the parameters to an
arithmetic operation are known at compile time (for example, using
literals and constants), then the GCCE compiler will be able to flag the
potential integer overflows—GCCE being one of the compilers that
targets the physical phone. Each of these should be reviewed to ensure
that the overflow does not cause an issue. Note that the four SID and
the four VID security policy macros will trigger a false positive
integer overflow and can be safely ignored.
Leaves and Traps
SymbianOS has a system known as leaves and traps
for handling many error conditions. Calling User::Leave(TInt32
ErrorCode) is directly analogous to the throw statement from standard
C++, and the TRAP/TRAPD macros take the place of the try/catch
statements. (The TRAP macro requires the developer to explicitly declare
an error code variable, whereas the TRAPD macro does this
automatically.) A function that could potentially cause such an error is
referred to as a function that “may leave.” These can be identified in
the standard library through a naming convention—each function whose
name ends with a capital L can potentially leave.
In
older versions of Symbian, these could be imagined as being implemented
using the setjmp and longjump pair of functions. This means that program
flow could jump from one function to another without performing
appropriate stack cleanup; the destructors for stack objects were not
executed. This necessitated implementing a custom system for managing
heap allocated memory in order to prevent memory leaks. This system is
called the Cleanup Stack. Every time a developer allocates an object on
the heap, a reference should be pushed onto the cleanup stack. Whenever
an object goes out of scope, it should be popped off the cleanup stack
and deallocated. If a function leaves, all objects on the current
cleanup stack frame are removed and deallocated. The beginning of a
stack frame is marked by the TRAP/TRAPD macros and can be nested.
Another consequence of
the abrupt transfer of program flow control was that constructors could
not leave. If they did, the partially constructed object would not be
properly cleaned up because the destructor would not be called and no
reference had been pushed onto the cleanup stack. In order to get around
this, many classes implement two-phase construction, where the actual
constructor does nothing but return a self-reference and a second method
ConstructL initializes the object. These methods are oftentimes
private. Instead, classes offer a public method, NewL or NewLC, that
creates an object, initializes it, and returns a reference.
In the 9.x
series of SymbianOS, the leaves and trap system has been implemented on
top of standard C++ exceptions. When a function leaves, several steps
are performed:
All objects in the current frame of the cleanup stack are removed and deallocated, calling any object destructors.
An
XLeaveException is allocated and thrown. This exception will be created
using preallocated memory if new memory cannot be obtained (for
example, during an out-of-memory condition).
Through the standard C++ exception procedure, the call stack is unwound and the destructor is called on each stack object.
One
of the TRAP/TRAPD macros catches the thrown exception and assigns the
error code to an integer variable that a developer should test. Any
other type of exception will cause the process to panic.
This backward compatibility has several interesting consequences. First, a developer should never
mix the use of standard exceptions and SymbianOS leaves. A function
that leaves should not call a function that throws exceptions without
enclosing it within a try/catch block in order to prevent propagation
back to a TRAP macro. Similarly, any function that may leave called from
code using only standard exceptions should be wrapped with one of the
TRAP/TRAPD macros.
When
leaves and traps were mapped onto the standard exception mechanism, the
use of two-phase construction remained. With the release of the EUserHL
Core Idioms Library, you may return to a more traditional Resource
Acquisition Is Initialization (RAII) model. When you define a new class,
you should use the CONSTRUCTORS_MAY_LEAVE macro as the first line of
code. This macro actually defines how the delete operator will affect
the class:
class CUseful : public CBase {
public:
CONSTRUCTORS_MAY_LEAVE
CUseful ();
~CUseful();
};
Finally, the use of a cleanup
stack continued to be useful because there were several classes that
did not adequately clean up properly upon destruction. These objects
required the developer to explicitly call a cleanup method—generally
Close, Release, or Destroy. The EUserHL Core Idioms Library also
includes a set of classes to assist the developer with proper resource
management in these cases. This includes the LCleanedupXXX and
LManagedXXX classes. The LCleanedupXXX group of classes, found in Table 1, is designed for use with local function variables, and the LManagedXXX group of classes, found in Table 7-2, is designed for use with class member variables.
Table 1. Local Variable Automatic Resource Management
Class Name | Purpose |
---|
LCleanedupPtr | Manage object pointers. |
< typename T > | Free memory when leaving scope. |
LCleanedupHandle | Manage resource objects (e.g., RFs & rfs). |
< typename T > | Call Close() when leaving scope. |
LCleanedupRef | Manage a reference to a resource object (e.g., RFs & rfs). |
< typename T > | Call Close() when leaving scope. |
LCleanedupArray | Manage an array of object pointers. |
< typename T > | Free all memory when leaving scope. (Prefer RArray.) |
LCleanedupGuard | Automatic management of generic objects. |
Table 2. Class Member Variable Automatic Resource Management
Class Name | Purpose |
---|
LManagedPtr | Manage object pointers. |
< typename T > | Free memory when leaving scope. |
LManagedHandle | Manage resource objects (e.g., RFs & rfs). |
< typename T > | Call Close() when leaving scope. |
LManagedRef | Manage a reference to a resource object (e.g., RFs & rfs). |
< typename T > | Call Close() when leaving scope. |
LManagedArray | Manage an array of object pointers. |
< typename T > | Free all memory when leaving scope. (Prefer RArray.) LManagedGuard Automatic management of generic objects. |
These classes implement what
can be viewed as a set of “smart pointers.” The developer no longer has
to worry about pushing and popping objects from the cleanup stack,
deleting memory before a reference goes out of scope, or calling a
resource cleanup function. This is all performed appropriately when one
of the LManagedXXX/LCleanedupXXX objects goes out of scope. In the
following example, the handle to the file server and to an open file are
automatically closed and released when the function ends:
void readFileL(LString filename, LString data) {
LCleanedupHandle<RFs> fs;
LCleanedupHandle<RFile> file;
TInt size;
fs->Connect() OR_LEAVE;
file->Open(*fs, filename, EFileRead) OR_LEAVE;
file->Size(size) OR_LEAVE;
data.SetLengthL(0);
data.ReserveFreeCapacityL(size);
file->Read(data) OR_LEAVE;
}
And in the following
example, the CMessageDigest object that is allocated in the constructor
of the CUseful class is automatically cleaned up when an instance goes
out of scope. The destructor does not need to do anything to prevent
memory leaks.
class CUseful : public CBase {
public:
CONSTRUCTORS_MAY_LEAVE
CUseful() : hash(CMessageDigestFactory::NewHMACL (
CMessageDigest::ESHA1, HMACKey)) { }
~CUseful(){ }
private:
LManagedPtr< CMessageDigest > hash;
};
The
different groups of classes, LCleanedupXXX and LManagedXXX, arise due
to interaction with the cleanup stack. Indeed, if the LCleanedupXXX
classes were used for class member variables, then objects would be
popped and destroyed from the cleanup stack out of order.
Use of these classes is
recommended in order to reduce the possibility of manual memory
management errors, such as memory leaks, double frees, null pointer
dereferences, and so on.
Automatic Protection Mechanisms
SymbianOS does not
guarantee the presence of any automatic protection mechanisms to
mitigate memory corruption vulnerabilities. Neither address space layout
randomization nor stack canaries are available in any form. A
nonexecutable stack is only available when run on hardware that supports
it—namely, ARMv6 and ARMv7, but not ARMv5.
Although such mechanisms are
not perfect in preventing the exploitation, they do increase the level
of skill required to craft a successful exploit. Because they are not
present, developers must continue to be exceptionally careful to reduce
the risk of code execution vulnerabilities.
P.I.P.S and OpenC
P.I.P.S. is a POSIX
compatibility layer that aides in the rapid porting of software to
SymbianOS-based phones. OpenC is an S60 extension of P.I.P.S. that
brings a larger set of ported libraries. These environments are not
suitable for GUI code, which must continue to be written in Symbian C++.
P.I.P.S. and OpenC are
implemented as shared libraries that are linked into native code
applications. This means that applications written for either of these
environments suffer from all of the same memory corruption flaws. In
fact, it is more likely to have such problems because the environment
does not provide safer alternatives—namely, descriptors and array
classes. As such, string handling is performed with C-style strings and
the associated set of blatantly unsafe functions, such as strcat and
strcpy.
P.I.P.S.
and OpenC should only be used for POSIX code ported from other
platforms, not for newly written code for the SymbianOS platform.