Skip to content

Commit f27c190

Browse files
committed
v4.3.1: Bug audit — fix 5 bugs found in comprehensive audit
- AutomationReceiver: move rate limit map to companion object (survived receiver re-creation per broadcast delivery — rate limiting was no-op) - GeoIpLookup: use compareAndSet for atomic window reset (race condition on concurrent lookups could bypass rate limit) - SourceHealthWorker: create ALERT_CHANNEL_ID before posting notification (channel may not exist if VPN service hasn't started yet) - DnsVpnService: add @volatile to pauseResumeJob (cross-thread visibility) - HomeViewModel: use synchronizedList for baselineRates + @volatile baselineQpm
1 parent b081008 commit f27c190

7 files changed

Lines changed: 27 additions & 14 deletions

File tree

CLAUDE.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# HostShield
22

33
## Overview
4-
Modern, AMOLED-dark hosts-based ad blocker app for Android. Inspired by AdAway. v4.3.0.
4+
Modern, AMOLED-dark hosts-based ad blocker app for Android. Inspired by AdAway. v4.3.1.
55

66
## Tech Stack
77
- Kotlin, Jetpack Compose, Material 3
@@ -70,6 +70,7 @@ cd app
7070
- Secrets configured: `KEYSTORE_BASE64`, `KEY_ALIAS`, `KEY_PASSWORD`, `STORE_PASSWORD`
7171

7272
## Version History
73+
- v4.3.1: Bug audit — fix AutomationReceiver rate limiting (static companion state), GeoIpLookup atomic CAS window reset, SourceHealthWorker ensures alert channel exists, pauseResumeJob @Volatile, baselineRates synchronized list
7374
- v4.3.0: Notification pause/resume action (5-min pause from notification), CNAME CLOAK badge in log detail sheet, pretty upstream server labels (DoH:Cloudflare), source health DEAD notifications (push alert), alerts notification channel, pause state bypasses blocking
7475
- v4.2.0: Fix DNS log data starvation (CNAME chains, resolved IPs, latency, upstream server now written to DB), CNAME-blocked domains now logged, fd error tracking + auto-restart on TUN error, IPv6 DoH support (honours useDoH flag), IPv6 DNS cache lookup, DohBypassUpdater uses shared OkHttpClient, app context threaded through all forward methods
7576
- v4.1.0: Custom upstream DNS UI in Settings, firewall rule export/import (JSON), automation audit log viewer screen, query rate anomaly detection (3x baseline warning on Home), dropped queries banner, cache hit rate on Home

app/app/build.gradle.kts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// HostShield v4.3.0
1+
// HostShield v4.3.1
22
plugins {
33
id("com.android.application")
44
id("org.jetbrains.kotlin.android")
@@ -15,8 +15,8 @@ android {
1515
applicationId = "com.hostshield"
1616
minSdk = 26
1717
targetSdk = 35
18-
versionCode = 43
19-
versionName = "4.3.0"
18+
versionCode = 44
19+
versionName = "4.3.1"
2020

2121
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
2222

app/app/src/main/java/com/hostshield/service/AutomationReceiver.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,10 @@ class AutomationReceiver : BroadcastReceiver() {
4343

4444
private val TRUSTED_UIDS = setOf(0, 2000) // root, shell
4545
private const val RATE_LIMIT_MS = 5_000L
46-
}
4746

48-
// Per-action rate limit tracking: "action:uid" -> last execution timestamp
49-
private val lastExecTime = java.util.concurrent.ConcurrentHashMap<String, Long>()
47+
// Static rate limit state — survives receiver re-creation per broadcast delivery
48+
private val lastExecTime = java.util.concurrent.ConcurrentHashMap<String, Long>()
49+
}
5050

5151
@Inject lateinit var prefs: AppPreferences
5252
@Inject lateinit var iptablesManager: IptablesManager

app/app/src/main/java/com/hostshield/service/DnsVpnService.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ class DnsVpnService : VpnService() {
263263

264264
// Pause state: when paused, all queries are allowed (no blocking)
265265
@Volatile private var isPaused = false
266-
private var pauseResumeJob: Job? = null
266+
@Volatile private var pauseResumeJob: Job? = null
267267

268268
override fun onCreate() {
269269
super.onCreate()

app/app/src/main/java/com/hostshield/service/SourceHealthWorker.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package com.hostshield.service
22

3+
import android.app.NotificationChannel
34
import android.app.NotificationManager
45
import android.content.Context
6+
import android.os.Build
57
import androidx.core.app.NotificationCompat
68
import androidx.hilt.work.HiltWorker
79
import androidx.work.*
@@ -116,6 +118,15 @@ class SourceHealthWorker @AssistedInject constructor(
116118
private fun notifyDeadSources(labels: List<String>) {
117119
try {
118120
val nm = applicationContext.getSystemService(NotificationManager::class.java) ?: return
121+
122+
// Ensure alert channel exists (VPN service may not have created it yet)
123+
NotificationChannel(
124+
DnsVpnService.ALERT_CHANNEL_ID,
125+
"HostShield Alerts",
126+
NotificationManager.IMPORTANCE_DEFAULT
127+
).apply { description = "Source health and system alerts" }
128+
.let { nm.createNotificationChannel(it) }
129+
119130
val text = if (labels.size == 1) {
120131
"${labels[0]} is unreachable after $DEAD_FAILURE_THRESHOLD failures"
121132
} else {

app/app/src/main/java/com/hostshield/ui/screens/home/HomeViewModel.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,8 @@ class HomeViewModel @Inject constructor(
157157
}
158158

159159
// Baseline query rate (rolling average over first 10 minutes)
160-
private val baselineRates = mutableListOf<Int>()
161-
private var baselineQpm = 0
160+
private val baselineRates = java.util.Collections.synchronizedList(mutableListOf<Int>())
161+
@Volatile private var baselineQpm = 0
162162

163163
/** Track live query rate + anomaly detection from the VPN live stream. */
164164
private fun trackQueryRate() {

app/app/src/main/java/com/hostshield/util/GeoIpLookup.kt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,18 +51,19 @@ class GeoIpLookup @Inject constructor() {
5151
private val backoffUntil = AtomicLong(0)
5252
private val consecutiveBackoffs = AtomicInteger(0)
5353

54-
/** Check if we're within rate limits. */
54+
/** Check if we're within rate limits. Thread-safe with CAS. */
5555
private fun canMakeRequest(): Boolean {
5656
val now = System.currentTimeMillis()
5757

5858
// Exponential backoff active?
5959
if (now < backoffUntil.get()) return false
6060

61-
// Reset window if expired
61+
// Atomic window reset — only one thread wins the CAS
6262
val start = windowStart.get()
6363
if (now - start > WINDOW_MS) {
64-
windowStart.set(now)
65-
requestCount.set(0)
64+
if (windowStart.compareAndSet(start, now)) {
65+
requestCount.set(0)
66+
}
6667
}
6768

6869
return requestCount.get() < MAX_REQUESTS_PER_MINUTE

0 commit comments

Comments
 (0)