Documentation

<- Back to documentation

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.
  • SSP, full RELRO and PIE and _FORTIFY_SOURCE=2 are enabled by default. The dynamic linker only supports position independent executables. Text relocations are not supported on 64-bit or modern app API levels or the base OS for 32-bit.
  • etc.

Exec-based spawning model

CopperheadOS spawns applications from the Zygote service in the traditional Unix way with fork and 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. The sandboxed processes aren’t tied to the architecture of the app, so on CopperheadOS they are set to always run as 64-bit unlike vanilla Chromium where they will always be 32-bit. 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 due to apparent race conditions.

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. It also temporarily makes use of -fwrapv to prevent optimizations from introducing security issues due to reliance on undefined signed overflow. However, this will be replaced by automatic integer overflow checking via usage of -fsanitize=shift,signed-integer-overflow -fsanitize-trap=shift,signed-integer-overflow once Chromium makes further progress towards eliminating bugs caught by UBSan.

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. The form autofill feature is also disabled by default until it has a saner implementation less vulnerable to phishing.

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

Hardened allocator

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.

Regions

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).

Small allocations

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.

Large allocations

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.

Extended _FORTIFY_SOURCE implementation

The _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 in addition to the coverage Bionic already had:

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 _FORTIFY_SOURCE 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.

The at_quick_exit and 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.

Miscellaneous improvements

Allocations larger than PTRDIFF_MAX are prevented, preventing a class of overflows. This has been upstreamed for mmap and mremap.

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 explicit_memset and secure_getenv for use elsewhere
  • replaced the broken implementations of issetugid and explicit_bzero
  • larger default stack size on 64-bit (1MiB -> 8MiB)
  • and various other assorted improvements

Android Runtime

As part of moving towards reducing trust in /data/ (i.e. state not covered by verified boot), WITH_DEXPREOPT and WITH_DEXPREOPT_PIC are always enabled and WITH_DEXPREOPT_BOOT_IMG_ONLY is 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.

Compiler hardening

  • 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.
  • Added -Wsuggest-attribute=format warning 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

  • dalvikcache_data_file execute access limited to domains for third party apps (isolated_app and untrusted_app), preventing a trivial form of malware persistence without re-exploitation
  • dalvikcache_data_file symlinks forbidden

See the Android Runtime section for details on how this works.

  • eliminated code injection holes
    • Removed gpu_device execute access - upstreamed
    • Removed ashmem and tmpfs execute access (traditionally used by the Dalvik JIT and CopperheadOS disables the usage in Firefox’s custom linker via an environment variable - no other compatibility issues have been found)
    • 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 (upstreamed) / 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.
  • Split out untrusted_apps signed with the release/media/shared keys (i.e. all of the base system untrusted_apps) into untrusted_base_app, allowing the removal of execmod, execmem, app_data_file execute, dalvikcache_data_file execute and asec access
  • Split out isolated_apps signed with the release/media/shared keys (i.e. all of the base system isolated_apps) into isolated_base_app, allowing the removal of dalvikcache_data_file execute
  • 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: zoneinfo, vmstat.

Basic network interface and routing information (/proc/net/dev, /proc/net/if_inet6, /proc/net/ipv6_route, /proc/net/route) is split out from the proc_net label into a new proc_net_devroute label with it permitted in the same domains and proc_net access is removed for untrusted_app / untrusted_base_app. Since monitoring network connections is a useful feature, the org.secuso.privacyfriendlynetmonitor app is bundled and given a dedicated netmonitor SELinux domain based on untrusted_base_app with proc_net access added back. Access is also still permitted via the disabled by default ADB shell. Since monitoring by users is covered well by the base system, there’s little reason for any other apps to have access to this information. A small subset of the information is available via NetworkStatsManager, but it requires the PACKAGE_USAGE_STATS permission to retrieve information for other apps which has to be manually granted to third party apps by the user and can’t simply be requested by an app directly.

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.

Reduced side channel leakage for file-based encryption

Padding for ext4 encryption file names is increased from the default of 4 bytes to 32 bytes. This reduces the amount of information leaked about file name length. There’s lots of room for further improvements reducing metadata leakage but it involves changes the ext4 encryption format and would need to happen in the Linux kernel.

PIN entry scrambling

A setting is offered for randomizing the layout of PIN entry. It will become significantly more useful once the 2 factor authentication feature above is implemented, since the combination of a strong passphrase with a fingerprint + PIN as a secondary unlock method for convenience will be the recommended setup for most people. It currently only impacts the lockscreen but may be extended. Note that the owner account setting also currently controls this for the lockscreen as a whole, but it will eventually use the per-user setting instead.

Privacy-focused defaults

The default settings have been altered to emphasize privacy 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

Over-the-air updates

Legacy update system (Nexus 5X, 6P, 9)

CopperheadOS uses an 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.

Modern update system based on update_engine (Pixel, Pixel XL)

The new update system is a minimal, clean layer on top of the update_engine infrastructure from ChromeOS and Brillo. It exclusively implements automatic background updates. User control will be offered via simple toggles to control whether it downloads on metered data connections or when roaming is detected, rather than updating ever being a manual procedure. Newer devices have dual OS partitions rather than only firmware partitions and can update the alternate set of partitions in the background at low priority without causing any disruption to users. By the time the user needs to be informed about an update, it’s already complete along with the bulk of the post-install work like ahead-of-time compilation for apps not bundled preoptimized within the OS, which is important for CopperheadOS since it disables JIT compilation / profiling in favor of simply performing full AOT compilation with no JIT and minimal use of the interpreter for cold code like one-time initialization to save space / memory.

The updater fetches a device-specific/channel-specific plain text file from a static file server with a space-separated version and build date on each line with the most recent builds at the top. Only the first line of the file is used by the update client within the OS. The client will try to fetch an incremental for the current version to the new version. If an incremental isn’t available, it will fall back to a full update package able to update from any past version. Currently, incrementals from the past 3 versions to the current are generated and then published. The incremental or full update is then downloaded, the signature is verified and the build date is then checked against the build date claimed by the server to prevent downgrade attacks by an attacker with control of the server. The payload is then passed to update_engine, which performs another layer of signature verification on the payload. When update_engine reports completion, a notification will be presented to the user with a reboot button, although the update will kick in when the device is booted again whether or not it’s via the notification.

The updater currently checks for updates every hour. If a failure occurs while trying to download an update, it saves the current download state and will resume the download next time it runs if that’s still the most recent update.

The dual partition system makes updates significantly more robust since the updates are performed on a set of unused partitions and have no impact until they are verified and marked as ready. The next boot of the OS will then boot into the new version. A verification step is run fairly late in the late boot process where every block from the OS partitions is read (update_verifier) in order to leverage dm-verity to detect any corruption. If anything goes wrong during booting and verification, it can automatically fall back to the previous version since it’s still installed on the previously used set of partitions.

Networking

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 ANDROID_ID instead. Note that the Qualcomm Atheros WiFi driver (qcacld-2.0) breaks when the MAC address is changed so both forms of randomization are disabled on devices using that driver (Nexus 5X, Pixel, Pixel XL).

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.

Permissions

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

NFC, NDEF Push and Bluetooth are disabled by default.

A minimal port of grsecurity’s PERF_HARDEN feature extends the kernel.perf_event_paranoid 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: 1, 2, 3 and the kernel functionality was merged alongside it.

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 security.ptrace_scope property.

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-focused apps

Chromium

See the top level section.

Silence

The AOSP SMS app (Messaging) is replaced with Silence to provide the option of end-to-end encryption over SMS when a data connection is not available to either contact to enable better options.

PDF Viewer

CopperheadOS includes a custom PDF Viewer based on pdf.js in a WebView. It doesn’t require any permissions since it relies on content providers via ACTION_VIEW and ACTION_OPEN_DOCUMENT for the application/pdf mime type rather than requesting storage access. It only has access to data that the user explicitly provides to it.

Since JavaScript is memory safe, memory safety bugs in the PDF implementation itself are not a concern and the performance is still tolerable. The app exposes a very small subset of the Chromium attack surface and the WebView sandbox acts as an additional layer of isolation that’s significantly stronger than the usual app sandbox. It renders via a 2D canvas and loads fonts via the FontFace API using the hardened Chromium font rendering stack (font sanitization, etc.). It loads a new viewer for each document in order to improve isolation between documents compared to reusing the same pdf.js environment.

The app uses Content Security Policy (CSP) to permit only static content from the app assets, image blob: data and a placeholder HTTPS URL used to receive data from the app. The policy prevents dynamic and inline JS/CSS and provides some attack surface reduction for the DOM. JS API attack surface is limited indirectly due to the code being static. Content access, file access and network access are also disabled for the WebView, so it can only access apk assets and resources. It receives the PDF data from the app by making a placeholder HTTPS request which is substituted with the PDF input stream by the app without needing to permit the WebView to have direct access to content providers. Various unnecessary WebView features are disabled including caching, cookies, saving form data and loading other URLs.

An alternative to this would be a custom isolatedProcess / seccomp-bpf sandbox to contain a pure Java PDF library and some associated native code, but pdf.js was deemed to be a more secure and viable option than any other existing open source libraries. The widely used libraries are written in C and C++ (mupdf, poppler, pdfium). The pdf.js solution is also nearly ideal when the source of the PDFs is Chromium or a Chromium-based browser due to presenting only a small subset of the same attack surface.

Kernel self-protection

CopperheadOS has some minimal kernel hardening changes that are independent of whether a device has a full PaX kernel port. PaX is documented separately from this section. Upstream kernel security features will often be enabled by Google and those features are not currently listed here.

Pages are zeroed upon being freed to reduce the lifetime of sensitive data. Pages are also verified to be zero before being doled out again to catch write-after-free bugs.

The slab allocator (SLUB) is hardened via disabling slab merging, additional memory corruption sanity checks, per-cache XOR encryption for the free lists and clearing data when freed. Ideally, a hardened allocator with out-of-line metadata would be implemented along the same lines as the malloc implementation in userspace.

For kernel attack surface reduction features, see the relevant section.

Miscellaneous features

SQLite’s 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 upstreamed.

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.

Regressions

These features were available on CopperheadOS in the past and are still relevant but are currently unavailable.

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.

PaX kernel

CopperheadOS maintained ports of PaX to the Samsung Galaxy S4 and Nexus 5 but those devices are no longer supported. Some PaX and grsecurity features are still included on a case-by-case basis, and many of the features are provided in other ways (hidepid, Android inet groups and SELinux which covers the same ground as grsecurity RBAC and many of the smaller features).

Kernel Self Protection Project

PaX and grsecurity features are being landed in mainline as part of the KSPP, so the set of added security features is changing as they land upstream and new features are introduced. This section will be updated to include a list of named PaX and grsecurity features that are still wanted for CopperheadOS along with some of the unnamed changes (i.e. without configuration options). One of the advantages of grsecurity kernels beyond the security features is the security backporting work going far beyond the kernel.org longterm kernels and Google’s current Android LTS branches.

Blocking issues for supporting current devices

PaX and grsecurity currently have poor support for 64-bit ARM, so the migration of Nexus devices to 64-bit has temporarily rendered most of the features unavailable. The features that are available are mostly those that are not useful to Android due to overlap with SELinux or not usable.

Another issue is that the PaX stable patches stopped being publicly available shortly after we began so and the sponsorship system wouldn’t work for CopperheadOS since the patches would need to be published alongside releases to comply with the GPL2 license of the Linux kernel. The test patches are quickly moving target. They might not be stable enough for CopperheadOS but it would be possible to maintain short-term branches lagging a bit behind PaX and grsecurity while pulling in kernel.org stable fixes along with the ones landing in grsecurity.

The biggest barrier to production quality usage of PaX / grsecurity on mobile for both Android and other Linux-based operating systems are the required SoC vendor kernels. Our Nexus devices are stuck on a long-term 3.10 branch with millions of lines of Qualcomm driver code not available in mainline. The Pixel phones are similarly stuck on 3.18. The Android common kernel changes are actually quite small now that a fair bit is upstreamed so mainline kernels would be fairly easy to use if it was only about traditional Linux vs. Android. For the Nexus 5 and Samsung Galaxy S4, LTS branches had to be maintained based on porting from the PaX stable patches near the same version (i.e. porting from 3.2 PaX stable to 3.4) so the unavailability of stable patches without the base being a moving target compounds this issue.

The positive side to the mobile driver issues is that they’re entirely hardware-specific. It wouldn’t be an issue for targeting laptop/desktop hardware and would be much less bad for Brillo class embedded devices. As this situation improves for mobile Linux, the problem will fade away.