Skip to content

Instantly share code, notes, and snippets.

@hackcatml
Last active October 11, 2024 09:15
Show Gist options
  • Save hackcatml/2dfd671599d563488f742c96edc71b7b to your computer and use it in GitHub Desktop.
Save hackcatml/2dfd671599d563488f742c96edc71b7b to your computer and use it in GitHub Desktop.
Unable to perform state transition issue
This issue occurred in the recently updated version of "com.android.art".
@hackcatml
Copy link
Author

hackcatml commented Oct 2, 2024

The issue was resolved by applying the above patch.
But when spawning the app, the Failed to reach single-threaded state error frequently occurred, causing the phone to soft reboot.
By referring to this commit, I modified frida-core/lib/payload/cloak.vala as follows, and the issue was resolved.

diff --git a/lib/payload/cloak.vala b/lib/payload/cloak.vala
index f0ca3f5e..b2e703ee 100644
--- a/lib/payload/cloak.vala
+++ b/lib/payload/cloak.vala
@@ -1,3 +1,4 @@
+// https://github.com/frida/frida-core/blob/b1fab8470aaa2edb4606d740466cd1446ed88918/lib/payload/cloak.vala
 namespace Frida {
 	public class ThreadIgnoreScope {
 		public enum Kind {
@@ -42,122 +43,101 @@ namespace Frida {
 		}
 	}
 
-#if ANDROID
+#if LINUX
 	public class ThreadCountCloaker : Object {
-		private ReadFunc * read_slot;
-		private static ReadFunc old_read_impl;
-
-		private static string expected_magic = "%u (".printf (Posix.getpid ());
-
-		[CCode (has_target = false)]
-		private delegate ssize_t ReadFunc (int fd, void * buf, size_t count);
+		private ReadListener listener;
 
 		construct {
-			Gum.Module.enumerate_imports ("libart.so", imp => {
-				if (imp.name == "read") {
-					read_slot = (ReadFunc *) imp.slot;
-					return false;
-				}
-				return true;
-			});
-			if (read_slot != null)
-				old_read_impl = update_read_slot (on_read);
+			listener = new ReadListener ();
+			Gum.Interceptor.obtain ().attach (
+				(void*) Gum.Module.find_export_by_name (Gum.Process.query_libc_name (), "read"),
+				listener);
 		}
 
 		~ThreadCountCloaker () {
-			if (read_slot != null)
-				update_read_slot (old_read_impl);
+			//  Gum.Interceptor.obtain ().detach (listener);
 		}
 
-		private ReadFunc update_read_slot (ReadFunc new_impl) {
-			Gum.PageProtection old_prot = READ;
-			Gum.Memory.query_protection (read_slot, out old_prot);
+		private class ReadListener : Object, Gum.InvocationListener {
+			private static string expected_magic = "%u (".printf (Posix.getpid ());
 
-			bool is_writable = (old_prot & Gum.PageProtection.WRITE) != 0;
-			if (!is_writable)
-				Gum.mprotect (read_slot, sizeof (void *), old_prot | WRITE);
-
-			ReadFunc old_impl = *read_slot;
-			*read_slot = new_impl;
+			public void on_enter (Gum.InvocationContext context) {
+				Invocation * invocation = context.get_listener_invocation_data (sizeof (Invocation));
+				invocation.fd = (int) context.get_nth_argument (0);
+				invocation.buf = context.get_nth_argument (1);
+				invocation.count = (size_t) context.get_nth_argument (2);
+			}
 
-			if (!is_writable)
-				Gum.mprotect (read_slot, sizeof (void *), old_prot);
+			public void on_leave (Gum.InvocationContext context) {
+				var n = (ssize_t) context.get_return_value ();
+				if (n > 0) {
+					Invocation * invocation = context.get_listener_invocation_data (sizeof (Invocation));
+					if (file_content_might_be_from_proc_self_stat (invocation.buf, n)) {
+						try {
+							if (file_descriptor_is_proc_self_stat (invocation.fd)) {
+								unowned string raw_str = (string) invocation.buf;
+								string str = raw_str.substring (0, n);
+
+								MatchInfo info;
+								if (/^(\d+ \(.+\)(?: [^ ]+){17}) \d+ (.+)/s.match (str, 0, out info)) {
+									string fields_before = info.fetch (1);
+									string fields_after = info.fetch (2);
+
+									// We cannot simply use the value we got from the kernel and subtract the number of cloaked threads,
+									// as there's a chance the total may have changed in the last moment.
+									uint num_uncloaked_threads = query_num_uncloaked_threads ();
+
+									string adjusted_str = "%s %u %s".printf (fields_before, num_uncloaked_threads, fields_after);
+
+									var adjusted_length = adjusted_str.length;
+									if (adjusted_length <= invocation.count) {
+										Memory.copy (invocation.buf, adjusted_str, adjusted_length);
+										context.replace_return_value ((void *) adjusted_length);
+									}
+								}
+							}
+						} catch (FileError e) {
+						}
+					}
+				}
+			}
 
-			return old_impl;
-		}
+			private static bool file_content_might_be_from_proc_self_stat (void * content, ssize_t size) {
+				if (size < expected_magic.length)
+					return false;
+				if (Memory.cmp (content, expected_magic, expected_magic.length) != 0)
+					return false;
+				unowned string raw_str = (string) content;
+				return raw_str[size - 1] == '\n';
+			}
 
-		private static ssize_t on_read (int fd, void * buf, size_t count) {
-			var n = old_read_impl (fd, buf, count);
-			if (n <= 0)
-				return n;
+			private static bool file_descriptor_is_proc_self_stat (int fd) throws FileError {
+				string path = FileUtils.read_link ("/proc/self/fd/%d".printf (fd));
+				uint pid = Posix.getpid ();
+				return (path == "/proc/%u/stat".printf (pid)) ||
+					(path == "/proc/%u/task/%u/stat".printf (pid, pid));
+			}
 
-			if (!file_content_might_be_from_proc_self_stat (buf, n))
+			private static uint query_num_uncloaked_threads () throws FileError {
+				uint n = 0;
+				var dir = Dir.open ("/proc/self/task");
+				string? name;
+				while ((name = dir.read_name ()) != null) {
+					var tid = uint.parse (name);
+					if (!Gum.Cloak.has_thread (tid))
+						n++;
+				}
 				return n;
-
-			try {
-				if (!file_descriptor_is_proc_self_stat (fd))
-					return n;
-
-				unowned string raw_str = (string) buf;
-				string str = raw_str.substring (0, n);
-
-				MatchInfo info;
-				if (!/^(\d+ \(.+\)(?: [^ ]+){17}) \d+ (.+)/s.match (str, 0, out info))
-					return n;
-				string fields_before = info.fetch (1);
-				string fields_after = info.fetch (2);
-
-				// We cannot simply use the value we got from the kernel and subtract the number of cloaked threads,
-				// as there's a chance the total may have changed in the last moment.
-				uint num_uncloaked_threads = query_num_uncloaked_threads ();
-
-				string adjusted_str = "%s %u %s".printf (fields_before, num_uncloaked_threads, fields_after);
-
-				var adjusted_length = adjusted_str.length;
-				if (adjusted_length > count)
-					return n;
-				Memory.copy (buf, adjusted_str, adjusted_length);
-				n = adjusted_length;
-			} catch (FileError e) {
 			}
 
-			return n;
-		}
-
-		private static bool file_content_might_be_from_proc_self_stat (void * content, ssize_t size) {
-			if (size < expected_magic.length)
-				return false;
-			if (Memory.cmp (content, expected_magic, expected_magic.length) != 0)
-				return false;
-			unowned string raw_str = (string) content;
-			return raw_str[size - 1] == '\n';
-		}
-
-		private static bool file_descriptor_is_proc_self_stat (int fd) throws FileError {
-			string path = FileUtils.read_link ("/proc/self/fd/%d".printf (fd));
-			uint pid = Posix.getpid ();
-			return (path == "/proc/%u/stat".printf (pid)) ||
-				(path == "/proc/%u/task/%u/stat".printf (pid, pid));
-		}
-
-		private static uint query_num_uncloaked_threads () throws FileError {
-			uint n = 0;
-			var dir = Dir.open ("/proc/self/task");
-			string? name;
-			while ((name = dir.read_name ()) != null) {
-				var tid = uint.parse (name);
-				if (!Gum.Cloak.has_thread (tid))
-					n++;
+			private struct Invocation {
+				public int fd;
+				public void * buf;
+				public size_t count;
 			}
-			return n;
 		}
 	}
-#else
-	public class ThreadCountCloaker : Object {
-	}
-#endif
 
-#if LINUX
 	public class ThreadListCloaker : Object, DirListFilter {
 		private string our_dir_by_pid;
 		private DirListCloaker cloaker;
@@ -464,6 +444,9 @@ namespace Frida {
 		public abstract bool matches_file (string name);
 	}
 #else
+	public class ThreadCountCloaker : Object {
+	}
+
 	public class ThreadListCloaker : Object {
 	}

@radubogdan2k
Copy link

@hackcatml Unfortunately, this has the same problem as the original PR. On certain phones (e.g. fully updated S21 Ultra, including the latest Google Play update), the function passed to Java.perform never gets called. No exceptions are thrown but the frida script is basically useless :(

@hackcatml
Copy link
Author

@hackcatml Unfortunately, this has the same problem as the original PR. On certain phones (e.g. fully updated S21 Ultra, including the latest Google Play update), the function passed to Java.perform never gets called. No exceptions are thrown but the frida script is basically useless :(

Try this.

Java.perform(function()
{
  Java.deoptimizeEverything();
  // Code
});

@radubogdan2k
Copy link

Java.deoptimizeEverything();

No change :(

@hackcatml
Copy link
Author

Java.deoptimizeEverything();

No change :(

It seems fine to me (com.android.art@350820960).
But Java hooking feels unstable.
It might be better to wait for the official Frida update.

image

@radubogdan2k
Copy link

Java.deoptimizeEverything();

No change :(

It seems fine to me (com.android.art@350820960). But Java hooking feels unstable. It might be better to wait for the official Frida update.

image

What phone are you using?

@radubogdan2k
Copy link

It might be better to wait for the official Frida update.

Definitely, but it seems like nobody's working on it :( Ole said he doesn't have the time and everybody else that gave it a go seems a bit stuck...

@hackcatml
Copy link
Author

Java.deoptimizeEverything();

No change :(

It seems fine to me (com.android.art@350820960). But Java hooking feels unstable. It might be better to wait for the official Frida update.
image

What phone are you using?

Pixel 4a, Android 13 with August 1 Google Play system update

@radubogdan2k
Copy link

Java.deoptimizeEverything();

No change :(

It seems fine to me (com.android.art@350820960). But Java hooking feels unstable. It might be better to wait for the official Frida update.
image

What phone are you using?

Pixel 4a, Android 13 with August 1 Google Play system update

I tried several phones, includ a Pixel 4a. The Pixel was the only one on which it worked.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment