<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom"><title>Alessandro Sangiorgi — GPU Performance Engineer</title><link href="https://contact.alessandrosangiorgi.net/" rel="alternate" type="text/html"/><link href="https://contact.alessandrosangiorgi.net/atom.xml" rel="self" type="application/atom+xml"/><id>https://contact.alessandrosangiorgi.net/</id><updated>2026-04-08T19:18:32Z</updated><author><name>Alessandro Sangiorgi</name><email>mail@alessandrosangiorgi.net</email><uri>https://contact.alessandrosangiorgi.net/</uri></author><entry><title>Retrieving the ARP Table on Android SDK 30+ via Netlink</title><link href="https://contact.alessandrosangiorgi.net/posts/ip-neigh-android-sdk30-netlink-workaround/" rel="alternate" type="text/html"/><id>https://contact.alessandrosangiorgi.net/posts/ip-neigh-android-sdk30-netlink-workaround/</id><published>2026-04-08T00:00:00Z</published><updated>2026-04-08T00:00:00Z</updated><summary>Google blocked netlink socket bind in Android 11 (targetSDK 30), breaking ARP table access for apps. I wrote a JNI library that bypasses the restriction by sending RTNetlink dump requests without binding — until SELinux closed the door for good. Ubiquiti wanted to buy it for WiFiMan; I open-sourced it instead.</summary><content type="html">&lt;p&gt;Starting with Android 11 (API level 30), Google removed the ability for apps to run &lt;code&gt;ip neigh&lt;/code&gt; or bind netlink sockets. This broke every app that relied on reading the ARP table — network scanners, device discovery tools, local network diagnostics. The change was intentional: Google argued that exposing the neighbor table leaks information about other devices on the local network, which is a privacy concern.&lt;/p&gt;
&lt;p&gt;The problem is that there was no replacement API. Apps that needed neighbor discovery — for example, to find smart home devices, printers, or other hosts on the LAN — were simply out of luck. So I wrote a native library that retrieves the ARP table via RTNetlink without binding the socket, shipped it as an Android library, and &lt;a href="https://github.com/fulvius31/ip-neigh-sdk30"&gt;open-sourced it&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="background-netlink-and-rtnetlink"&gt;Background: Netlink and RTNetlink&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Netlink&lt;/strong&gt; is the Linux kernel&amp;rsquo;s IPC mechanism for communication between the kernel and user-space processes. It&amp;rsquo;s a socket-based interface (&lt;code&gt;AF_NETLINK&lt;/code&gt;) that replaces older &lt;code&gt;ioctl&lt;/code&gt;-based approaches for configuring networking, routing, and other kernel subsystems. Netlink sockets support multicast (subscribing to kernel events) and request-response patterns (dumping tables, modifying routes).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;RTNetlink&lt;/strong&gt; (&lt;code&gt;NETLINK_ROUTE&lt;/code&gt;) is the most commonly used netlink family. It handles everything related to network configuration: routes, addresses, links, neighbors (ARP/NDP), traffic control. When you run &lt;code&gt;ip neigh show&lt;/code&gt;, &lt;code&gt;iproute2&lt;/code&gt; opens an &lt;code&gt;AF_NETLINK&lt;/code&gt; socket with the &lt;code&gt;NETLINK_ROUTE&lt;/code&gt; protocol, sends an &lt;code&gt;RTM_GETNEIGH&lt;/code&gt; message, and parses the kernel&amp;rsquo;s response.&lt;/p&gt;
&lt;p&gt;The standard flow looks like this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create a socket: &lt;code&gt;socket(AF_NETLINK, SOCK_DGRAM, NETLINK_ROUTE)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bind&lt;/strong&gt; it to a netlink address with your PID&lt;/li&gt;
&lt;li&gt;Send a dump request (&lt;code&gt;NLM_F_REQUEST | NLM_F_DUMP&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Receive and parse the response&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Google&amp;rsquo;s restriction in SDK 30 targets &lt;strong&gt;step 2&lt;/strong&gt; — apps can no longer &lt;code&gt;bind()&lt;/code&gt; a netlink socket. Without binding, the traditional flow fails.&lt;/p&gt;
&lt;h2 id="the-workaround-skip-the-bind"&gt;The Workaround: Skip the Bind&lt;/h2&gt;
&lt;p&gt;The key insight is that &lt;code&gt;bind()&lt;/code&gt; is not strictly required. You can &lt;code&gt;send()&lt;/code&gt; on an unbound netlink socket and the kernel will still route the response back to you. The socket gets an implicit binding when you send the first message. This is the core of the workaround.&lt;/p&gt;
&lt;p&gt;The implementation lives in a JNI (Java Native Interface) layer — C code compiled with the Android NDK and called from Java/Kotlin. The Java side opens a &lt;code&gt;ParcelFileDescriptor&lt;/code&gt; pipe and passes the write end&amp;rsquo;s file descriptor down to native code. The native code does the netlink work and writes the results back through the pipe, which the Java side reads.&lt;/p&gt;
&lt;h3 id="creating-the-socket-and-sending-the-request"&gt;Creating the Socket and Sending the Request&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;struct&lt;/span&gt; sockaddr_nl saddr;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;int&lt;/span&gt; s &lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt; &lt;span style="color:#d2a8ff;font-weight:bold"&gt;socket&lt;/span&gt;(AF_NETLINK, SOCK_DGRAM, NETLINK_ROUTE);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#d2a8ff;font-weight:bold"&gt;memset&lt;/span&gt;(&lt;span style="color:#ff7b72;font-weight:bold"&gt;&amp;amp;&lt;/span&gt;saddr, &lt;span style="color:#a5d6ff"&gt;0&lt;/span&gt;, &lt;span style="color:#ff7b72"&gt;sizeof&lt;/span&gt;(saddr));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;saddr.nl_family &lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt; AF_NETLINK;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;saddr.nl_pid &lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt; &lt;span style="color:#d2a8ff;font-weight:bold"&gt;getpid&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#8b949e;font-style:italic"&gt;// No bind() call — this is the whole trick
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The dump request asks the kernel for the neighbor table. The message type is &lt;code&gt;RTM_GETNEIGH&lt;/code&gt; (value 30), and the flags include &lt;code&gt;NLM_F_DUMP&lt;/code&gt; to request the full table:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;int&lt;/span&gt; &lt;span style="color:#d2a8ff;font-weight:bold"&gt;do_route_dump_request&lt;/span&gt;(&lt;span style="color:#ff7b72"&gt;int&lt;/span&gt; sock)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;struct&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;struct&lt;/span&gt; nlmsghdr nlh;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;struct&lt;/span&gt; rtmsg rtm;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } nl_request;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; nl_request.nlh.nlmsg_type &lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt; &lt;span style="color:#a5d6ff"&gt;30&lt;/span&gt;; &lt;span style="color:#8b949e;font-style:italic"&gt;// RTM_GETNEIGH
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; nl_request.nlh.nlmsg_flags &lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt; NLM_F_REQUEST &lt;span style="color:#ff7b72;font-weight:bold"&gt;|&lt;/span&gt; NLM_F_DUMP;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; nl_request.nlh.nlmsg_pid &lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt; &lt;span style="color:#d2a8ff;font-weight:bold"&gt;getpid&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; nl_request.nlh.nlmsg_len &lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;sizeof&lt;/span&gt;(nl_request);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; nl_request.nlh.nlmsg_seq &lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt; &lt;span style="color:#a5d6ff"&gt;0&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; nl_request.rtm.rtm_family &lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt; AF_INET;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;return&lt;/span&gt; &lt;span style="color:#d2a8ff;font-weight:bold"&gt;send&lt;/span&gt;(sock, &lt;span style="color:#ff7b72;font-weight:bold"&gt;&amp;amp;&lt;/span&gt;nl_request, &lt;span style="color:#ff7b72"&gt;sizeof&lt;/span&gt;(nl_request), &lt;span style="color:#a5d6ff"&gt;0&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;nlmsghdr&lt;/code&gt; is the standard netlink message header — every netlink message starts with one. It specifies the message type, flags, the sender&amp;rsquo;s PID, and the total message length. The &lt;code&gt;rtmsg&lt;/code&gt; payload that follows tells the kernel we want IPv4 neighbors (&lt;code&gt;AF_INET&lt;/code&gt;).&lt;/p&gt;
&lt;h3 id="receiving-and-parsing-the-response"&gt;Receiving and Parsing the Response&lt;/h3&gt;
&lt;p&gt;The response arrives as a stream of &lt;code&gt;nlmsghdr&lt;/code&gt; messages, each containing a neighbor entry. The receive logic uses a two-pass approach — first peek to get the total message size, then allocate and read:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;static&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;int&lt;/span&gt; &lt;span style="color:#d2a8ff;font-weight:bold"&gt;rtnl_recvmsg&lt;/span&gt;(&lt;span style="color:#ff7b72"&gt;int&lt;/span&gt; fd, &lt;span style="color:#ff7b72"&gt;struct&lt;/span&gt; msghdr &lt;span style="color:#ff7b72;font-weight:bold"&gt;*&lt;/span&gt;msg, &lt;span style="color:#ff7b72"&gt;char&lt;/span&gt; &lt;span style="color:#ff7b72;font-weight:bold"&gt;**&lt;/span&gt;answer)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;struct&lt;/span&gt; iovec &lt;span style="color:#ff7b72;font-weight:bold"&gt;*&lt;/span&gt;iov &lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt; msg&lt;span style="color:#ff7b72;font-weight:bold"&gt;-&amp;gt;&lt;/span&gt;msg_iov;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;char&lt;/span&gt; &lt;span style="color:#ff7b72;font-weight:bold"&gt;*&lt;/span&gt;buf;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;int&lt;/span&gt; len;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; iov&lt;span style="color:#ff7b72;font-weight:bold"&gt;-&amp;gt;&lt;/span&gt;iov_base &lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt; NULL;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; iov&lt;span style="color:#ff7b72;font-weight:bold"&gt;-&amp;gt;&lt;/span&gt;iov_len &lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt; &lt;span style="color:#a5d6ff"&gt;0&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; len &lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt; &lt;span style="color:#d2a8ff;font-weight:bold"&gt;rtnl_receive&lt;/span&gt;(fd, msg, MSG_PEEK &lt;span style="color:#ff7b72;font-weight:bold"&gt;|&lt;/span&gt; MSG_TRUNC);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;if&lt;/span&gt; (len &lt;span style="color:#ff7b72;font-weight:bold"&gt;&amp;lt;&lt;/span&gt; &lt;span style="color:#a5d6ff"&gt;0&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;return&lt;/span&gt; len;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; buf &lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt; &lt;span style="color:#d2a8ff;font-weight:bold"&gt;malloc&lt;/span&gt;(len);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;if&lt;/span&gt; (&lt;span style="color:#ff7b72;font-weight:bold"&gt;!&lt;/span&gt;buf)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;return&lt;/span&gt; &lt;span style="color:#ff7b72;font-weight:bold"&gt;-&lt;/span&gt;ENOMEM;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; iov&lt;span style="color:#ff7b72;font-weight:bold"&gt;-&amp;gt;&lt;/span&gt;iov_base &lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt; buf;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; iov&lt;span style="color:#ff7b72;font-weight:bold"&gt;-&amp;gt;&lt;/span&gt;iov_len &lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt; len;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; len &lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt; &lt;span style="color:#d2a8ff;font-weight:bold"&gt;rtnl_receive&lt;/span&gt;(fd, msg, &lt;span style="color:#a5d6ff"&gt;0&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;if&lt;/span&gt; (len &lt;span style="color:#ff7b72;font-weight:bold"&gt;&amp;lt;&lt;/span&gt; &lt;span style="color:#a5d6ff"&gt;0&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#d2a8ff;font-weight:bold"&gt;free&lt;/span&gt;(buf);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;return&lt;/span&gt; len;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72;font-weight:bold"&gt;*&lt;/span&gt;answer &lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt; buf;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;return&lt;/span&gt; len;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;MSG_PEEK | MSG_TRUNC&lt;/code&gt; is a useful trick: it returns the total message size without consuming the data, so we know exactly how much to &lt;code&gt;malloc()&lt;/code&gt; before the real read.&lt;/p&gt;
&lt;h3 id="extracting-neighbor-entries"&gt;Extracting Neighbor Entries&lt;/h3&gt;
&lt;p&gt;Each netlink message with type &lt;code&gt;RTM_GETNEIGH&lt;/code&gt; (28 in the response) contains a &lt;code&gt;ndmsg&lt;/code&gt; structure followed by route attributes. The parsing loop walks the message chain using the standard &lt;code&gt;NLMSG_OK&lt;/code&gt; / &lt;code&gt;NLMSG_NEXT&lt;/code&gt; macros and extracts the IP address, MAC address, interface name, and NUD (Neighbor Unreachability Detection) state:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;while&lt;/span&gt; (&lt;span style="color:#d2a8ff;font-weight:bold"&gt;NLMSG_OK&lt;/span&gt;(h, msglen)) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;if&lt;/span&gt; (h&lt;span style="color:#ff7b72;font-weight:bold"&gt;-&amp;gt;&lt;/span&gt;nlmsg_type &lt;span style="color:#ff7b72;font-weight:bold"&gt;==&lt;/span&gt; &lt;span style="color:#a5d6ff"&gt;28&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; route_entry &lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt; (&lt;span style="color:#ff7b72"&gt;struct&lt;/span&gt; rtmsg &lt;span style="color:#ff7b72;font-weight:bold"&gt;*&lt;/span&gt;) &lt;span style="color:#d2a8ff;font-weight:bold"&gt;NLMSG_DATA&lt;/span&gt;(h);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; route_attribute &lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt; (&lt;span style="color:#ff7b72"&gt;struct&lt;/span&gt; rtattr &lt;span style="color:#ff7b72;font-weight:bold"&gt;*&lt;/span&gt;) &lt;span style="color:#d2a8ff;font-weight:bold"&gt;RTM_RTA&lt;/span&gt;(route_entry);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;if&lt;/span&gt; (route_attribute&lt;span style="color:#ff7b72;font-weight:bold"&gt;-&amp;gt;&lt;/span&gt;rta_type &lt;span style="color:#ff7b72;font-weight:bold"&gt;==&lt;/span&gt; RTA_DST) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#d2a8ff;font-weight:bold"&gt;inet_ntop&lt;/span&gt;(AF_INET, &lt;span style="color:#d2a8ff;font-weight:bold"&gt;RTA_DATA&lt;/span&gt;(route_attribute),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; destination_address, &lt;span style="color:#ff7b72"&gt;sizeof&lt;/span&gt;(destination_address));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; inf_msg_ptr &lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt; (&lt;span style="color:#ff7b72"&gt;struct&lt;/span&gt; ifinfomsg &lt;span style="color:#ff7b72;font-weight:bold"&gt;*&lt;/span&gt;) &lt;span style="color:#d2a8ff;font-weight:bold"&gt;NLMSG_DATA&lt;/span&gt;(h);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; rta_ptr &lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt; (&lt;span style="color:#ff7b72"&gt;struct&lt;/span&gt; rtattr &lt;span style="color:#ff7b72;font-weight:bold"&gt;*&lt;/span&gt;) &lt;span style="color:#d2a8ff;font-weight:bold"&gt;IFLA_RTA&lt;/span&gt;(inf_msg_ptr);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#d2a8ff;font-weight:bold"&gt;memcpy&lt;/span&gt;(mac_buf, &lt;span style="color:#d2a8ff;font-weight:bold"&gt;RTA_DATA&lt;/span&gt;(rta_ptr), &lt;span style="color:#a5d6ff"&gt;10&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; rtmp &lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt; (&lt;span style="color:#ff7b72"&gt;struct&lt;/span&gt; ndmsg &lt;span style="color:#ff7b72;font-weight:bold"&gt;*&lt;/span&gt;) &lt;span style="color:#d2a8ff;font-weight:bold"&gt;NLMSG_DATA&lt;/span&gt;(h);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;char&lt;/span&gt; ifname[&lt;span style="color:#a5d6ff"&gt;1024&lt;/span&gt;];
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#d2a8ff;font-weight:bold"&gt;if_indextoname&lt;/span&gt;(inf_msg_ptr&lt;span style="color:#ff7b72;font-weight:bold"&gt;-&amp;gt;&lt;/span&gt;ifi_index, ifname);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#8b949e;font-style:italic"&gt;// Output depends on NUD state
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;switch&lt;/span&gt; (rtmp&lt;span style="color:#ff7b72;font-weight:bold"&gt;-&amp;gt;&lt;/span&gt;ndm_state) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;case&lt;/span&gt; &lt;span style="color:#79c0ff;font-weight:bold"&gt;NUD_REACHABLE&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#d2a8ff;font-weight:bold"&gt;fprintf&lt;/span&gt;(fd, &lt;span style="color:#a5d6ff"&gt;&amp;#34;%s dev %s lladdr %02x:%02x:%02x:%02x:%02x:%02x REACHABLE&lt;/span&gt;&lt;span style="color:#79c0ff"&gt;\n&lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; destination_address, ifname,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; mac_buf[&lt;span style="color:#a5d6ff"&gt;4&lt;/span&gt;], mac_buf[&lt;span style="color:#a5d6ff"&gt;5&lt;/span&gt;], mac_buf[&lt;span style="color:#a5d6ff"&gt;6&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; mac_buf[&lt;span style="color:#a5d6ff"&gt;7&lt;/span&gt;], mac_buf[&lt;span style="color:#a5d6ff"&gt;8&lt;/span&gt;], mac_buf[&lt;span style="color:#a5d6ff"&gt;9&lt;/span&gt;]);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;break&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;case&lt;/span&gt; &lt;span style="color:#79c0ff;font-weight:bold"&gt;NUD_STALE&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#8b949e;font-style:italic"&gt;// ... same format, STALE state
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;break&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;case&lt;/span&gt; &lt;span style="color:#79c0ff;font-weight:bold"&gt;NUD_DELAY&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;case&lt;/span&gt; &lt;span style="color:#79c0ff;font-weight:bold"&gt;NUD_PROBE&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;case&lt;/span&gt; &lt;span style="color:#79c0ff;font-weight:bold"&gt;NUD_FAILED&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#8b949e;font-style:italic"&gt;// ... handle each state
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;break&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; h &lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt; &lt;span style="color:#d2a8ff;font-weight:bold"&gt;NLMSG_NEXT&lt;/span&gt;(h, msglen);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The NUD states map directly to what &lt;code&gt;ip neigh&lt;/code&gt; shows: &lt;code&gt;REACHABLE&lt;/code&gt; means the entry was recently confirmed, &lt;code&gt;STALE&lt;/code&gt; means it hasn&amp;rsquo;t been used recently, &lt;code&gt;DELAY&lt;/code&gt; and &lt;code&gt;PROBE&lt;/code&gt; are transitional states during revalidation, and &lt;code&gt;FAILED&lt;/code&gt; means resolution timed out. The output format intentionally mirrors &lt;code&gt;ip neigh show&lt;/code&gt; so that existing parsing code works unchanged.&lt;/p&gt;
&lt;p&gt;The output is written to the file descriptor passed from Java, making it available to the app through the &lt;code&gt;ParcelFileDescriptor&lt;/code&gt; pipe.&lt;/p&gt;
&lt;h2 id="selinux-closes-the-door"&gt;SELinux Closes the Door&lt;/h2&gt;
&lt;p&gt;This workaround worked well for a while. But Google eventually patched it — not at the socket API level, but through &lt;strong&gt;SELinux policy&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Android&amp;rsquo;s SELinux policies were updated to deny &lt;code&gt;nlmsg_read&lt;/code&gt; permissions on &lt;code&gt;netlink_route_socket&lt;/code&gt; for untrusted app domains. This means that even if you manage to create and use a netlink socket without binding, the kernel&amp;rsquo;s SELinux hooks will block the &lt;code&gt;RTM_GETNEIGH&lt;/code&gt; dump request at the security policy level. The &lt;code&gt;send()&lt;/code&gt; succeeds but the response never comes — or the socket operation itself is denied with &lt;code&gt;EACCES&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;SELinux operates at a layer below the system call interface. There is no user-space workaround for a mandatory access control denial. The library stopped working on devices with updated SELinux policies, which rolled out progressively across Android 12 and later.&lt;/p&gt;
&lt;h2 id="the-ubiquiti-story"&gt;The Ubiquiti Story&lt;/h2&gt;
&lt;p&gt;Before I open-sourced the library, &lt;a href="https://ui.com"&gt;Ubiquiti&lt;/a&gt; reached out. They wanted to buy the workaround for their app &lt;a href="https://play.google.com/store/apps/details?id=com.ubnt.usurvey"&gt;WiFiMan&lt;/a&gt;, a popular network scanner that was affected by the same SDK 30 restriction. They offered to pay for a license.&lt;/p&gt;
&lt;p&gt;I declined the offer. I preferred to release the code as open source under CC0-1.0 (public domain). Shortly after, Ubiquiti was able to implement their own solution independently.&lt;/p&gt;
&lt;h2 id="source-code"&gt;Source Code&lt;/h2&gt;
&lt;p&gt;The library is available at &lt;a href="https://github.com/fulvius31/ip-neigh-sdk30"&gt;github.com/fulvius31/ip-neigh-sdk30&lt;/a&gt;, licensed under CC0-1.0.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s no longer effective on modern Android due to the SELinux changes, but it documents a real technique for working with RTNetlink from Android native code and the cat-and-mouse game between app developers and platform restrictions. It was a fun workaround while it lasted.&lt;/p&gt;</content><category term="android"/><category term="netlink"/><category term="jni"/><category term="networking"/><category term="arp"/><category term="open-source"/></entry><entry><title>Blocking WiFi De-Auth Attacks in the Kernel with eBPF and XDP</title><link href="https://contact.alessandrosangiorgi.net/posts/ebpf-xdp-wifi-deauth-defense/" rel="alternate" type="text/html"/><id>https://contact.alessandrosangiorgi.net/posts/ebpf-xdp-wifi-deauth-defense/</id><published>2026-04-07T00:00:00Z</published><updated>2026-04-07T00:00:00Z</updated><summary>I patched the Linux mac80211 kernel module to support XDP on wireless interfaces and built an eBPF program that detects and drops 802.11 de-authentication floods — reducing detection time by 60% vs libpcap and improving throughput stability over 802.11w. Published at IEEE NetSoft 2025.</summary><content type="html">&lt;p&gt;802.11 de-authentication attacks are one of the oldest and cheapest WiFi denial-of-service techniques. An attacker sends forged de-auth management frames to disconnect clients from an access point — &lt;code&gt;aireplay-ng&lt;/code&gt; sends 128 per attack command (64 directed at the AP, 64 at the client). Despite being a known problem for over two decades, the main defense is still 802.11w (Protected Management Frames), which requires both AP and client support and still degrades under high-rate floods.&lt;/p&gt;
&lt;p&gt;I took a different approach: detect and drop de-auth floods &lt;strong&gt;in the kernel&lt;/strong&gt; using eBPF and XDP — at the mac80211 wireless driver level, before the frames ever reach the network stack. This is the first application of eBPF to the 802.11 protocol. The paper was published at &lt;a href="https://ieeexplore.ieee.org/document/11080545"&gt;IEEE NetSoft 2025&lt;/a&gt; as first author.&lt;/p&gt;
&lt;h2 id="why-not-just-use-libpcap-or-80211w"&gt;Why Not Just Use libpcap or 802.11w?&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;libpcap&lt;/strong&gt; is the traditional approach — capture packets in user space and analyze them. But it introduces latency from kernel-to-user-space copying, requires monitor mode (not all drivers support it), and is purely passive — it can detect but can&amp;rsquo;t drop packets. Under attack, the copy overhead spikes exactly when you need speed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;802.11w&lt;/strong&gt; encrypts management frames to prevent spoofing, but it struggles with high-rate de-auth floods. Our experiments show that under sustained attack, 802.11w throughput drops below 5 Mbps with erratic spikes, while our eBPF/XDP solution maintains a consistent 10–12 Mbps. Additionally, many legacy devices and drivers lack PMF support — some APs are forced to disable 802.11w entirely to maintain compatibility.&lt;/p&gt;
&lt;p&gt;XDP (eXpress Data Path) runs eBPF programs at the &lt;strong&gt;earliest possible point&lt;/strong&gt; in the Linux network stack — at the driver level, before a &lt;code&gt;sk_buff&lt;/code&gt; is even allocated. But XDP doesn&amp;rsquo;t work on WiFi interfaces. The Linux mac80211 subsystem has no &lt;code&gt;ndo_bpf&lt;/code&gt; callback. So I patched the kernel.&lt;/p&gt;
&lt;h2 id="patching-mac80211-for-xdp-support"&gt;Patching mac80211 for XDP Support&lt;/h2&gt;
&lt;p&gt;I modified two files in the &lt;code&gt;mac80211&lt;/code&gt; kernel module — the framework that implements IEEE 802.11 for wireless LAN drivers. Implementing XDP at the mac80211 level (rather than in individual drivers) ensures compatibility across all wireless drivers that use this module.&lt;/p&gt;
&lt;h3 id="ifacec-the-ndo_bpf-callback"&gt;&lt;code&gt;iface.c&lt;/code&gt;: The &lt;code&gt;ndo_bpf&lt;/code&gt; Callback&lt;/h3&gt;
&lt;p&gt;This patch adds the infrastructure to attach and detach BPF programs on a wireless interface. It registers a &lt;code&gt;.ndo_bpf&lt;/code&gt; handler in the &lt;code&gt;ieee80211_dataif_ops&lt;/code&gt; structure — the net device operations table for mac80211 data interfaces:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;static&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;int&lt;/span&gt; &lt;span style="color:#d2a8ff;font-weight:bold"&gt;generic_xdp_setup&lt;/span&gt;(&lt;span style="color:#ff7b72"&gt;struct&lt;/span&gt; net_device &lt;span style="color:#ff7b72;font-weight:bold"&gt;*&lt;/span&gt;dev, &lt;span style="color:#ff7b72"&gt;struct&lt;/span&gt; netdev_bpf &lt;span style="color:#ff7b72;font-weight:bold"&gt;*&lt;/span&gt;bpf)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;struct&lt;/span&gt; bpf_prog &lt;span style="color:#ff7b72;font-weight:bold"&gt;*&lt;/span&gt;old_prog, &lt;span style="color:#ff7b72;font-weight:bold"&gt;*&lt;/span&gt;new_prog;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; new_prog &lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt; bpf&lt;span style="color:#ff7b72;font-weight:bold"&gt;-&amp;gt;&lt;/span&gt;prog;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; old_prog &lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt; &lt;span style="color:#d2a8ff;font-weight:bold"&gt;xchg&lt;/span&gt;(&lt;span style="color:#ff7b72;font-weight:bold"&gt;&amp;amp;&lt;/span&gt;dev&lt;span style="color:#ff7b72;font-weight:bold"&gt;-&amp;gt;&lt;/span&gt;xdp_prog, new_prog);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;if&lt;/span&gt; (old_prog)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#d2a8ff;font-weight:bold"&gt;bpf_prog_put&lt;/span&gt;(old_prog);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;return&lt;/span&gt; &lt;span style="color:#a5d6ff"&gt;0&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;static&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;int&lt;/span&gt; &lt;span style="color:#d2a8ff;font-weight:bold"&gt;ieee80211_xdp&lt;/span&gt;(&lt;span style="color:#ff7b72"&gt;struct&lt;/span&gt; net_device &lt;span style="color:#ff7b72;font-weight:bold"&gt;*&lt;/span&gt;dev, &lt;span style="color:#ff7b72"&gt;struct&lt;/span&gt; netdev_bpf &lt;span style="color:#ff7b72;font-weight:bold"&gt;*&lt;/span&gt;xdp)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;switch&lt;/span&gt; (xdp&lt;span style="color:#ff7b72;font-weight:bold"&gt;-&amp;gt;&lt;/span&gt;command) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;case&lt;/span&gt; &lt;span style="color:#79c0ff;font-weight:bold"&gt;XDP_SETUP_PROG&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;return&lt;/span&gt; &lt;span style="color:#d2a8ff;font-weight:bold"&gt;generic_xdp_setup&lt;/span&gt;(dev, xdp);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;default&lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;return&lt;/span&gt; &lt;span style="color:#ff7b72;font-weight:bold"&gt;-&lt;/span&gt;EINVAL;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;xchg()&lt;/code&gt; atomically swaps the old program pointer with the new one. &lt;code&gt;ieee80211_xdp&lt;/code&gt; is then registered in the device ops:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;static&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;const&lt;/span&gt; &lt;span style="color:#ff7b72"&gt;struct&lt;/span&gt; net_device_ops ieee80211_dataif_ops &lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#8b949e;font-style:italic"&gt;// ... existing ops ...
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .ndo_bpf &lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt; ieee80211_xdp,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;};
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;ndo_bpf&lt;/code&gt; routine is invoked from user space via Netlink sockets using &lt;code&gt;iproute2&lt;/code&gt; or &lt;code&gt;bpftool&lt;/code&gt;. After this patch, &lt;code&gt;ip link set dev wlan0 xdp obj program.o&lt;/code&gt; works on wireless interfaces.&lt;/p&gt;
&lt;h3 id="rxc-xdp-execution-in-the-receive-path"&gt;&lt;code&gt;rx.c&lt;/code&gt;: XDP Execution in the Receive Path&lt;/h3&gt;
&lt;p&gt;This patch hooks into &lt;code&gt;ieee80211_rx_napi()&lt;/code&gt; — the main receive function that processes incoming packets using NAPI (New API) for efficient interrupt handling. It checks for an attached XDP program and runs it on every incoming frame:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;xdp_prog &lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt; &lt;span style="color:#d2a8ff;font-weight:bold"&gt;ieee80211_get_xdp_prog&lt;/span&gt;(sdata&lt;span style="color:#ff7b72;font-weight:bold"&gt;-&amp;gt;&lt;/span&gt;dev);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;if&lt;/span&gt; (xdp_prog) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;struct&lt;/span&gt; xdp_buff xdp;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; xdp.data &lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt; skb&lt;span style="color:#ff7b72;font-weight:bold"&gt;-&amp;gt;&lt;/span&gt;data;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; xdp.data_end &lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt; skb&lt;span style="color:#ff7b72;font-weight:bold"&gt;-&amp;gt;&lt;/span&gt;data &lt;span style="color:#ff7b72;font-weight:bold"&gt;+&lt;/span&gt; skb&lt;span style="color:#ff7b72;font-weight:bold"&gt;-&amp;gt;&lt;/span&gt;len;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; act &lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt; &lt;span style="color:#d2a8ff;font-weight:bold"&gt;bpf_prog_run_xdp&lt;/span&gt;(xdp_prog, &lt;span style="color:#ff7b72;font-weight:bold"&gt;&amp;amp;&lt;/span&gt;xdp);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;switch&lt;/span&gt; (act) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;case&lt;/span&gt; &lt;span style="color:#79c0ff;font-weight:bold"&gt;XDP_PASS&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;break&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;case&lt;/span&gt; &lt;span style="color:#79c0ff;font-weight:bold"&gt;XDP_DROP&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#d2a8ff;font-weight:bold"&gt;kfree_skb&lt;/span&gt;(skb);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;return&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The packet data is accessed through the &lt;code&gt;sk_buff&lt;/code&gt; structure, which contains the complete 802.11 frame including headers and payload. Two actions are handled: &lt;code&gt;XDP_DROP&lt;/code&gt; flushes the &lt;code&gt;skb&lt;/code&gt; immediately, &lt;code&gt;XDP_PASS&lt;/code&gt; allows normal processing to continue.&lt;/p&gt;
&lt;h2 id="the-ebpf-de-auth-detector"&gt;The eBPF De-Auth Detector&lt;/h2&gt;
&lt;p&gt;With XDP working on wireless interfaces, the detection program parses 802.11 management frames and identifies de-authentication packets by their frame control field:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;struct&lt;/span&gt; ieee80211_mgmt {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;uint16_t&lt;/span&gt; frame_control;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;uint16_t&lt;/span&gt; duration_id;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;uint8_t&lt;/span&gt; da[&lt;span style="color:#a5d6ff"&gt;6&lt;/span&gt;]; &lt;span style="color:#8b949e;font-style:italic"&gt;// Destination address
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;uint8_t&lt;/span&gt; sa[&lt;span style="color:#a5d6ff"&gt;6&lt;/span&gt;]; &lt;span style="color:#8b949e;font-style:italic"&gt;// Source address
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;uint8_t&lt;/span&gt; bssid[&lt;span style="color:#a5d6ff"&gt;6&lt;/span&gt;]; &lt;span style="color:#8b949e;font-style:italic"&gt;// AP MAC
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;uint16_t&lt;/span&gt; seq_ctrl;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;uint16_t&lt;/span&gt; reason_code;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;};
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The Frame Control field encodes the type (bits 2–3) and subtype (bits 4–7). Management frames have type &lt;code&gt;0&lt;/code&gt;, de-authentication has subtype &lt;code&gt;0xC&lt;/code&gt; (12):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;uint8_t&lt;/span&gt; type &lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt; (mgmt&lt;span style="color:#ff7b72;font-weight:bold"&gt;-&amp;gt;&lt;/span&gt;frame_control &lt;span style="color:#ff7b72;font-weight:bold"&gt;&amp;amp;&lt;/span&gt; &lt;span style="color:#a5d6ff"&gt;0x000C&lt;/span&gt;) &lt;span style="color:#ff7b72;font-weight:bold"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#a5d6ff"&gt;2&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;uint8_t&lt;/span&gt; subtype &lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt; (mgmt&lt;span style="color:#ff7b72;font-weight:bold"&gt;-&amp;gt;&lt;/span&gt;frame_control &lt;span style="color:#ff7b72;font-weight:bold"&gt;&amp;amp;&lt;/span&gt; &lt;span style="color:#a5d6ff"&gt;0x00F0&lt;/span&gt;) &lt;span style="color:#ff7b72;font-weight:bold"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#a5d6ff"&gt;4&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;if&lt;/span&gt; (subtype &lt;span style="color:#ff7b72;font-weight:bold"&gt;==&lt;/span&gt; &lt;span style="color:#a5d6ff"&gt;0xC&lt;/span&gt; &lt;span style="color:#ff7b72;font-weight:bold"&gt;&amp;amp;&amp;amp;&lt;/span&gt; type &lt;span style="color:#ff7b72;font-weight:bold"&gt;==&lt;/span&gt; &lt;span style="color:#a5d6ff"&gt;0&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#8b949e;font-style:italic"&gt;// De-authentication frame detected
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;State is tracked in a per-CPU array map (lock-free counters):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;struct&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#d2a8ff;font-weight:bold"&gt;__uint&lt;/span&gt;(type, BPF_MAP_TYPE_PERCPU_ARRAY);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#d2a8ff;font-weight:bold"&gt;__uint&lt;/span&gt;(key_size, &lt;span style="color:#ff7b72"&gt;sizeof&lt;/span&gt;(&lt;span style="color:#ff7b72"&gt;uint32_t&lt;/span&gt;));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#d2a8ff;font-weight:bold"&gt;__uint&lt;/span&gt;(value_size, &lt;span style="color:#ff7b72"&gt;sizeof&lt;/span&gt;(&lt;span style="color:#ff7b72"&gt;uint64_t&lt;/span&gt;));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#d2a8ff;font-weight:bold"&gt;__uint&lt;/span&gt;(max_entries, &lt;span style="color:#a5d6ff"&gt;2&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;} packet_count &lt;span style="color:#d2a8ff;font-weight:bold"&gt;SEC&lt;/span&gt;(&lt;span style="color:#a5d6ff"&gt;&amp;#34;.maps&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;When de-auth frames exceed a threshold, the program logs the attack metadata (BSSID, source/destination MACs, reason code, nanosecond timestamp) and returns &lt;code&gt;XDP_DROP&lt;/code&gt;. The frame is gone before the network stack ever sees it.&lt;/p&gt;
&lt;p&gt;Compile and attach:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;clang -O2 -target bpf -c deauth_ebpf.c
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ip link set dev wlan0 xdp obj deauth_ebpf.o
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Once deployed, the protected device can only be disconnected manually — all de-authentication frames are filtered out.&lt;/p&gt;
&lt;h2 id="evaluation"&gt;Evaluation&lt;/h2&gt;
&lt;h3 id="ebpf-vs-libpcap-detection-time"&gt;eBPF vs. libpcap: Detection Time&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Setup&lt;/strong&gt;: Two Debian Bookworm VMs (kernel 6.1.52, 4 GB RAM, 12 logical CPUs) with external ATHEROS UB93 [0108] USB wireless cards using the &lt;code&gt;ath9k_htc&lt;/code&gt; driver. One VM as the victim, one as the attacker running a modified &lt;code&gt;aireplay-ng&lt;/code&gt; with nanosecond timestamps. Both eBPF and libpcap detectors ran on the victim, logging de-auth events with nanosecond precision.&lt;/p&gt;
&lt;p&gt;eBPF reduces detection time by &lt;strong&gt;60%&lt;/strong&gt; on average compared to libpcap, with peak improvements of up to &lt;strong&gt;7x&lt;/strong&gt;. The CDF of detection times shows XDP/eBPF reaching 100% distribution at significantly lower thresholds — it is consistently faster, not just on average. Both solutions maintain a relatively constant memory footprint, with libpcap exhibiting slightly higher average usage.&lt;/p&gt;
&lt;h3 id="ebpfxdp-vs-80211w-throughput-under-attack"&gt;eBPF/XDP vs. 802.11w: Throughput Under Attack&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Setup&lt;/strong&gt;: Two Raspberry Pi 4s running OpenWrt — one as AP, one as client — with &lt;code&gt;ath9k-htc&lt;/code&gt; USB wireless cards (because the onboard Broadcom &lt;code&gt;brcmfmac&lt;/code&gt; doesn&amp;rsquo;t support WPA3/PMF configurations required for 802.11w testing). The client ran &lt;code&gt;iperf3&lt;/code&gt; against an Android 15 device as the server. An attacker on a separate laptop with an Intel wireless card in monitor mode launched sustained de-auth floods with &lt;code&gt;aireplay-ng&lt;/code&gt; over a 5-minute window.&lt;/p&gt;
&lt;p&gt;Throughput comparison under sustained attack:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;eBPF/XDP&lt;/th&gt;
&lt;th&gt;802.11w&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Stabilization&lt;/td&gt;
&lt;td&gt;Fast, consistent&lt;/td&gt;
&lt;td&gt;Slow, erratic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sustained throughput&lt;/td&gt;
&lt;td&gt;10–12 Mbps&lt;/td&gt;
&lt;td&gt;Frequent drops below 5 Mbps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Variance&lt;/td&gt;
&lt;td&gt;Minimal fluctuations&lt;/td&gt;
&lt;td&gt;Frequent dips and spikes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Both solutions initially reach ~20 Mbps, but eBPF/XDP stabilizes faster and maintains a consistent throughput with fewer deviations. 802.11w exhibits erratic performance with frequent dips, making it less reliable for latency-sensitive applications like security cameras or streaming sessions — exactly the IoT scenarios where de-auth attacks are most damaging.&lt;/p&gt;
&lt;h2 id="the-openwrt-integration"&gt;The OpenWrt Integration&lt;/h2&gt;
&lt;p&gt;For real-world deployment on embedded hardware, the mac80211 patches are integrated into a custom &lt;a href="https://github.com/fulvius31/openwrt_mac80211_xdp_ebpf"&gt;OpenWrt build&lt;/a&gt; targeting Raspberry Pi. This includes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The two mac80211 kernel patches applied to the OpenWrt build system&lt;/li&gt;
&lt;li&gt;BPF toolchain configuration for cross-compilation (clang/LLVM pipeline)&lt;/li&gt;
&lt;li&gt;Build infrastructure for compiling eBPF programs against OpenWrt kernel headers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Flash a Raspberry Pi with the custom image, attach an &lt;code&gt;ath9k_htc&lt;/code&gt; USB WiFi adapter, load the eBPF program, and you have a wireless intrusion prevention sensor on a $35 board.&lt;/p&gt;
&lt;h2 id="limitations-and-future-work"&gt;Limitations and Future Work&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No source verification&lt;/strong&gt; — the current solution does not verify the legitimacy of the source of de-authentication packets. A future MAC address whitelist mechanism would allow selective protection for critical devices (IoT cameras, medical equipment) while preserving standard de-auth behavior for other clients.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Threshold tuning&lt;/strong&gt; — the current fixed threshold works for research. Production deployments would need adaptive thresholds based on network characteristics.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Single-run throughput data&lt;/strong&gt; — the throughput comparison results come from single runs. We release the code for reproducibility and further testing.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="source-code"&gt;Source Code&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;eBPF program&lt;/strong&gt;: &lt;a href="https://github.com/fulvius31/deauth_ebpf"&gt;github.com/fulvius31/deauth_ebpf&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OpenWrt + mac80211 patches&lt;/strong&gt;: &lt;a href="https://github.com/fulvius31/openwrt_mac80211_xdp_ebpf"&gt;github.com/fulvius31/openwrt_mac80211_xdp_ebpf&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;A. Sangiorgi, A. Pinto, R. Tourani, F. Esposito, &amp;ldquo;Mitigating De-authentication DoS Attacks in 802.11 via eBPF and XDP,&amp;rdquo; IEEE NetSoft 2025. &lt;strong&gt;First author.&lt;/strong&gt; Department of Computer Science, Saint Louis University.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;This work was supported by NSF awards OAC-2201536 and CPS 2133407.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;DOI: &lt;a href="https://ieeexplore.ieee.org/document/11080545"&gt;10.1109/NetSoft61042.2025.11080545&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;</content><category term="ebpf"/><category term="xdp"/><category term="security"/><category term="linux"/><category term="kernel"/><category term="wifi"/><category term="research"/><category term="ieee"/></entry><entry><title>Intercepting Android's ManagedProvisioning: A PendingIntent Vulnerability in AOSP</title><link href="https://contact.alessandrosangiorgi.net/posts/managedprovisioning-pendingintent-vulnerability/" rel="alternate" type="text/html"/><id>https://contact.alessandrosangiorgi.net/posts/managedprovisioning-pendingintent-vulnerability/</id><published>2026-04-06T00:00:00Z</published><updated>2026-04-06T00:00:00Z</updated><summary>I found a vulnerability in Android's ManagedProvisioning that lets any unprivileged app intercept privileged provisioning callbacks. Google classified it as low severity.</summary><content type="html">&lt;p&gt;I found and reported a vulnerability in Android&amp;rsquo;s &lt;code&gt;ManagedProvisioning&lt;/code&gt; component — the system app responsible for setting up enterprise-managed (work) profiles. The bug allows any unprivileged third-party app to intercept a privileged provisioning callback, leaking install timing, session metadata, and in some cases package details — all without any special permissions.&lt;/p&gt;
&lt;p&gt;Google acknowledged the report, logged it for potential remediation, and classified it as low severity. Here&amp;rsquo;s the full breakdown.&lt;/p&gt;
&lt;h2 id="the-bug"&gt;The Bug&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;InstallPackageTask&lt;/code&gt; in &lt;code&gt;packages/apps/ManagedProvisioning&lt;/code&gt; creates the &lt;code&gt;PackageInstaller.Session.commit()&lt;/code&gt; status receiver using a &lt;strong&gt;mutable implicit PendingIntent&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The callback intent uses a predictable action string:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;com.android.managedprovisioning.task.InstallPackageTask.DONE.&amp;lt;sessionId&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This is passed to &lt;code&gt;PendingIntent.getBroadcast(...)&lt;/code&gt; without &lt;code&gt;setPackage()&lt;/code&gt; or an explicit component. On Android U+ builds, the code also adds &lt;code&gt;FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT&lt;/code&gt;, which &lt;strong&gt;explicitly opts out&lt;/strong&gt; of Android&amp;rsquo;s own protection against unsafe mutable implicit PendingIntents.&lt;/p&gt;
&lt;p&gt;Because the action includes the session ID, any app that observes the session ID can dynamically register a matching &lt;code&gt;BroadcastReceiver&lt;/code&gt; and receive the install-status callback.&lt;/p&gt;
&lt;h2 id="why-this-matters"&gt;Why This Matters&lt;/h2&gt;
&lt;p&gt;The callback originates from a &lt;strong&gt;privileged provisioning component&lt;/strong&gt; — the system flow that sets up enterprise work profiles, installs MDM agents, and configures managed devices. Android&amp;rsquo;s own documentation treats mutable implicit PendingIntents as unsafe and recommends making them explicit or package-scoped.&lt;/p&gt;
&lt;p&gt;The practical impact:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Disclosure of provisioning/install timing&lt;/strong&gt; — an attacker can observe exactly when enterprise provisioning packages are being installed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Session-correlated callback metadata&lt;/strong&gt; — the intercepted broadcast carries extras tied to the install session&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Package detail leakage&lt;/strong&gt; — for some apps (like Google Digital Wellbeing), the intercepted callback reveals package details without any permissions&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Enterprise fingerprinting&lt;/strong&gt; — a malicious app can detect and fingerprint when an MDM solution is being deployed on the device&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="proof-of-concept"&gt;Proof of Concept&lt;/h2&gt;
&lt;p&gt;The exploit is straightforward. A third-party app:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Calls &lt;code&gt;PackageInstaller.registerSessionCallback(...)&lt;/code&gt; to observe newly created install sessions&lt;/li&gt;
&lt;li&gt;Extracts the session ID in real time from &lt;code&gt;onCreated(int sessionId)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Dynamically registers a &lt;code&gt;BroadcastReceiver&lt;/code&gt; for the exact action string&lt;/li&gt;
&lt;li&gt;Receives the callback broadcast when &lt;code&gt;session.commit(...)&lt;/code&gt; completes&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;PackageInstaller&lt;span style="color:#6e7681"&gt; &lt;/span&gt;installer&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;context.getPackageManager().getPackageInstaller();&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;installer.registerSessionCallback(&lt;span style="color:#ff7b72"&gt;new&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;PackageInstaller.SessionCallback()&lt;span style="color:#6e7681"&gt; &lt;/span&gt;{&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#d2a8ff;font-weight:bold"&gt;@Override&lt;/span&gt;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;public&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;void&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#d2a8ff;font-weight:bold"&gt;onCreated&lt;/span&gt;(&lt;span style="color:#ff7b72"&gt;int&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;sessionId)&lt;span style="color:#6e7681"&gt; &lt;/span&gt;{&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;String&lt;span style="color:#6e7681"&gt; &lt;/span&gt;action&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;&amp;#34;com.android.managedprovisioning.task.InstallPackageTask.DONE.&amp;#34;&lt;/span&gt;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;+&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;sessionId;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;BroadcastReceiver&lt;span style="color:#6e7681"&gt; &lt;/span&gt;receiver&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;new&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;ManagedProvisioningHijackReceiver();&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;IntentFilter&lt;span style="color:#6e7681"&gt; &lt;/span&gt;filter&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;new&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;IntentFilter(action);&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;context.registerReceiver(&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;receiver,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;filter,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;Context.RECEIVER_EXPORTED);&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;Log.w(&lt;span style="color:#a5d6ff"&gt;&amp;#34;PoC&amp;#34;&lt;/span&gt;,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;&amp;#34;Registered receiver for action: &amp;#34;&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;+&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;action);&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;}&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#d2a8ff;font-weight:bold"&gt;@Override&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;public&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;void&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#d2a8ff;font-weight:bold"&gt;onBadgingChanged&lt;/span&gt;(&lt;span style="color:#ff7b72"&gt;int&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;sessionId)&lt;span style="color:#6e7681"&gt; &lt;/span&gt;{}&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#d2a8ff;font-weight:bold"&gt;@Override&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;public&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;void&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#d2a8ff;font-weight:bold"&gt;onActiveChanged&lt;/span&gt;(&lt;span style="color:#ff7b72"&gt;int&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;sessionId,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;boolean&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;active)&lt;span style="color:#6e7681"&gt; &lt;/span&gt;{}&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#d2a8ff;font-weight:bold"&gt;@Override&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;public&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;void&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#d2a8ff;font-weight:bold"&gt;onProgressChanged&lt;/span&gt;(&lt;span style="color:#ff7b72"&gt;int&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;sessionId,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;float&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;progress)&lt;span style="color:#6e7681"&gt; &lt;/span&gt;{}&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#d2a8ff;font-weight:bold"&gt;@Override&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;public&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;void&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#d2a8ff;font-weight:bold"&gt;onFinished&lt;/span&gt;(&lt;span style="color:#ff7b72"&gt;int&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;sessionId,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;boolean&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;success)&lt;span style="color:#6e7681"&gt; &lt;/span&gt;{}&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The receiver logs everything it intercepts:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#d2a8ff;font-weight:bold"&gt;@Override&lt;/span&gt;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;public&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;void&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#d2a8ff;font-weight:bold"&gt;onReceive&lt;/span&gt;(Context&lt;span style="color:#6e7681"&gt; &lt;/span&gt;context,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;Intent&lt;span style="color:#6e7681"&gt; &lt;/span&gt;intent)&lt;span style="color:#6e7681"&gt; &lt;/span&gt;{&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;Log.w(TAG,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;&amp;#34;INTERCEPTED PROVISIONING CALLBACK BROADCAST&amp;#34;&lt;/span&gt;);&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;Log.w(TAG,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;&amp;#34;Action: &amp;#34;&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;+&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;intent.getAction());&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;Bundle&lt;span style="color:#6e7681"&gt; &lt;/span&gt;extras&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;intent.getExtras();&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;if&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;(extras&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;!=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#79c0ff"&gt;null&lt;/span&gt;)&lt;span style="color:#6e7681"&gt; &lt;/span&gt;{&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;for&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;(String&lt;span style="color:#6e7681"&gt; &lt;/span&gt;key&lt;span style="color:#6e7681"&gt; &lt;/span&gt;:&lt;span style="color:#6e7681"&gt; &lt;/span&gt;extras.keySet())&lt;span style="color:#6e7681"&gt; &lt;/span&gt;{&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;Log.w(TAG,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;key&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;+&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;&amp;#34; = &amp;#34;&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;+&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;extras.get(key));&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;}&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;}&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;No special permissions required.&lt;/p&gt;
&lt;h2 id="video-demo"&gt;Video Demo&lt;/h2&gt;
&lt;video controls playsinline style="width:100%;max-width:720px;border-radius:6px;border:1px solid #2a2a2a;margin:1rem 0;"&gt;
&lt;source src="/media/poc_managedprovisioning.mp4" type="video/mp4"&gt;
Your browser does not support the video tag.
&lt;/video&gt;
&lt;p&gt;The video shows the PoC app intercepting the provisioning callback in real time on a Pixel device. For apps like Google Digital Wellbeing, the intercepted broadcast reveals package details without any permissions on the attacker&amp;rsquo;s side.&lt;/p&gt;
&lt;h2 id="root-cause"&gt;Root Cause&lt;/h2&gt;
&lt;p&gt;The issue boils down to three compounding decisions in the code:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Mutable PendingIntent&lt;/strong&gt; — &lt;code&gt;FLAG_MUTABLE&lt;/code&gt; allows the intent to be modified&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Implicit broadcast&lt;/strong&gt; — no &lt;code&gt;setPackage()&lt;/code&gt;, no explicit component, so any app can register for the action&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Predictable action string&lt;/strong&gt; — the action includes the session ID, which is observable via &lt;code&gt;PackageInstaller.SessionCallback&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;On U+ builds, &lt;code&gt;FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT&lt;/code&gt; is added, which explicitly bypasses the platform&amp;rsquo;s own safety check. The fix is straightforward: make the callback intent explicit (set a component) or at minimum package-scoped (&lt;code&gt;setPackage()&lt;/code&gt;).&lt;/p&gt;
&lt;h2 id="what-i-did-not-demonstrate"&gt;What I Did NOT Demonstrate&lt;/h2&gt;
&lt;p&gt;To be precise about the scope:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No arbitrary code execution&lt;/li&gt;
&lt;li&gt;No package replacement or privilege escalation&lt;/li&gt;
&lt;li&gt;No confirmed denial of service against provisioning (the success path relies on &lt;code&gt;SessionCallback.onFinished()&lt;/code&gt; + &lt;code&gt;ACTION_PACKAGE_ADDED&lt;/code&gt;, not this callback alone)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The demonstrated impact is &lt;strong&gt;callback interception and information disclosure&lt;/strong&gt; from a privileged provisioning flow.&lt;/p&gt;
&lt;h2 id="googles-response"&gt;Google&amp;rsquo;s Response&lt;/h2&gt;
&lt;p&gt;I reported this through the &lt;a href="https://bughunters.google.com"&gt;Android &amp;amp; Google Device Vulnerability Reward Program&lt;/a&gt;. The report was tracked as &lt;a href="https://issuetracker.google.com/issues/493654042"&gt;Issue 493654042&lt;/a&gt; on Google&amp;rsquo;s Issue Tracker.&lt;/p&gt;
&lt;p&gt;After investigation, the Android Security Team classified it as &lt;strong&gt;low severity&lt;/strong&gt; and marked it as &lt;strong&gt;Infeasible&lt;/strong&gt; for immediate action:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Thank you for taking the time to submit this report to the Android &amp;amp; Google Device Vulnerability Reward Program!&lt;/p&gt;
&lt;p&gt;We have investigated this issue and determined that this is low severity.&lt;/p&gt;
&lt;p&gt;We have logged this issue for potential remediation in a future version. At this time, this report is considered closed and will no longer be monitored.&lt;/p&gt;
&lt;p&gt;— Android Security Team&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The issue was logged for potential remediation in a future Android version but is not being actively tracked.&lt;/p&gt;
&lt;h2 id="my-take"&gt;My Take&lt;/h2&gt;
&lt;p&gt;I understand the &amp;ldquo;low severity&amp;rdquo; classification — there&amp;rsquo;s no RCE, no privilege escalation, and the provisioning flow doesn&amp;rsquo;t solely depend on this callback. But I think the response undersells the issue:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The code &lt;strong&gt;explicitly bypasses&lt;/strong&gt; a platform safety mechanism (&lt;code&gt;FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT&lt;/code&gt;) that exists specifically to prevent this class of bug&lt;/li&gt;
&lt;li&gt;Enterprise provisioning metadata shouldn&amp;rsquo;t be observable by unprivileged apps&lt;/li&gt;
&lt;li&gt;The fix is trivial: one line — &lt;code&gt;intent.setPackage(context.getPackageName())&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Sometimes the value of security research isn&amp;rsquo;t in the severity score but in showing that even Google&amp;rsquo;s own platform code doesn&amp;rsquo;t always follow Google&amp;rsquo;s own security guidance.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Tested on: &lt;code&gt;google/husky_beta/husky:CinnamonBun/CP21.260206.011/14911669:user/release-keys&lt;/code&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Source: &lt;a href="https://android.googlesource.com/platform/packages/apps/ManagedProvisioning"&gt;packages/apps/ManagedProvisioning&lt;/a&gt; — &lt;code&gt;InstallPackageTask.java&lt;/code&gt;&lt;/em&gt;&lt;/p&gt;</content><category term="android"/><category term="security"/><category term="vrp"/><category term="aosp"/><category term="pendingintent"/></entry></feed>