Has this ever happened to you?
You integrate a library into your project. The library isn’t working for a bizarre reason. The library’s maintainer shrugs and says “IDK, it works on Linux. Never tested it on Android”.
You’re fucked – you have no idea what’s going on. You look through the logs for a relevant error, and you see:
type=1400 audit(0.0:744): avc: denied [some ridiculous garbage]
That looks like an error. Is it causing your issue?
No one’s going to be able to tell you. No one knows what this shit is. If they do, there’s a chance they will just say “you aren’t expected to understand this” or “Users aren’t expected to interpret this”, “look for another error”.
Welcome to the world of sepolicy.
Sepolicy Basics
SELinux/sepolicy define access rules on Android.
These rules are as stringently-enforced as a file being read-only, but can be more elaborate than file permissions rules. Using sepolicy, the OS dev can craft rules on the system like “third-party apps can’t read files matching this regex” or “processes with privileged access cannot also access the network”.
As you might expect, such a system is byzantine: knowledge of how to fully wield sepolicy is limited to a subset of OS devs. As per Android, you shouldn’t have to learn sepolicy to do normal Android app development.
But also as per Android, you shouldn’t have to debug “avc: denied” errors, either.
What’s up with “avc: denied” errors?
“avc: denied” errors are sepolicy access denials. It means your app was denied access to something (a file, socket, etc) due to an sepolicy policy.
The main way apps trigger sepolicy denials is by trying to access things through the filesystem instead of the Android SDK.
Examples of things that are against sepolicy rules:
- reading
/proc/version
- Reading the device serial number
- reading
/sys/class/power_supply/battery
- dlsym-ing libraries in
vendor/lib[64]
- A small subset of socket/network ops
Many of these restricted values are exposed with a level of indirection via the Android SDK. To read the system battery level, for instance, you are supposed to go through the Android SDK API – you are not supposed to read the level out of /sys/class/power_supply/battery
, as the API prevents abuses.
As a result, Android platform devs may tell you that you should never encounter sepolicy violations unless you are doing something the wrong way. The solution is to just do things the right way.
However, many of the operations prevented by sepolicy on Android are allowed on other flavors of Linux. So if you pull a Linux library into your Android project, it may perform some restricted operation – never anticipating that it can fail – fail, and trigger a “WTF” no-log failure condition.
In which case, you’re left with one choice: try to understand the cryptic “avc: denied” error message to determine why the error occurred.
Challenges with understanding sepolicy
The deck is really stacked against app devs when it comes to understanding sepolicy. Because it’s assumed that everyone who needs to know about sepolicy denials is an OS dev or a bad actor, all the tools helpful for tracking down sepolicy denials require root.
So you can’t get a callstack to the denial; you also can’t always get the exact name/path of the resource involved in the denial. Despite dealing with incomplete information, there is often a way to understand and root-cause sepolicy denials when you have to.
Root-causing “avc: denied” errors
First, make sure the error you’re looking at is actually coming from your app.
The logs may contain a lot of “avc: denied” errors unrelated to your app. Most developers have no idea what sepolicy is, so unless the error actually causes an issue, many of them just shrug and move on without investigating. As a result, your logs are probably full of benign “avc: denied” lines that have nothing to do with you.
You know the avc: denied
error message is related to your app if:
- the line contains either
scontext=u:r:untrusted_app
ortcontext=u:r:untrusted_app
- the line contains
app=[your app's package name]
If both these conditions are not met, then the error came from another process on the system and is most likely a) unrelated to your issue and b) benign.
Example of a relevant log:
8-22 00:22:15.523 xxxxx xxxxx W cat : type=1400 audit(0.0:744): avc: denied { read } for name="version" dev="proc" ino=4026532196 scontext=u:r:untrusted_app:s0:c104,c256,c512,c768 tcontext=u:object_r:proc_version:s0 tclass=file permissive=0 app=com.someorg.MyApp
This one is related to com.someorg.MyApp
’s issue. scontext
is untrusted_app
and the package listed in app=
matches the package name we’re looking for.
Example of an irrelevant log:
08-22 00:33:18.811 571 571 E SELinux : avc: denied { find } for pid=21516 uid=1000 name=GatekeeperService scontext=u:r:wifitelemetry:s0 tcontext=u:object_r:ovr_gatekeeper_service:s0 tclass=service_manager permissive=0
Neither scontext
or tcontext
mentioned untrusted_app
and there is no package listed. Not our issue.
Now that we’ve determined whether the error is relevant, the next step is to interpret the error.
Example 1
Let’s break down the log line:
8-22 00:22:15.523 xxxxx xxxxx W cat : type=1400 audit(0.0:744): avc: denied { read } for name="version" dev="proc" ino=4026532196 scontext=u:r:untrusted_app:s0:c104,c256,c512,c768 tcontext=u:object_r:proc_version:s0 tclass=file permissive=0 app=com.someorg.MyApp
-
name="version"
tells us that the name of the file accessed by read isversion
. -
dev=proc
tells us thatversion
is located in theproc
filesystem. We’ll have to read sepolicy to know where that is. More on this later. -
avc: denied { read }
indicates that the relevant operation was a read -
ino=4026532196
tells us the inode number of the file being accessed byread
. -
scontext=u:r:untrusted_app:s0:c104,c256,c512,c768
tells us the source context. This indicates that the operation was
attempted by anuntrusted_app
(which is a generic label for apps without special privileges). The specific app is indicated byapp=
(see below) -
tcontext=u:object_r:proc_version:s0
tells us the target context. This indicates that the target of the read has the labelproc_version
- Note: labels can apply to more than file in some instances. Not this one, though.
-
tclass=file
denotes that the resource being accessed is a file. -
permissive=0
indicates that the system is enforcing SELinux rules. Unless your HMD is rooted,permissive
should always be0
. -
app=com.someorg.MyApp
specifies which app was involved in this denial.
Based on this info, we now know:
- The denied operation was a
read
- The
read
attempt is initiated by our app - The target of
read
is a file - The name of the file being read (‘version’)
- The inode number of the file
- That the file is located in the proc filesystem
This is more than enough information to track down the path of the file.
adb shell find -inum 4026532187 2> /dev/null
returns ./proc/version
.
Therefore, we have the answer! the denial came from our app attempting to read /proc/version
somewhere.
And indeed, we see in stock AOSP’s sepolicy (note: I’m actually guessing this is the rule that gets applied, someone correct me if they can prove me wrong) that untrusted apps are not permitted to read from /proc/version
.
However, the SEPolicy for adb has no such restriction. This explains why we could read the file using adb shell
, but not in the app itself.
Example 2
Here’s one I found for VRChat
08-22 14:19:25.320 27926 27926 W at.oculus.quest: type=1400 audit(0.0:300101): avc: denied { read } for name="u:object_r:serialno_prop:s0" dev="tmpfs" ino=12416 scontext=u:r:untrusted_app:s0:c123,c256,c512,c768 tcontext=u:object_r:serialno_prop:s0 tclass=file permissive=0 app=com.vrchat.oculus.quest
-
avc: denied { read }
indicates that the relevant operation was a read -
name="u:object_r:serialno_prop:s0"
doesn’t tell us anything yet. It’s not a file name like last time, just a data label.
dev="tmpfs"
tells us the resource resides in a tmpfs filesystem, which is memory-based. -
ino=12416
tells us the inode number of the file being accessed byread
. -
scontext=u:r:untrusted_app:s0:c123,c256,c512,c768
tells us the source context isuntrusted_app
. -
tcontext=u:object_r:serialno_prop:s0
tells us the target of the read has a context labelu:object_r:serialno_prop:s0
– same as thename
field. -
tclass=file
denotes that the resource being accessed is a file. -
permissive=0
indicates that the system is enforcing SELinux rules. -
app=com.vrchat.oculus.quest
specifies which app was involved in this denial.
Unfortunately, this time, searching for the file via adb shell find -inum 12416 2> /dev/null
returns nothing. This probably means the file is not visible to an unprivileged adb shell.
What we know:
- The denied operation was a
read
- The
read
attempt is initiated by the VRChat app - The target of
read
is a file - The file is stored in
tmpfs
- we can’t access the file over
adb
either
What we don’t know
- the file’s name
- the path to the file
What do we do?
Easy way
This one would actually be a pain the in ass to figure out. It helps a lot to know the following:
- In AOSP, SEPolicy (context) labels with the
_prop
suffix refer to system properties - AOSP defines system properties using a standard macro, which defines properties s.t. if the property’s name is
x
, its data label isx_prop
. Therefore, we know that the property’s name is "serialno`. - System properties are actually stored as files under
/data/property
, which is not readable without root. This helps explain why the violation is classified as a file read. - The device’s serial number is considered a a sensitive field in modern Android, as it uniquely identifies a device and is non-resettable. Apps are supposed to request permission from the user and access this field via the Android SDK. By contrast, most system properties can be read by the app via a different API
Based on this info, I would guess that VRChat is attempting to read the device serial number via its system property – an operation that is restricted for the device serial number. This call is likely returning nothing to them right now, which may be fine if the serial was just being used for bug reporting or telemetry.
If we were the developers of this app, I would probably verify this hypothesis for checking the codebase for places where the code attempts to read the “ro.serialno” property.
Hard way
Let’s say we didn’t know all that.
let’s search inside stock AOSP’s SEPolicy for the label serialno_prop
. We specifically want to figure out where it’s getting defined.
- Search the sepolicy directory for instances of
u:object_r:serialno_prop:s0
The line we’re looking for is in property_contexts and has the following:
ro.serialno u:object_r:serialno_prop:s0
This means that the label u:object_r:serialno_prop:s0
uniquely refers to ro.serialno
, which is a system property name.
At this point, if you google “accessing ro.serialno
from app”, you should be able to learn the relevent info about how the field is accessed and how accesses are restricted.
Example 3
I don’t really want to do another example, but it’s important, because otherwise you may think all these are ultimately solveable via these two methods. They’re not.
Here’s one from DeoVR on Quest 2:
08-22 14:19:09.460 27189 27189 W UnityMain: type=1400 audit(0.0:300093): avc: denied { read } for name="u:object_r:vendor_board_init_prop:s0" dev="tmpfs" ino=12458 scontext=u:r:untrusted_app:s0:c125,c256,c512,c768 tcontext=u:object_r:vendor_board_init_prop:s0 tclass=file permissive=0 app=com.deovr.gearvr
Skipping ahead, this is a system property read, like in example 2. Let’s try to use the same strategy to understand it.
Easy way
Ok, so by our earlier logic, there should be a system property called vendor_board_init
somewhere on the system, which the Deo app can’t access.
Nope. querying for the property via adb shell getprop vendor_board_init
returns no such property.
Hard way
Let’s try this again.
By our previous example, we should search the stock AOSP sepolicy directory for u:object_r:vendor_board_init_prop:s0
.
However, this returns nothing. No such property is defined.
A hint at what’s happening comes from its name. vendor
properties are generally not defined by the OS, but rather someone the OS considers a “vendor”, e.g. the SoC manufacturer. Vendor-specific code is not a part of stock AOSP and is therefore not searchable online.
Additionally, it seems like this property isn’t following the naming convention of [name-of-property]_prop
. They are allowed to do that.
So what do we do?
Alternative Way
We suspect u:object_r:vendor_board_init_prop:s0
isn’t in AOSP because it’s vendor-defined. We don’t have source access to the vendor’s sepolicy. Is there a way to access it anyway?
Turns out, there is. sepolicy files are stored in plaintext on Android devices. Per the Android Vendor Interface (VINTF), the default location for vendor files on the device is /vendor/etc/selinux/
.
So let’s try adb shell grep u:object_r:vendor_board_init_prop:s0
This returns several hits in /vendor/etc/selinux/vendor_property_context
ro.vendor.audio.fcs.speaker u:object_r:vendor_board_init_prop:s0
ro.vendor.hw.device_config u:object_r:vendor_board_init_prop:s0
ro.vendor.hw.deviceid u:object_r:vendor_board_init_prop:s0
ro.vendor.product.model_extended u:object_r:vendor_board_init_prop:s0
vendor.board_init.complete u:object_r:vendor_board_init_prop:s0
There are several properties defined under u:object_r:vendor_board_init_prop:s0
– it’s not following the convention of “one property per label” that we saw in Example 2.
When this happens (as as far as I know) it isn’t possible to further determine which of these properties triggered the “avc: denied” message. Our best bet is to search the app’s source code for references to any of the above properties. If they are all using the same label, that means the same rules will apply to all of them, and any attempts to read will be equally illegal.
Alternative way #2
In the logs, there is a log line directly below the avc: denied
line
libc : Access denied finding property "ro.vendor.product.model_extended"
The logs themselves don’t give us nearly enough info to confirm that this is the property that vendor_board_init_prop
refers to, but it’d be a good start.
Checking /vendor/etc/selinux/vendor_property_context
confirms that ro.vendor.product.model_extended
is under u:object_r:vendor_board_init_prop:s0
. Good chance that’s the correct property.
Is sepolicy actually your bug?
Fun thing to ask yourself after diving into OS internals to understand something.
All of the above errors are things I found in working apps. None of them were causing noticeable issues.
It’s pretty common for app devs to ignore “avc: denied” error messages produced by their app until they notice a problem. By contrast, they tend to stand out as relevant as soon as you’re trying to track down a bug.
Best practice should be to pay attention to these errors as you develop – try to solve them when they are introduced if you can, but failing that, at least know what they are. Pay attention when they’re introduced. That way, the next time you have a bizarre bug you can’t figure out, you know whether the “avc: denied” error points to it, and whether you actually need to dive into sepolicy to undestand your issue.