Sepolicy Basics for VR App Devs: what's up with "avc: denied" errors

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:

  1. the line contains either scontext=u:r:untrusted_app or tcontext=u:r:untrusted_app
  2. 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 is version.
  • dev=proc tells us that version is located in the proc 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 by read.
  • scontext=u:r:untrusted_app:s0:c104,c256,c512,c768 tells us the source context. This indicates that the operation was
    attempted by an untrusted_app (which is a generic label for apps without special privileges). The specific app is indicated by app= (see below)
  • tcontext=u:object_r:proc_version:s0 tells us the target context. This indicates that the target of the read has the label proc_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 be 0.
  • 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 by read.
  • scontext=u:r:untrusted_app:s0:c123,c256,c512,c768 tells us the source context is untrusted_app.
  • tcontext=u:object_r:serialno_prop:s0 tells us the target of the read has a context label u:object_r:serialno_prop:s0 – same as the name 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:

  1. In AOSP, SEPolicy (context) labels with the _prop suffix refer to system properties
  2. AOSP defines system properties using a standard macro, which defines properties s.t. if the property’s name is x, its data label is x_prop. Therefore, we know that the property’s name is "serialno`.
  3. 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.
  4. 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.

Notes about tools:

I didn’t talk about any sepolicy/SELinux tools in this post because it was low-effort and I couldn’t think of ways to make them sound useful. But here are some that you could find useful if you read about them somewhere else.

sesearch/seinfo

These tools help you search for rules in the device’s policy file.

These are not Android tools: they are tools for the more generic linux thing, SELinux. Importantly, the standard linux tools won’t work on Android policy filees. Instead use the ones included in the binary release of this project

The x86_64 tools wouldn’t work on my PC. What did work is pushing the arm64 binaries to my Android device and running them there.

For more info:

Audit2Allow

audit2allow is often used by devs to ‘pretty-print’ “avc: denied” error messages. It takes the denial log as input, and prints it out as a rule to allow the access.

While the output of audit2allow may be easier to grok, it is missing basically all the critical info you would need to figure out where the file involved in a denial is, e.g. the inode and the app package name. So it probably isn’t useful here, but feel free to prove me wrong.

ls -Z

on Android, ls -Z [path-to-a-thing] will show you the sepolicy label associated with that thing. For instance,

adb shell ls -Z /proc/version

outputs

u:object_r:prov_version:s0 /proc/version