Technical overview of CopperheadOS
This is a technical overview of the currently implemented features in CopperheadOS. For a list of planned features, see the issues tagged as enhancements on the tracker. The scope will be expanded as more features are implemented.
Prelude: Android baseline
The Android Open Source Project provides a robust base to build upon. The baseline security model and features are not documented here, only the CopperheadOS improvements. To summarize some of the standard security features inherited from Android:
- Full Disk Encryption at the filesystem layer, covering all data (AES-256-XTS) and metadata (AES-256-CBC+CTS). The encryption key is randomly generated, and then encrypted with a key encryption key derived via scrypt from the passphrase the verified boot key and the hardware-bound Trusted Execution Environment key which also implements rate limiting below the OS layer.
- Full verified boot, covering all firmware and OS partitions. The unverified partitions holding user data are wiped by a factory reset.
- Baseline app isolation via unique uid/gid pairs for each app.
- App permission model including the ability to revoke permissions and supply fake data. Most permissions are based on dynamic checks for IPC requests, while a small subset make use of secondary groups.
- Full system SELinux policy with fine-grained domains. There is no unconfined domain, and MLS provides very strong multi-user isolation.
- Fine-grained ioctl command filtering is done via SELinux. This is better than using seccomp-bpf to filter ioctl parameters since it’s per-device-type and lower overhead. This is currently used for socket ioctls, but is being expanded to cover everything.
- Kernel attack surface reduction via seccomp-bpf. The sandbox is entered after initialization to reduce the necessary system calls. This is currently done for the media extractor and codec services but is being expanded.
- Widespread adoption of memory-safe languages, including within the base OS.
- Higher-entropy ASLR than the upstream Linux kernel defaults paired with library load order randomization in the linker.
Exec-based spawning model
CopperheadOS spawns applications from the Zygote service in the traditional Unix way with
exec rather than Android’s standard model using only
fork. This results in the address
space layout and stack/setjmp canaries being randomized for each spawned application rather than
having the same layout and canary values reused for all applications until reboot. In addition to
hardening applications from exploitation, this also hardens the base system as the large, near
root equivalent system_server service is spawned from the Zygote.
This required some workarounds due to code depending on the Zygote spawning model.
Support for shared RELRO sections has been removed, since it’s unusable with exec-based spawning and results in a reduction of complexity and SELinux permissions. Similarly, the AtlasAsset service has been removed since it wouldn’t be possible for it to work as currently designed.
Chromium / WebView
CopperheadOS ships a custom build of Android’s WebView along with a standalone build of Chromium from the same source tree. Unlike Google’s Chrome app, the CopperheadOS Chromium app is 64-bit.
Chromium supports per-site-instance sandboxing via isolatedProcess and seccomp-bpf. The WebView has early support for sandboxing via a single isolatedProcess renderer for each app using it. It isn’t yet enabled in stock Android, but CopperheadOS force enables it. This also means that the WebView renderers are always 64-bit even for 32-bit apps. Once enabling multiple renderer processes is stable, CopperheadOS will enable more of them since the memory cost is acceptable for the niche. It currently results in stability issues.
CopperheadOS also builds Chromium with -fstack-protector-strong rather than -fstack-protector and removes mremap from the system call whitelist since CopperheadOS is currently not using it as an optimization in the system allocator and it’s not used elsewhere.
Using Chromium or a WebView-based browser has the advantage of reducing attack surface compared to another browser engine, since the Chromium-based WebView is already a baseline Android component. The standalone Chromium also has a far superior sandbox to the one available for the WebView at the moment, although it will continue to catch up.
Chromium’s default settings have been adjusted to be more suited to CopperheadOS. Navigation error correction, contextual search, network prediction, metrics and hyperlink auditing are disabled by default. The welcome page with the opt-out for metrics is omitted since it’s already disabled, as are the 3 forms of promotion of Google’s data reduction proxy.
Note that while no features have been removed, Chromium for Android depends on Play Services for Google account integration and some other features based on Google services so those features are not available on CopperheadOS.
DuckDuckGo has been added as a search engine option and is set as the default. It supports the search suggestion API used by Chromium. A missing feature is search-by-image, which is available with Bing or Google set as the search engine.
Bionic libc improvements
CopperheadOS replaces the system allocator with a port of OpenBSD’s malloc implementation. Various enhancements have been made to the standard OpenBSD allocator, some of which have been upstreamed.
The allocator doesn’t use any inline metadata, so traditional allocator exploitation techniques are not possible. The out-of-line metadata results in full detection of invalid free calls. It is guaranteed to abort for pointers that are not active malloc allocations.
The configuration is made read-only at runtime and the rest of the global state is protected to some extent via randomization and canaries. CopperheadOS has some small extensions to improve the randomization and may do more in the future.
In order to mitigate vulnerabilities caused by offsets from unchecked malloc calls, CopperheadOS sets the allocator to abort on out-of-memory by default. It can be turned off on a case by case basis in processes verified to check for out-of-memory for all allocator calls. Android uses full overcommit paired with a userspace out-of-memory killer, so the abort only occurs when a process exhausts the virtual address space. In practice, it’s not possible for an attacker to trigger virtual memory exhaustion without also being able to trigger the out-of-memory killer, and this mitigates many severe vulnerabilities. The address space is very large on all of the first-tier CopperheadOS devices (47-bit) so it can only really be exhausted if there’s a severe logic error or an attacker has control of the allocated size.
OpenBSD malloc is a zone-based allocator, similar in design to Android’s standard jemalloc allocator. Unlike jemalloc, it uses page-aligned regions instead of 2MiB aligned regions so it doesn’t cause a loss of mmap randomization entropy. The standard allocator loses 9 bits of mmap entropy on systems with 4096 byte pages.
A randomized page cache provides a layer on top of mmap and munmap. Spans of pages are cached in an array with up to 256 slots using randomized searches. Regions are split at this layer but not merged together. It’s meant to provide a thin layer over mmap, partly to benefit from fine-grained randomization by the kernel. Linux only randomizes the mmap base, unlike OpenBSD, so for now it’s not on par with how it works there.
A user-facing setting is exposed for enabling page cache memory protection to trigger aborts for use-after-free bugs. For small allocations, a whole page needs to be cleared out for this to work, but another technique is used to provide a comparable mitigation (see below).
Fine-grained randomization is performed for small allocations by choosing a random pool to satisfy requests and then choosing a random free slot within a page provided by that pool. Freed small allocations are quarantined before being put back into circulation via a randomized delayed allocation pool. These features raise the difficulty of exploiting vulnerabilities by making the internal heap layout and allocator behavior unpredictable.
CopperheadOS uses a ring buffer to extend the randomized quarantine with a deterministic layer enforcing a minimum delay before allocations are put back into circulation. The memory dedicated to the quarantine is evenly split between the two layers. CopperheadOS also adds a hash table providing enhanced double-free detection by tracking all delayed allocations within the ring buffer and randomized array. Previously, double-free could not be detected if the previously freed allocation was still in the quarantine. CopperheadOS makes the quarantine size configurable, with the size limit exposed as a user-facing setting.
Small allocations are filled with junk data upon being released. This prevents many information leaks caused by use without initialization and can make the exploitation of use-after-free and double free vulnerabilities more difficult. When allocations leave the quarantine, the junk data is validated to detect write-after-free. By default, 32 bytes are checked and full validation can be enabled via a user-facing setting. Junk validation was implemented in CopperheadOS and then successfully upstreamed into OpenBSD.
Canaries can be placed at the end of small allocations to absorb small overflows and catch various forms of heap corruption upon free. This was a successfully upstreamed CopperheadOS extension. In CopperheadOS, this is enabled by default despite the memory usage cost since the supported devices have lots of memory and there’s a negligible performance impact.
In OpenBSD, only the leading 2048 bytes of large allocations are junk filled by default and junk filling isn’t used at all if page cache memory protection is enabled. CopperheadOS removes these optimizations, so it either uses full junk filling (default) or none. It makes sense for the cost to scale up with allocation size and junk filling is important for preventing information leaks via uninitialized malloc usage even with page cache memory protection enabled.
A user-facing setting is exposed for placing guard pages at the end of large allocations to prevent and detect overflows at the cost of higher memory usage and reduced performance. It may be enabled by default on 64-bit in the future. Large allocations can be moved as close as possible to the end of the allocated region in order to trigger faults for small overflows, and CopperheadOS enables this by default when guard pages are enabled but not otherwise, unlike OpenBSD where it is simply enabled by default.
_FORTIFY_SOURCE feature provides buffer overflow checking for standard C library functions
in cases where the compiler can determine the buffer size at compile-time.
Copperhead has added fortified implementations of the following functions:
- fread - upstreamed
- fwrite - upstreamed
- getcwd - upstreamed
- memchr - upstreamed
- memrchr - upstreamed
- pread - upstreamed
- pread64 - upstreamed
- pwrite - upstreamed
- pwrite64 - upstreamed
- readlink - upstreamed
- readlinkat - upstreamed
- realpath - upstreamed
- send - submitted upstream
- sendto - submitted upstream
- write - upstreamed
Additionally, the dlmalloc API has been annotated with
alloc_size attributes to provide buffer
overflow checking for the remaining code using the extended API.
Some false positives in jemalloc were fixed in
order to land support for
write fortification in AOSP.
Dynamic object size queries
In CopperheadOS, system calls perform dynamic overflow checks in addition to the
checks based on compile-time buffer sizes from compiler analysis. The standard system call symbols
are really wrappers querying
__dynamic_object_size and then calling through to the usual raw
system call wrappers. The makes the feature a drop-in enhancement even for precompiled third party
code, much like the hardened allocator.
The main component of the
__dynamic_object_size feature is querying malloc for the object size.
OpenBSD malloc tracks allocation metadata entirely via out-of-line data structures so it can
accurately respond to these queries with either the size class of the allocation (minus the offset
into it) or a sentinel value. Recursion would trigger deadlocking, so CopperheadOS tracks whether
it is inside a malloc call via thread-local storage. The malloc queries are are disabled within
malloc calls. This allows the dynamic object size checks to be used even for libc, after early
initialization. Note that this currently returns a sentinel value for addresses beyond the first
page of an allocation, but improving this is on the roadmap.
Before querying malloc, there’s special handling for addresses within the calling thread’s stack, the executable and the isolated library region. These paths can only give a rough upper bound or abort the process if the address isn’t part of any valid object. It reduces the performance cost of the feature because querying malloc is relatively expensive.
It’s restricted to system calls as it would be too expensive elsewhere. The read-only-after-init global used to enable this feature after early init code may be extended into a configurable feature. Calls like fread and fwrite sit in a middle ground between calls like memcpy and system calls so there could be 3 levels to the performance vs security compromise: off, system calls, system calls + fread/fwrite and everything. This would end up being part of a high level performance vs. security slider exposed to users.
Function pointer protection
Writable function pointers in libc have been eliminated, removing low-hanging fruit for hijacking control flow with memory corruption vulnerabilities.
pthread_atfork callback registration functions have been extended with
the same memory protection offered by the
atexit implementation inherited from OpenBSD.
The vDSO function pointer table is made read-only after initialization, as is the pointer to the function pointer table used to implement Android’s malloc debugging features. This has been upstreamed.
Mangling of the setjmp registers was implemented upstream based on input from Copperhead.
A dedicated memory region is created for mapping libraries, to isolate them from the rest of the mmap heap. It is currently 128M on 32-bit and 1024M on 64-bit. A randomly sized protected region of up to the same size is placed before it to provide a random base within the mmap heap. The address space is reused via a simple address-ordered best-fit allocator to keep fragmentation at a minimum for dynamically loaded/unloaded libraries (plugin systems, etc.).
Secondary stacks are randomized by inserting a random span of protected memory above the stack and choosing a random base within it. This has been submitted upstream.
Secondary stacks are guaranteed to have at least one guard page above the stack, catching sequential
overflows past the stack mapping. The
pthread_internal_t structure is placed in a separate mapping
rather than being placed within the stack mapping directly above the stack. It contains thread-local
storage and the value compared against stack canaries is stored there on some architectures.
Signal stacks were given guard pages to catch stack overflows. This was upstreamed.
Assorted small improvements:
- have getauxval(…) set errno to ENOENT for missing values, per glibc 2.19 - upstreamed
- fix the mremap signature - upstreamed
- implementations of
secure_getenvfor use elsewhere
- replaced the broken implementations of
- larger default stack size on 64-bit (1MiB -> 8MiB)
- and various other assorted improvements
As part of moving towards reducing trust in /data/ (i.e. state not covered by verified boot),
WITH_DEXPREOPT_PIC are always enabled and
always disabled to reduce usage of /data/dalvik-cache by the base system to a minimum. There is no
usage of executable data in /data/dalvik-cache for the base system since even boot.oat is position
independent. The boot.art file still has to be relocated via patchoat and is still stored in
/data/dalvik-cache but isn’t executable. Avoiding that is also planned. The Android Runtime has
been taught not to look for executable code (oat and odex files) in /data/dalvik-cache and execute
and symlink read permissions for the dalvik cache label have been removed for system_server and
domains only used by the base system, leaving it permitted by the policy only for untrusted_app,
isolated_app and the shell domain for adb shell.
CopperheadOS disables the ART JIT compiler and JIT profiling for security reasons. It also disables the usage of any generated profiles for compilation by switching the defaults from profile-based ahead-of-time verification/optimization to full verification/optimization. It currently leaves in place the default of using interpret-only verification at first-boot/boot/install and only doing optimized compilation in the background and for the new online A/B update system. This may be changed due to the cost of the JIT being disabled.
CopperheadOS has ports of the PaX kernel hardening patch to the supported devices. It may use a larger subset of grsecurity in the future, but most of the additional features over PaX are not usable on Android or are already provided in other ways. It would make more sense to extract only the useful features (PROC_MEMMAP, KSTACKOVERFLOW, HIDESYM, RANDSTRUCT, RWXMAP_LOG and maybe a few more) rather than maintaining a full port.
Note that PaX currently has little support for 64-bit ARM, so the migration of Nexus devices to 64-bit has temporarily rendered most of the features unavailable.
PaX’s userspace hardening features are all enabled. The RANDMMAP feature provides significantly stronger Address Space Layout Randomization (ASLR) than the vanilla kernel and eliminates the mmap address hint footgun. PAGEEXEC turns no-execute violations into fatal errors rather than recoverable ones. MPROTECT prevents runtime code modification/injection.
A toggle for soft mode is exposed in Settings -> Developer options along with a status entry in Settings -> About device displaying whether a PaX kernel is in use and the soft mode setting.
Some kernel self-protection features are enabled (MEMORY_SANITIZE, REFCOUNT, USERCOPY) along with the baseline improvements without configuration options. KERNEXEC and UDEREF are available for devices using the 3.10 kernel. The features implemented via compiler plugins have been tested and are work well, but the necessary changes to support them are unfinished and aren’t yet published.
- A custom -fsanitize=local-init sanitizer is used to zero all uninitialized local variables to prevent information leaks. Existing optimization passes are quite good at removing it when it’s unnecessary.
- Lightweight bounds checking for statically sized arrays via -fsanitize=bounds -fsanitize-trap=bounds is enabled by default in userspace for C and C++. It is currently disabled for a few libraries where it catches out-of-bounds access bugs in regular use.
- For some sub-projects, lightweight object size bounds checking is performed (including extended checks for arrays) via -fsanitize=object-size -fsanitize-trap=object-size. This has a lot of overlap with the bounds sanitizer, but the redundant checks are optimized out when both are set to trap on error. It would be nice to enable this globally, but there’s too much code relying on undefined out-of-bounds accesses.
- For some sub-projects, both unsigned and signed integer overflow checking via -fsanitize=integer -fsanitize-trap=integer (mostly backported from AOSP master).
- Stack overflow checking for supported architectures via -fstack-check (not on ARM yet due to a severe bug) - submitted upstream for the NDK
- For code where signed integer overflow checking is not enabled, overflow is made into well-defined behavior via -fwrapv to avoid security vulnerabilities like incorrectly written overflow checks being optimized out.
- Expanded stack overflow canaries to all relevant functions via -fstack-protector-strong for both the NDK and base system. It has also been upstreamed for both the NDK and base system. CopperheadOS also enables -fstack-protector-strong for the Linux kernel. The lack of SSP for the kernel on ARM64 was reported upstream and -fstack-protector-strong has now been enabled.
-Wsuggest-attribute=formatwarning to audit for missing format attributes - dozens of cases have been found and fixed, providing more coverage for warnings about exploitable format string bugs.
Enhanced SELinux policies
- eliminated code injection holes
- Removed gpu_device execute access - upstreamed
- Removed ashmem execute access
- Removed tmpfs execute access
- Removed execmod (text relocations) from domains other than untrusted_app - upstreamed
- Removed execmem from all but the untrusted_app, isolated_app and mediadrmserver domains (note: mediadrmserver will likely be removed). This is possible due to disabling the ART JIT compiler and forcing usage of the multiprocess/sandboxed WebView which runs as isolated_app.
- Removed priv_app app_data_file execute / execute_no_trans access
- The remaining hole in full (not just in-memory) w^x is app_data_file execution for untrusted_app. The intention is to allow apps to opt-in to removing it and then upstream that, with the goal of changing the default as a breaking change in a future API level to force opting into the less secure policy along with discouraging the pattern.
- removed mediaserver’s write access to sysfs - upstreamed
- reported a hole in the isolation for system apps running as the system user upstream, which was prompted fixed: ptrace could be used to take control of other system apps running as the system user
Access to timing information via /proc/ has been limited to a few core services in order to close sensitive information from being obtained via timing side channels. This has been upstreamed. This is being gradually extended to enforce fine-grained restrictions for the rest of the sensitive information exposed in /proc: 1, vmstat.
Encryption and authentication
Full disk encryption is enabled by default on all supported devices, not just those shipping that way with the stock operating system.
Support for a separate encryption password
Note: this feature needs to be redesigned for Android Nougat, and will be replaced by a superior 2 factor authentication mechanism.
In vanilla Android, the encryption password is tied to the lockscreen password. That’s the default in CopperheadOS, but there’s full support for setting a separate encryption password. This allows for a convenient pattern, pin or password to be used for unlocking the screen while using a very strong encryption passphrase. If desired, the separate encryption password can be removed in favor of coupling it to the lockscreen password again.
When a separate encryption password is set, the lockscreen will force a reboot after 5 failed unlocking attempts to force the entry of the encryption passphrase. This makes it possible to use a convenient unlocking method without brute force being feasible. It offers similar benefits as wiping after a given number of failures or using a fingerprint scanner without the associated drawbacks.
Support for longer passwords
The maximum password length is raised from 16 characters to 64 characters.
The default settings have been altered to emphasize privacy/security over small conveniences.
Location tagging is disabled by default in the Camera app, and there is no longer a prompt about choosing whether to enable it on the first launch. It can still be enabled using the Camera’s settings menu.
- passwords are hidden by default
- sensitive notifications are hidden on the lockscreen by default
- NFC and NDEF Push are disabled by default
Security-focused built-in apps
The Messaging application is replaced with SMSSecure for authenticated encryption via SMS.
CopperheadOS uses an extremely simple over-the-air update server written in Go paired with a security-focused fork of CyanogenMod’s update client. The client has been ported to using private downloads and an internal storage directory rather than using shared storage so other apps can’t interfere with updates. CopperheadOS is fully signed (unlike CyanogenMod) so an update modified by another app would fail to pass signature verification in the recovery image but using shared data would allow other apps to prevent updating. It has also been altered to work properly with encryption enabled with a normal recovery image. CyanogenMod expects the recovery to prompt the user for the encryption password and mount the data partition.
Interfaces are given a random MAC address whenever they are brought up. This can be disabled via a
toggle in the network settings. Wireless MAC addresses are also unconditionally randomized during
scanning (pre-associated). The hostname is randomized at boot by default, and it can also be
disabled in order to use the persistent hostname based on
The kernel TCP/IP settings are adjusted to prioritize security. A minimal firewall is provided with some options that are always sane including dropping traffic with conntrack’s INVALID state and reverse path filtering. Support for IP sets is enabled in the kernel and the ipset utility is provided, but not yet integrated in an automated way. Android has group-based control over networking so basic control over networking is in the realm of permissions, but more advanced firewall features might be provided down the road.
Platform key signature permissions are restricted to system applications, to prevent old applications signed with the platform key remaining as a threat.
Access to the clipboard from applications in the background is disallowed. A global toggle is provided in the Security settings to reactivate it. This toggle will be phased out in favor of a new dangerous permission. It’s needed until a new permission is implemented and then adopted by the relevant clipboard manager applications.
Attack surface reduction
A minimal port of grsecurity’s
PERF_HARDEN feature extends the
sysctl with an additional level (3). This is enabled by default on production builds, with a
system property (
security.perf_harden) accessible to the ADB shell user for controlling it. The
userspace integration has been upstreamed:
3 and the kernel functionality was merged
CopperheadOS disables unprivileged access to ptrace by default by enabling the stackable Yama LSM
and setting the
kernel.yama.ptrace_scope sysctl to 2 by default. This can be controlled by the
ADB shell user via the
A port of grsecurity’s
DENYUSB feature provides a
kernel.deny_new_usb sysctl for disabling the
recognition of new USB devices. It reduces the attack surface exposing by USB drivers when active.
Integration in the lockscreen infrastructure provides automatic toggling based on whether the
screen is locked. A system property is exposed to users via Settings -> Security -> Device
security for choosing between it being enabled, controlled by lock state or disabled. The current
threat model only involves protecting the OS after the initial decryption process. It doesn’t
provide any protection for the early boot environment or the recovery image as that would be a
different kind of feature.
Unlike Android, CopperheadOS uses separate kernel builds for production (user) and developer (userdebug, eng) builds. This will make it possible to disable unnecessary debugging features in production.
CopperheadOS disables the kernel’s
CONFIG_AIO feature. It isn’t used or exposed by the base
system and is a dubious feature. It performs no better than thread pools and it can still block,
along with having coverage of only a tiny portion of blocking system calls even when considering
only commonly used system calls for IO. There are no known compatibility issues caused by having
this disabled. Since this is such a dubious niche feature, it’s also very poorly tested and it
doesn’t get much attention. Proposed improvements have been blocked based on the concern that
POSIX AIO is such a bad interface that trying to improve/extend it would be harmful.
Security level and advanced configuration
Note: This is not yet implemented for the port to Nougat, due to an upstream regression in property handling.
A slider is exposed in Settings -> Security -> Advanced for controlling the balance between performance and security. By default, it starts at 50%. It provides high level control over various performance vs. security tunables exposed there. All of the options can also be set manually rather than using the slider.
SECURE_DELETE feature is enabled, resulting in deleted content being overwritten with
zeros. This prevents sensitive data from lingering around in databases after it’s deleted. SQLite
is widely used by Android’s base system is the standard storage mechanism for applications, so
this results in lots of coverage. This has been
upstreamed. The default journal mode is
also set to
TRUNCATE rather than
PERSIST to stop data from lingering in the journal after
transactions. This change has also been
The hidepid=2 option is enabled for procfs, hiding processes owned by other UIDs. Since non-system apps each have a unique UID, this prevents apps from obtaining sensitive information about each other via /proc. There are exceptions for a few of the core services via the gid mount option (lmkd, servicemanager, keystore, debuggerd, logd, system_server) but not for apps. A subset of this was provided by SELinux, but it isn’t fine-grained enough. This enhancement was adopted upstream based on the implementation in CopperheadOS (it had been planned, but they were unaware of the gid mount option).
Some misuses of
memset for sanitizing data were replaced with
explicit_memset, to stop the
compiler from optimizing them out.
Many global function pointers in Android’s codebase have been made read-only. This is ongoing work and will need to be complemented with Control Flow Integrity (CFI) as many are compiler-generated. Some of this work has been upstreamed: 1, 2.
A port of grsecurity’s
DEVICE_SIDECHANNEL feature prevents information leakage via timing data
leaked by character and block devices. Processes without the MKNOD capability are provided with
the creation time as the access and modify time, along with not receiving notifications for either
access or modify events.