Disable full payload on low memory

Closes #220
This commit is contained in:
emanuele-f 2022-06-22 12:17:09 +02:00
parent f4fe45a442
commit 7238f7ea4a
6 changed files with 149 additions and 2 deletions

View File

@ -140,6 +140,7 @@ public class CaptureService extends VpnService implements Runnable {
private int mSocks5Port;
private String mSocks5Auth;
private CaptureStats mLastStats;
private boolean mLowMemory;
/* The maximum connections to log into the ConnectionsRegister. Older connections are dropped.
* Max estimated memory usage: less than 4 MB (+8 MB with payload mode minimal). */
@ -280,6 +281,7 @@ public class CaptureService extends VpnService implements Runnable {
vpn_ipv4 = VPN_IP_ADDRESS;
last_bytes = 0;
last_connections = 0;
mLowMemory = false;
conn_reg = new ConnectionsRegister(this, CONNECTIONS_LOG_SIZE);
mPcapUri = null;
mDumper = null;
@ -731,6 +733,10 @@ public class CaptureService extends VpnService implements Runnable {
return((INSTANCE != null) && (INSTANCE.mMitmReceiver != null) && INSTANCE.mMitmReceiver.isProxyRunning());
}
public static boolean isLowMemory() {
return((INSTANCE != null) && (INSTANCE.mLowMemory));
}
public static boolean isAlwaysOnVPN() {
return((INSTANCE != null) && INSTANCE.mIsAlwaysOnVPN);
}
@ -936,6 +942,9 @@ public class CaptureService extends VpnService implements Runnable {
checkBlacklistsUpdates();
if(!mLowMemory)
checkAvailableHeap();
// synchronize the conn_reg to ensure that newConnections and connectionsUpdates run atomically
// thus preventing the ConnectionsAdapter from interleaving other operations
synchronized (conn_reg) {
@ -978,6 +987,61 @@ public class CaptureService extends VpnService implements Runnable {
}
}
private void checkAvailableHeap() {
// This does not account per-app jvm limits
long availableHeap = Utils.getAvailableHeap();
if(availableHeap <= Utils.LOW_HEAP_THRESHOLD) {
Log.w(TAG, "Detected low HEAP memory: " + Utils.formatBytes(availableHeap));
handleLowMemory();
}
}
// NOTE: this is only called on low system memory (e.g. obtained via getMemoryInfo). The app
// may still run out of heap memory, whose monitoring requires polling (see checkAvailableHeap)
@Override
public void onTrimMemory(int level) {
String lvlStr = Utils.trimlvl2str(level);
boolean lowMemory = (level != TRIM_MEMORY_UI_HIDDEN) && (level >= TRIM_MEMORY_RUNNING_LOW);
boolean critical = lowMemory && (level >= TRIM_MEMORY_RUNNING_CRITICAL);
Log.w(TAG, "onTrimMemory: " + lvlStr + " - low= " + lowMemory + ", critical=" + critical);
if(lowMemory)
handleLowMemory();
}
private void handleLowMemory() {
Log.w(TAG, "handleLowMemory called");
mLowMemory = true;
if(getCurPayloadMode() == Prefs.PayloadMode.FULL) {
Log.w(TAG, "Releasing full payload memory");
// Disable full payload for new connections
mSettings.full_payload = false;
setPayloadMode(Prefs.PayloadMode.NONE.ordinal());
// Release memory for existing connections
if(conn_reg != null) {
conn_reg.releasePayloadMemory();
// Reclaim released memory
System.gc();
Log.i(TAG, "Memory stats after GC:\n" + Utils.getMemoryStats(this));
}
if(mSettings.tls_decryption) {
// TLS decryption without payload has little use, stop the capture all together
stopService();
mHandler.post(() -> Utils.showToastLong(this, R.string.capture_stopped_low_memory));
} else
mHandler.post(() -> Utils.showToastLong(this, R.string.full_payload_memory_released));
} else // TODO lower memory consumption (e.g. reduce connections register size)
Log.w(TAG, "low memory detected, expect crashes");
}
/* The following methods are called from native code */
public String getVpnIPv4() {
@ -1231,4 +1295,5 @@ public class CaptureService extends VpnService implements Runnable {
public static native int getNumCheckedMalwareConnections();
public static native int getNumCheckedFirewallConnections();
public static native int rootCmd(String prog, String args);
public static native void setPayloadMode(int mode);
}

View File

@ -391,4 +391,13 @@ public class ConnectionsRegister {
Collections.sort(rv);
return rv;
}
public synchronized void releasePayloadMemory() {
Log.d(TAG, "releaseFullPayloadMemory called");
for(int i=0; i<mCurItems; i++) {
ConnectionDescriptor conn = mItemsRing[i];
conn.dropPayload();
}
}
}

View File

@ -22,12 +22,14 @@ package com.emanuelef.remote_capture;
import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.Dialog;
import android.app.PendingIntent;
import android.app.UiModeManager;
import android.content.ActivityNotFoundException;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.ComponentCallbacks2;
import android.content.ComponentName;
import android.content.ContentValues;
import android.content.Context;
@ -132,6 +134,7 @@ public class Utils {
public static final int PER_USER_RANGE = 100000;
public static final int UID_UNKNOWN = -1;
public static final int UID_NO_FILTER = -2;
public static final int LOW_HEAP_THRESHOLD = 10485760 /* 10 MB */;
private static Boolean rootAvailable = null;
private static Locale primaryLocale = null;
@ -1208,4 +1211,49 @@ public class Utils {
public static boolean rootGrantPermission(Context context, String perm) {
return CaptureService.rootCmd("pm", String.format("grant --user %d %s %s", getUserId(getPCAPdroidUid(context)), BuildConfig.APPLICATION_ID, perm)) == 0;
}
// Returns the available dalvik vm heap size for this app. Exceeding this size will result into
// an OOM exception
public static long getAvailableHeap() {
Runtime runtime = Runtime.getRuntime();
// maxMemory: max memory which can be allocated on this app vm (should correspond to getMemoryClass)
// totalMemory: currently allocated memory (used/unused) by the vm
// freeMemory: free portion of the totalMemory
long unallocated = runtime.maxMemory() - runtime.totalMemory();
return unallocated + runtime.freeMemory();
}
public static String trimlvl2str(int lvl) {
switch (lvl) {
case ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN: return "TRIM_MEMORY_UI_HIDDEN";
case ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE: return "TRIM_MEMORY_RUNNING_MODERATE";
case ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW: return "TRIM_MEMORY_RUNNING_LOW";
case ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL: return "TRIM_MEMORY_RUNNING_CRITICAL";
case ComponentCallbacks2.TRIM_MEMORY_BACKGROUND: return "TRIM_MEMORY_BACKGROUND";
case ComponentCallbacks2.TRIM_MEMORY_MODERATE: return "TRIM_MEMORY_MODERATE";
case ComponentCallbacks2.TRIM_MEMORY_COMPLETE: return "TRIM_MEMORY_COMPLETE";
default: return "TRIM_UNKNOWN";
}
}
public static String getMemoryStats(Context context) {
// This accounts system-wide limits
ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo();
activityManager.getMemoryInfo(memoryInfo);
ActivityManager.RunningAppProcessInfo memState = new ActivityManager.RunningAppProcessInfo();
ActivityManager.getMyMemoryState(memState);
// This accounts app-specific limits (dalvik heap)
Runtime runtime = Runtime.getRuntime();
long heapAvailable = getAvailableHeap();
boolean heapLow = heapAvailable <= LOW_HEAP_THRESHOLD;
return "[Runtime] free: " + Utils.formatBytes(runtime.freeMemory()) + ", max: " + Utils.formatBytes(runtime.maxMemory()) + ", allocated: " + Utils.formatBytes(runtime.totalMemory()) + ", available: " + Utils.formatBytes(heapAvailable) + ", low=" + heapLow +
"\n[MemoryState] pid: " + memState.pid + ", trimlevel: " + trimlvl2str(memState.lastTrimLevel) +
"\n[MemoryInfo] available: " + Utils.formatBytes(memoryInfo.availMem) + ", total: " + Utils.formatBytes(memoryInfo.totalMem) + ", lowthresh: " + Utils.formatBytes(memoryInfo.threshold) + ", low=" + memoryInfo.lowMemory +
"\n[MemoryClass] standard: " + activityManager.getMemoryClass() + " MB, large: " + activityManager.getLargeMemoryClass() + " MB";
}
}

View File

@ -169,8 +169,12 @@ public class ConnectionDescriptor {
// Payload for decryptable connections should be received via the MitmReceiver
assert(isNotDecryptable());
payload_chunks = update.payload_chunks;
payload_truncated = update.payload_truncated;
// Some pending updates with payload may still be received after low memory has been
// triggered and payload disabled
if(!CaptureService.isLowMemory()) {
payload_chunks = update.payload_chunks;
payload_truncated = update.payload_truncated;
}
}
}
@ -298,6 +302,10 @@ public class ConnectionDescriptor {
payload_length += chunk.payload.length;
}
public void dropPayload() {
payload_chunks = null;
}
private boolean hasHttp(boolean is_sent) {
if(getNumPayloadChunks() == 0)
return false;

View File

@ -806,6 +806,21 @@ Java_com_emanuelef_remote_1capture_CaptureService_rootCmd(JNIEnv *env, jclass cl
return rv;
}
/* ******************************************************* */
JNIEXPORT void JNICALL
Java_com_emanuelef_remote_1capture_CaptureService_setPayloadMode(JNIEnv *env, jclass clazz, jint mode) {
pcapdroid_t *pd = global_pd;
if(!pd) {
log_e("NULL pd instance");
return;
}
pd->payload_mode = mode;
}
/* ******************************************************* */
char* getStringPref(pcapdroid_t *pd, const char *key, char *buf, int bufsize) {
JNIEnv *env = pd->env;

View File

@ -362,4 +362,6 @@
<string name="permission_granted">%1$s permission was granted</string>
<string name="permission_grant_fail">%1$s permission could not be granted</string>
<string name="connection_not_found">Could not find the given connection</string>
<string name="full_payload_memory_released">The app ran out of memory. Connections payload is now disabled</string>
<string name="capture_stopped_low_memory">Capture stopped due to low memory</string>
</resources>