Skip to content

Commit fa9107b

Browse files
nqmgamingclaude
andcommitted
fix: drop blocking /proc and root-shell reads from the DNS hot path
Measured on a rooted Android 13 device (SM-N986N) with Root Proxy mode running, logcat showed findUidFromProcNet being walked end to end per DNS query — 892, 408 and 168 "Cannot read /proc/net/udp: EACCES" lines in three consecutive minutes, i.e. ~7 queries/sec sustained, each one: - parsing /proc/net/udp and /proc/net/udp6 in full, twice, only to fail. The old comment assumed SELinux merely narrowed these reads to our own sockets; on this device they are flat EACCES, so the result is a guaranteed miss. - then running Shell.cmd("cat /proc/net/udp /proc/net/udp6").exec(), a blocking libsu call. libsu serialises every command on one shared shell, so DNS resolution queued behind these — and behind every iptables call. That is issue #130's failure mode, which the snapshotter was meant to have fixed. It didn't: a 100ms snapshot interval cannot catch DNS sockets that live a few milliseconds, so queries kept falling through to the shell. On API 29+ the hot path now consults only the background snapshot, which is what the existing comment already promised ("read this map without blocking — no inline shell"). API < 29 keeps the direct /proc reads, where they work without root and without SELinux denial. findUidFromProcNetRoot is deleted rather than left unreachable. What this costs is attribution, not filtering: a query whose socket the snapshot missed is logged without an app name instead of with one. Blocking decisions never consulted this UID. Not yet measured after the change — reinstalling stopped the service, so the post-fix logcat is empty for lack of traffic rather than for lack of these calls. The pre-fix numbers above are real; the after is pending a run with protection enabled. SNAPSHOT_INTERVAL_MS stays at 100ms, so root mode still issues ~10 shell commands/sec from the snapshotter itself. That is a separate cost, left deliberately untouched here. Refs #130, #226 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 6432a4d commit fa9107b

1 file changed

Lines changed: 22 additions & 71 deletions

File tree

app/src/main/java/app/pwhs/blockads/utils/AppNameResolver.kt

Lines changed: 22 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,6 @@ class AppNameResolver(private val context: Context) {
4040
// DNS query lookups read this map without blocking — no inline shell.
4141
@Volatile private var procNetSnapshot: Map<Int, Int> = emptyMap()
4242

43-
// Read from the DNS hot path (many Go threads) to decide whether the
44-
// /proc and root-shell lookups are worth attempting, so it needs to be
45-
// visible across threads — see [rootModeActive].
4643
@Volatile private var snapshotJob: Job? = null
4744

4845
/**
@@ -145,52 +142,41 @@ class AppNameResolver(private val context: Context) {
145142
}
146143

147144
/**
148-
* Look up /proc/net/udp and /proc/net/udp6 to find the UID owning the given port.
149-
* Used on API < 29 and on Android 10+ in Root Proxy mode.
145+
* Look up the UID owning the given port. Runs inline on every DNS query,
146+
* so nothing blocking is allowed here.
150147
*
151-
* Every step here runs inline on a DNS query, so each one must justify
152-
* itself per query. Outside Root Proxy mode none of them can: on Android
153-
* 10+ SELinux limits the unprivileged read to our own sockets (so it can
154-
* never see the querying app's), and the root-shell read needs a root
155-
* shell we don't have. Both are therefore gated — see [rootModeActive].
156-
* Without that gate a VPN-mode query walks the whole chain and ends on a
157-
* blocking libsu command; libsu serialises everything on one shared
158-
* shell, so every DNS query queues behind the previous one. That is
159-
* issue #130's failure mode, which the snapshotter fixed for Root
160-
* Proxy mode only.
148+
* On API 29+ the only source consulted is [procNetSnapshot], which
149+
* [startSnapshotter] refreshes in the background. The two paths that used
150+
* to follow it are gone from the hot path, because measurement on a rooted
151+
* Android 13 device showed what they actually cost per query:
152+
*
153+
* - `/proc/net/udp{,6}` direct reads: not merely SELinux-narrowed as the
154+
* old comment assumed, but flat `EACCES`. Two full-file parses per
155+
* query to guarantee a miss (~7 queries/sec sustained in logcat).
156+
* - the libsu root read: a blocking shell command per query. libsu runs
157+
* everything on one shared shell, so DNS resolution serialised behind
158+
* it — and behind every iptables call too. That is issue #130's
159+
* failure mode, which the snapshotter was supposed to have fixed; it
160+
* didn't, because a 100ms snapshot interval misses DNS sockets that
161+
* live for a few milliseconds, so queries kept falling through.
162+
*
163+
* The cost of dropping them is attribution, not filtering: a query whose
164+
* socket the snapshot didn't catch is logged as unresolved instead of
165+
* named. Blocking decisions never depended on this. API < 29 keeps the
166+
* direct reads, where they work without root and without SELinux denial.
161167
*/
162168
private fun findUidFromProcNet(port: Int): Int {
163-
// Hot path (Root Proxy mode): read from the background snapshot.
164169
procNetSnapshot[port]?.let { return it }
165170

166-
val legacyAndroid = Build.VERSION.SDK_INT < Build.VERSION_CODES.Q
167-
if (!legacyAndroid && !rootModeActive) return -1
171+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) return -1
168172

169173
val hexPort = String.format("%04X", port)
170-
171-
// Unprivileged read fallback (works for own app's sockets and on older Android)
172174
findUidInProcFile("/proc/net/udp", hexPort)?.let { return it }
173175
findUidInProcFile("/proc/net/udp6", hexPort)?.let { return it }
174176

175-
if (!rootModeActive) return -1
176-
177-
// Root-privileged read: on Android 10+, SELinux restricts /proc/net/udp
178-
// to only show the app's own sockets. In Root Proxy mode the snapshotter
179-
// already covers this path; this branch is the slow-path safety net.
180-
findUidFromProcNetRoot(hexPort)?.let { return it }
181-
182177
return -1
183178
}
184179

185-
/**
186-
* True while Root Proxy mode is running — [startSnapshotter] is called
187-
* only by RootProxyService, so the snapshot job's presence is exactly the
188-
* signal we need. Deliberately not `Shell.isAppGrantedRoot()`: that call
189-
* can itself construct the shell this gate exists to avoid.
190-
*/
191-
private val rootModeActive: Boolean
192-
get() = snapshotJob != null
193-
194180
/**
195181
* Build a port → UID map from the full /proc/net/udp{,6} table via
196182
* the libsu persistent root shell. Sockets owned by our own app
@@ -222,41 +208,6 @@ class AppNameResolver(private val context: Context) {
222208
return map
223209
}
224210

225-
/**
226-
* Read /proc/net/udp and /proc/net/udp6 via root shell (libsu).
227-
* This bypasses SELinux restrictions on Android 10+ that prevent
228-
* normal apps from seeing other apps' socket entries.
229-
*/
230-
private fun findUidFromProcNetRoot(hexPort: String): Int? {
231-
try {
232-
val result = com.topjohnwu.superuser.Shell.cmd(
233-
"cat /proc/net/udp /proc/net/udp6 2>/dev/null"
234-
).exec()
235-
if (!result.isSuccess) return null
236-
237-
for (line in result.out) {
238-
try {
239-
val parts = line.trim().split("\\s+".toRegex())
240-
if (parts.size >= 8) {
241-
val localAddress = parts[1]
242-
val colonIndex = localAddress.lastIndexOf(':')
243-
if (colonIndex >= 0) {
244-
val localPort = localAddress.substring(colonIndex + 1)
245-
if (localPort.equals(hexPort, ignoreCase = true)) {
246-
return parts[7].toIntOrNull()
247-
}
248-
}
249-
}
250-
} catch (_: Exception) {
251-
// Skip malformed lines
252-
}
253-
}
254-
} catch (e: Exception) {
255-
Timber.d("Root /proc/net lookup failed: ${e.message}")
256-
}
257-
return null
258-
}
259-
260211
private fun findUidInProcFile(path: String, hexPort: String): Int? {
261212
try {
262213
File(path).bufferedReader(Charsets.UTF_8).use { reader ->

0 commit comments

Comments
 (0)