From 08e702606b0e419acec074b61f2e23a364346ac1 Mon Sep 17 00:00:00 2001 From: Muhtasham Nawr al-Mahmud Date: Sat, 8 Aug 2026 05:35:15 +0600 Subject: [PATCH] Add support for hooking class initializers Expose as $clinit on class wrappers, alongside $init and $new, so a class' static initialization can be intercepted, inspected or skipped: const Foo = Java.use('com.example.Foo'); Foo.$clinit.implementation = function () { this.$clinit(); }; As a class is only ever initialized once, the hook must be installed before anything touches the class. Resolving through JNI would defeat that: GetStaticMethodID() goes through FindMethodJNI(), which begins by calling EnsureInitialized(), and would therefore run the very initializer we are about to hook. Java reflection is no help either, as is exposed by neither getDeclaredMethods() nor getDeclaredConstructors(), which is how $init and $new resolve their method IDs. The ID is therefore taken from ClassModel, which enumerates methods without going through method resolution. Unlike $init, $clinit is a plain Method rather than a MethodDispatcher, as a class has at most one class initializer and its signature is always ()V, leaving nothing to dispatch on. Class model model_new()'s three branches disagreed on constructors. The JVMTI branch added every method GetClassMethods() reports, so and became members under their raw names, reachable as wrapper['']; the ART branch dropped everything carrying kAccConstructor; the reflection fallback uses getDeclaredMethods(), which never reports constructors at all. All three now agree that constructors are not members. Instance ones are dropped, as $new and $init resolve them independently and need the signatures only reflection provides. The class initializer is kept in Model::class_initializer rather than in the members hash table, and read back through the new model_find_class_initializer(). Putting it in the table would surface it in model_list(), which backs the wrapper Proxy's ownKeys() trap, and as getOwnPropertyDescriptor() reports every member as enumerable, Object.entries() on a wrapper would resolve it through makeMethodFromSpec(). That calls ToReflectedMethod(), which yields a Constructor for , and would then invoke java.lang.reflect.Method methods on it. The ART branch identifies as IsConstructor() && IsStatic(), matching ArtMethod::IsClassInitializer(), and names it without consulting ART for the same reason. Java.enumerateMethods() now reports $clinit instead of skipping it, mirroring how is reported as $init. https://android.googlesource.com/platform/art/+/refs/heads/main/runtime/art_method.h Class initialization Java.use() no longer initializes the class; that now happens when a static field is first read or an implementation is installed. ART rewrites the quick entrypoint of every static method of a class as that class becomes initialized, via ClassLinker::FixupStaticTrampolines(), and ArtMethodMangler.replace() snapshots quickCode when the hook is installed, so hooking a static method of a not-yet-initialized class would leave the mangler holding an entrypoint ART replaces moments later, silently bypassing the hook. Initializing at hook time preserves that ordering while leaving classes that are merely wrapped alone. CLASS_INITIALIZER_METHOD is exempt, as forcing initialization is precisely what hooking it is meant to intercept. https://android.googlesource.com/platform/art/+/refs/heads/main/runtime/class_linker.cc ensureClassInitialized() had not initialized anything since 7983e81, which replaced its dummy field lookup with getClassName() to avoid the NoSuchFieldError it had to clear afterwards. The exception was noise, but the lookup was the mechanism: FindFieldJNI() begins by calling EnsureInitialized(), and getClassName() reaches nothing of the sort. Resolve Object.hashCode() instead, which every class inherits and FindClassMethod() finds by walking the superclass chain. Interfaces are the one exception, as FindInterfaceMethodWithSignature() searches only their declared and superinterface methods; they are initialized all the same, before the lookup fails. https://android.googlesource.com/platform/art/+/refs/heads/main/runtime/jni/jni_internal.cc Invocation ClassLinker::InitializeClass() runs the initializer through ArtMethod::Invoke() rather than from a call site. That function hands over to the interpreter -- running the DEX bytecode straight from the code item, never consulting the quick entrypoint the mangler patched -- whenever the calling thread is forced to interpret, and dispatches through the entrypoint otherwise. Hooking a class initializer therefore took effect only when ART happened to pick the latter branch, which varies with the API level and the compiler filter. Every other method has bytecode or quick call sites that stay instrumented; has only this one. ArtMethod::Invoke() is therefore instrumented as well, swapping in the replacement method, which is native and thus not eligible for interpretation, putting the call back onto the quick path. It needs a stack check written for the stack it actually sees: the fragment is pushed after the hook has run, so the one in find_replacement_method_from_quick_code() does not hold, and a class initializer re-entered through this.$clinit() recursed until the stack ran out. Our own JNI replacement stub is on top precisely when the hook is invoking the original, so compare against it directly, masking the tag bit off the top of the stack the way ManagedStack::GetTopQuickFrame() does. The comparison is per-method, leaving a replacement free to invoke some other hooked method and still reach that method's hook. The deoptimization APIs are the other caller of this hook and were exposed to the same recursion, so they get the fix too. https://android.googlesource.com/platform/art/+/refs/heads/main/runtime/art_method.cc HotSpot $clinit was enumerable on the JVM but never hookable: jni_FindClass() passes init=true down to find_class_from_class_loader(), so Java.use() ran the initializer before the caller could install anything. Resolve through Class.forName(name, false, loader) instead. That loads without initializing, but stops short of linking too, and JVMTI will not enumerate the members of a class that has yet to be prepared: GetClassMethods() fails with JVMTI_ERROR_CLASS_NOT_PREPARED, and model_new() ignores the return value, leaving it to walk and free a count and array it never wrote. Ask for the declared methods first, as JVM_GetClassDeclaredMethods() links the class without initializing it. jni_FindClass() takes its loader from the topmost Java frame on the calling thread, falling back to the system one when the thread has none, so Class.forName() has to be told the same or classes only a custom loader can see would stop resolving from inside a hook. getCallerClassLoader() works it out with GetStackTrace(), GetMethodDeclaringClass() and GetClassLoader(), none of which need a capability. jvm.js's ensureClassInitialized() was an empty function, which no longer holds now that Java.use() leaves the class alone: every static field would read back its default. jni_GetMethodID() calls klass->initialize() before the lookup, so the trick ART uses ports over unchanged, down to interfaces needing the pending exception cleared afterwards. https://github.com/openjdk/jdk/blob/master/src/hotspot/share/prims/jni.cpp Tests ClassInitializerTest covers hooking a class initializer, replacing it, invoking the original from the replacement, enumeration, and the class that has none. The harness needed repairs to run at all on newer API levels: the runner gives ART back its signal handlers instead of stubbing out libsigchain, script code is spliced into the bundle rather than appended to it, a script that never replies fails with its own error and a logcat dump instead of timing out silently, and APP_PLATFORM is clamped to what the NDK actually ships. The test matrix extends to Android 16, dropping x86 in favour of x86_64 throughout. --- .github/workflows/linting.yml | 6 +- .github/workflows/tests.yml | 16 ++-- index.d.ts | 14 ++++ lib/android.js | 83 ++++++++++++++++++- lib/class-factory.js | 97 +++++++++++++++++++++- lib/class-model.js | 48 +++++++---- lib/env.js | 47 +++++++++++ lib/jvm.js | 49 +++++++++++ lib/jvmti.js | 15 ++++ test/Application.mk | 7 +- test/Makefile | 9 +- test/bundle.js | 2 +- test/re/frida/ClassCreationTest.java | 6 +- test/re/frida/ClassInitializerTest.java | 105 ++++++++++++++++++++++++ test/re/frida/ClassRegistryTest.java | 6 +- test/re/frida/MethodTest.java | 6 +- test/re/frida/Script.java | 10 ++- test/re/frida/TestRunner.java | 81 +++++++++++++++++- test/runner.c | 78 +++++++++++++++++- 19 files changed, 632 insertions(+), 53 deletions(-) create mode 100644 test/re/frida/ClassInitializerTest.java diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index adc979e6..a4e4cfb9 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -4,9 +4,9 @@ jobs: eslint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 with: - node-version: 18 + node-version: 22 - run: npm install - run: npm run lint diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d77a2577..59806e64 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -4,26 +4,22 @@ jobs: tests: runs-on: ubuntu-latest strategy: + fail-fast: false matrix: - api: [26, 27, 28, 29, 30, 31] - arch: [x86, x86_64] - exclude: - - api: 30 - arch: x86 - - api: 31 - arch: x86 + api: [26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36] + arch: [x86_64] env: ANDROID_EMULATOR_WAIT_TIME_BEFORE_KILL: 200 # Default is 20 steps: - name: Check out repo - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Enable KVM run: | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules sudo udevadm control --reload-rules sudo udevadm trigger --name-match=kvm - name: AVD cache - uses: actions/cache@v4 + uses: actions/cache@v6 id: avd-cache with: path: | @@ -50,7 +46,7 @@ jobs: disable-animations: true script: >- sdkmanager "platforms;android-${{ matrix.api }}" - && PATH="$ANDROID_SDK_ROOT/build-tools/34.0.0:$PATH" + && PATH="$ANDROID_SDK_ROOT/build-tools/37.0.0:$PATH" ANDROID_ARCH=${{ matrix.arch }} ANDROID_ABI=${{ matrix.arch }} ANDROID_API_LEVEL=${{ matrix.api }} diff --git a/index.d.ts b/index.d.ts index 8237d839..ddf4fb09 100644 --- a/index.d.ts +++ b/index.d.ts @@ -378,6 +378,20 @@ declare module "frida-java-bridge" { */ $init: MethodDispatcher; + /** + * The class initializer, i.e. ``, which the runtime invokes once to + * initialize the class' static state. + * + * Replace the `implementation` property to hook it. As the class is only ever + * initialized once, this must happen before anything touches it -- accessing a + * static member, or calling a static method, is enough to trigger it. Note that + * `Java.use()` itself deliberately does not. Throws if the class has no class + * initializer. + * + * Not supported on Dalvik, i.e. Android < 5.0. + */ + $clinit: Method; + /** * Eagerly deletes the underlying JNI global reference without having to * wait for the object to become unreachable and the JavaScript diff --git a/lib/android.js b/lib/android.js index d023166d..7d28dd37 100644 --- a/lib/android.js +++ b/lib/android.js @@ -113,6 +113,7 @@ let thunkPage = null; let thunkOffset = 0; let taughtArtAboutReplacementMethods = false; let taughtArtAboutMethodInstrumentation = false; +let taughtArtAboutRuntimeMethodInvocation = false; let backtraceModule = null; const jdwpSessions = []; let socketpair = null; @@ -586,7 +587,16 @@ export function ensureClassInitialized (env, classRef) { return; } - env.getClassName(classRef); + // FindMethodJNI() begins by calling EnsureInitialized(), so resolving any method initializes + // the class. Every class inherits Object.hashCode() and FindClassMethod() walks the superclass + // chain, so the lookup succeeds and nothing is thrown. + const method = env.getMethodId(classRef, 'hashCode', '()I'); + if (method.isNull()) { + // Interfaces are the one exception, as FindInterfaceMethodWithSignature() searches only + // their declared and superinterface methods, never Object's. They are initialized all the + // same, before the lookup fails. + env.exceptionClear(); + } } function getArtVMSpec (api) { @@ -1977,6 +1987,50 @@ on_interpreter_do_call (GumInvocationContext * ic) gum_invocation_context_replace_nth_argument (ic, 0, replacement_method); } +gpointer +find_replacement_method_from_runtime_invoke (gpointer method, + gpointer thread) +{ + gpointer replacement_method; + gpointer managed_stack; + gpointer * top_quick_frame; + + replacement_method = get_replacement_method (method); + if (replacement_method == NULL) + return NULL; + + /* + * Stack check, as above, but for callers that have yet to push a fragment of + * their own -- ArtMethod::Invoke() does so only once we have returned, leaving + * the frame on top still belonging to whoever called it. + * + * Our own JNI replacement stub is therefore on top precisely when this is the + * hook invoking the original, which is the one case where the original is what + * must run. The check is per-method, so one replacement invoking a different + * hooked method still reaches that method's hook. + */ + managed_stack = thread + ${threadOffsets.managedStack}; + top_quick_frame = GSIZE_TO_POINTER (*((gsize *) (managed_stack + ${managedStackOffsets.topQuickFrame})) & ~((gsize) 1)); + if (top_quick_frame != NULL && *top_quick_frame == replacement_method) + return NULL; + + return replacement_method; +} + +void +on_art_method_invoke (GumInvocationContext * ic) +{ + gpointer method, thread; + gpointer replacement_method; + + method = gum_invocation_context_get_nth_argument (ic, 0); + thread = gum_invocation_context_get_nth_argument (ic, 1); + + replacement_method = find_replacement_method_from_runtime_invoke (method, thread); + if (replacement_method != NULL) + gum_invocation_context_replace_nth_argument (ic, 0, replacement_method); +} + gpointer on_art_method_get_oat_quick_method_header (gpointer method, gpointer pc) @@ -2059,6 +2113,7 @@ on_leave_gc_concurrent_copying_copying_phase (GumInvocationContext * ic) doCall: cm.on_interpreter_do_call }, ArtMethod: { + invoke: cm.on_art_method_invoke, getOatQuickMethodHeader: cm.on_art_method_get_oat_quick_method_header, prettyMethod: cm.on_art_method_pretty_method }, @@ -2181,6 +2236,28 @@ function instrumentArtFixupStaticTrampolines () { } } +export function ensureArtKnowsHowToHandleRuntimeMethodInvocation () { + if (taughtArtAboutRuntimeMethodInvocation) { + return; + } + taughtArtAboutRuntimeMethodInvocation = true; + + /* + * ArtMethod::Invoke() hands over to the interpreter instead of the quick entrypoints -- and + * thus bypasses our instrumentation of them -- whenever the calling thread is forced to use + * the interpreter. Swapping in our replacement, which is native and therefore not eligible for + * interpretation, puts the call back onto the quick path. + * + * https://android.googlesource.com/platform/art/+/refs/heads/main/runtime/art_method.cc + */ + const invoke = getApi().find('_ZN3art9ArtMethod6InvokeEPNS_6ThreadEPjjPNS_6JValueEPKc'); + if (invoke === null) { + return; + } + + Interceptor.attach(invoke, artController.hooks.ArtMethod.invoke); +} + function ensureArtKnowsHowToHandleReplacementMethods (vm) { if (taughtArtAboutReplacementMethods) { return; @@ -3990,6 +4067,8 @@ export function deoptimizeBootImage (vm, env) { throw new Error('This API is only available on Android >= 8.0'); } + ensureArtKnowsHowToHandleRuntimeMethodInvocation(); + withRunnableArtThread(vm, env, thread => { api['art::Runtime::DeoptimizeBootImage'](api.artRuntime); }); @@ -4002,6 +4081,8 @@ function requestDeoptimization (vm, env, kind, method) { throw new Error('This API is only available on Android >= 7.0'); } + ensureArtKnowsHowToHandleRuntimeMethodInvocation(); + withRunnableArtThread(vm, env, thread => { if (getAndroidApiLevel() < 30) { if (!api.isJdwpStarted()) { diff --git a/lib/class-factory.js b/lib/class-factory.js index 68a1a3b4..97e2084c 100644 --- a/lib/class-factory.js +++ b/lib/class-factory.js @@ -1,6 +1,10 @@ import Env from './env.js'; import * as android from './android.js'; -import { ensureClassInitialized as jvmEnsureClassInitialized, makeMethodMangler as jvmMakeMethodMangler } from './jvm.js'; +import { + ensureClassInitialized as jvmEnsureClassInitialized, + findClassWithoutInitializing as jvmFindClassWithoutInitializing, + makeMethodMangler as jvmMakeMethodMangler +} from './jvm.js'; import ClassModel from './class-model.js'; import LRU from './lru.js'; import mkdex from './mkdex.js'; @@ -21,6 +25,7 @@ const kAccStatic = 0x0008; const CONSTRUCTOR_METHOD = 1; const STATIC_METHOD = 2; const INSTANCE_METHOD = 3; +const CLASS_INITIALIZER_METHOD = 4; const STATIC_FIELD = 1; const INSTANCE_FIELD = 2; @@ -235,6 +240,14 @@ export default class ClassFactory { return this[Symbol.for('c')]; } }, + [Symbol.for('ci')]: { + value: [null] + }, + $ci: { + get () { + return this[Symbol.for('ci')]; + } + }, [Symbol.for('m')]: { value: new Map() }, @@ -282,8 +295,6 @@ export default class ClassFactory { try { const classHandle = h.value; - ensureClassInitialized(env, classHandle); - proto.$l = ClassModel.build(classHandle, env); } finally { h.unref(env); @@ -914,6 +925,18 @@ Object.defineProperties(Wrapper.prototype, { return this[Symbol.for('init')]; } }, + [Symbol.for('clinit')]: { + enumerable: false, + get () { + return this.$getClassInitializer(); + } + }, + $clinit: { + enumerable: true, + get () { + return this[Symbol.for('clinit')]; + } + }, [Symbol.for('dispose')]: { enumerable: false, value () { @@ -1092,6 +1115,39 @@ Object.defineProperties(Wrapper.prototype, { return this[Symbol.for('getCtor')](type); } }, + [Symbol.for('getClassInitializer')]: { + enumerable: false, + value () { + const slot = this.$ci; + + let clinit = slot[0]; + if (clinit === null) { + const classWrapper = this.$w; + const { $n: className, $f: factory } = classWrapper; + + /* + * Taken from our own model instead of GetStaticMethodID(), which resolves through + * FindMethodJNI() -- and that calls EnsureInitialized(), running the very initializer we + * are about to hook. Superclasses are not consulted, as is never inherited. + */ + const methodId = this.$l.findClassInitializer(); + if (methodId === null) { + throw new Error(`${className} has no class initializer`); + } + + clinit = makeMethod('$clinit', classWrapper, CLASS_INITIALIZER_METHOD, methodId, + factory._getType('void', false), [], vm.getEnv()); + slot[0] = clinit; + } + + return clinit; + } + }, + $getClassInitializer: { + value () { + return this[Symbol.for('getClassInitializer')](); + } + }, [Symbol.for('borrowClassHandle')]: { enumerable: false, value (env) { @@ -1279,6 +1335,9 @@ function makeBasicClassHandleGetter (className) { const tid = getCurrentThreadId(); ignore(tid); try { + if (!isArtVm) { + return jvmFindClassWithoutInitializing(env, className); + } return env.findClass(canonicalClassName); } finally { unignore(tid); @@ -1644,7 +1703,7 @@ function makeMethod (methodName, classWrapper, type, methodId, retType, argTypes if (type === INSTANCE_METHOD) { callVirtually = env.vaMethod(rawRetType, rawArgTypes, invocationOptions); callDirectly = env.nonvirtualVaMethod(rawRetType, rawArgTypes, invocationOptions); - } else if (type === STATIC_METHOD) { + } else if (type === STATIC_METHOD || type === CLASS_INITIALIZER_METHOD) { callVirtually = env.staticVaMethod(rawRetType, rawArgTypes, invocationOptions); callDirectly = callVirtually; } else { @@ -1722,6 +1781,26 @@ methodPrototype = Object.create(Function.prototype, { if (fn !== null) { const [methodName, classWrapper, type, methodId, retType, argTypes] = params; + if (type !== CLASS_INITIALIZER_METHOD) { + // ART rewrites every static method's quick entrypoint once the class is initialized, + // which would leave the mangler holding a stale one. + const env = vm.getEnv(); + const h = classWrapper.$borrowClassHandle(env); + try { + ensureClassInitialized(env, h.value); + } finally { + h.unref(env); + } + } else if (isArtVm) { + /* + * Initializing is exactly what hooking is meant to intercept, so it is exempt + * from the above and needs something else: ClassLinker reaches through + * ArtMethod::Invoke(), which hands over to the interpreter -- bypassing the entrypoint + * the mangler patched -- whenever the calling thread is forced to interpret. + */ + android.ensureArtKnowsHowToHandleRuntimeMethodInvocation(); + } + const replacement = implement(methodName, classWrapper, type, retType, argTypes, fn, this); const mangler = makeMethodMangler(methodId); replacement._m = mangler; @@ -2047,6 +2126,16 @@ function makeFieldFromSpec (name, spec, classHandle, classWrapper, env) { const id = ptr(spec.substr(3)); const { $f: factory } = classWrapper; + /* + * GetStaticField() makes no initialization check of its own, and the field ID comes from + * ClassModel rather than GetStaticFieldID(), so nothing else on this path would run the + * initializer. Members are resolved lazily, so this happens on first access rather than in + * Java.use(), leaving $clinit hookable. + */ + if (type === STATIC_FIELD) { + ensureClassInitialized(env, classHandle); + } + let fieldType; const field = env.toReflectedField(classHandle, id, (type === STATIC_FIELD) ? 1 : 0); try { diff --git a/lib/class-model.js b/lib/class-model.js index 28b68b07..f90bbfae 100644 --- a/lib/class-model.js +++ b/lib/class-model.js @@ -54,6 +54,7 @@ typedef void (* ArtPrettyMethodFunc) (StdString * result, ArtMethod * method, jb struct _Model { GHashTable * members; + jmethodID class_initializer; }; struct _EnumerateMethodsContext @@ -256,6 +257,7 @@ model_new (jclass class_handle, members = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); model->members = members; + model->class_initializer = NULL; if (jvmti != NULL) { @@ -282,7 +284,11 @@ model_new (jclass class_handle, get_method_name (jvmti, method, &name, NULL, NULL); get_method_modifiers (jvmti, method, &modifiers); - model_add_method (model, name, method, modifiers); + /* Not a member: it is reached through $clinit. */ + if (strcmp (name, "") == 0) + model->class_initializer = method; + else + model_add_method (model, name, method, modifiers); deallocate (jvmti, name); } @@ -327,13 +333,20 @@ model_new (jclass class_handle, id = elements + (i * art_api.method_size); access_flags = *(guint32 *) (id + art_api.method_offset_access_flags); + is_static = (access_flags & kAccStatic) != 0; + modifiers = access_flags & 0xffff; + if ((access_flags & kAccConstructor) != 0) + { + /* ArtMethod::IsClassInitializer() is IsConstructor() && IsStatic(). */ + if (is_static) + model->class_initializer = id; continue; - is_static = (access_flags & kAccStatic) != 0; + } + method = to_reflected_method (env, class_handle, id, is_static); name = call_object_method (env, method, java_api.method.get_name); name_str = get_string_utf_chars (env, name, NULL); - modifiers = access_flags & 0xffff; model_add_method (model, name_str, id, modifiers); @@ -508,6 +521,12 @@ model_find (Model * self, return g_hash_table_lookup (self->members, member); } +jmethodID +model_find_class_initializer (Model * self) +{ + return self->class_initializer; +} + gchar * model_list (Model * self) { @@ -619,7 +638,7 @@ collect_matching_class_methods (ArtClassVisitor * self, { ArtMethod * method; guint32 access_flags; - jboolean is_constructor; + jboolean is_constructor, is_static; StdString method_name = { 0, }; const gchar * bare_method_name; gchar * bare_method_name_copy = NULL; @@ -630,6 +649,7 @@ collect_matching_class_methods (ArtClassVisitor * self, access_flags = *(guint32 *) ((gpointer) method + art_api.method_offset_access_flags); is_constructor = (access_flags & kAccConstructor) != 0; + is_static = (access_flags & kAccStatic) != 0; art_api.pretty_method (&method_name, method, ctx->include_signature); bare_method_name = std_string_c_str (&method_name); @@ -640,14 +660,12 @@ collect_matching_class_methods (ArtClassVisitor * self, return_type_end = strchr (bare_method_name, ' '); name_begin = return_type_end + 1 + class_name_length + 1; - if (is_constructor && g_str_has_prefix (name_begin, "")) - goto skip_method; name = g_string_sized_new (64); if (is_constructor) { - g_string_append (name, "$init"); + g_string_append (name, is_static ? "$clinit" : "$init"); g_string_append (name, strchr (name_begin, '>') + 1); } else @@ -662,14 +680,8 @@ collect_matching_class_methods (ArtClassVisitor * self, } else { - const gchar * name_begin; - - name_begin = bare_method_name + class_name_length + 1; - if (is_constructor && strcmp (name_begin, "") == 0) - goto skip_method; - if (is_constructor) - bare_method_name = "$init"; + bare_method_name = is_static ? "$clinit" : "$init"; else bare_method_name += class_name_length + 1; } @@ -844,7 +856,7 @@ enumerate_methods_jvm (const gchar * class_query, if (strcmp (method_name, "") == 0) method_name = "$init"; else if (strcmp (method_name, "") == 0) - goto skip_method; + method_name = "$clinit"; } if (include_signature) @@ -1303,6 +1315,11 @@ export default class Model { return cm.find(this.handle, Memory.allocUtf8String(member)).readUtf8String(); } + findClassInitializer () { + const id = cm.findClassInitializer(this.handle); + return !id.isNull() ? id : null; + } + list () { const str = cm.list(this.handle); try { @@ -1409,6 +1426,7 @@ function compileModule (env) { new: new NativeFunction(cm.model_new, 'pointer', ['pointer', 'pointer', 'pointer'], reentrantOptions), has: new NativeFunction(cm.model_has, 'bool', ['pointer', 'pointer'], fastOptions), find: new NativeFunction(cm.model_find, 'pointer', ['pointer', 'pointer'], fastOptions), + findClassInitializer: new NativeFunction(cm.model_find_class_initializer, 'pointer', ['pointer'], fastOptions), list: new NativeFunction(cm.model_list, 'pointer', ['pointer'], fastOptions), enumerateMethodsArt: new NativeFunction(cm.enumerate_methods_art, 'pointer', ['pointer', 'pointer', 'bool', 'bool', 'bool'], reentrantOptions), diff --git a/lib/env.js b/lib/env.js index b8a5a7d1..d2d37037 100644 --- a/lib/env.js +++ b/lib/env.js @@ -214,6 +214,33 @@ Env.prototype.findClass = proxy(6, 'pointer', ['pointer', 'pointer'], function ( return result; }); +/* + * Same, but for the JVM, whose FindClass() also initializes -- running the very class initializer + * that hooking $clinit exists to intercept. Class.forName() stops short of that, but stops short + * of linking too, and JVMTI will not enumerate the members of an unprepared class, so ask for the + * declared methods to link it. + * + * Takes the name in dotted form, as Class.forName() does, and the loader to resolve it against -- + * FindClass() picks one implicitly, so it is up to the caller to pick the same. + */ +Env.prototype.findClassWithoutInitializing = function (name, loader) { + const Class = this.javaLangClass(); + const nameValue = this.newStringUtf(name); + try { + const handle = this.staticVaMethod('pointer', ['pointer', 'int', 'pointer'])( + this.handle, Class.handle, Class.forName, nameValue, 0, loader); + this.throwIfExceptionPending(); + + const methods = this.vaMethod('pointer', [])(this.handle, handle, Class.getDeclaredMethods); + this.throwIfExceptionPending(); + this.deleteLocalRef(methods); + + return handle; + } finally { + this.deleteLocalRef(nameValue); + } +}; + Env.prototype.throwIfExceptionPending = function () { const throwable = this.exceptionOccurred(); if (throwable.isNull()) { @@ -659,6 +686,8 @@ Env.prototype.javaLangClass = function () { const get = this.getMethodId.bind(this, handle); javaLangClass = { handle: register(this.newGlobalRef(handle)), + forName: this.getStaticMethodId(handle, 'forName', + '(Ljava/lang/String;ZLjava/lang/ClassLoader;)Ljava/lang/Class;'), getName: get('getName', '()Ljava/lang/String;'), getSimpleName: get('getSimpleName', '()Ljava/lang/String;'), getGenericSuperclass: get('getGenericSuperclass', '()Ljava/lang/reflect/Type;'), @@ -677,6 +706,24 @@ Env.prototype.javaLangClass = function () { return javaLangClass; }; +let systemClassLoader = null; +Env.prototype.systemClassLoader = function () { + if (systemClassLoader === null) { + const handle = this.findClass('java/lang/ClassLoader'); + try { + const getSystemClassLoader = this.getStaticMethodId(handle, 'getSystemClassLoader', + '()Ljava/lang/ClassLoader;'); + const loader = this.staticVaMethod('pointer', [])(this.handle, handle, getSystemClassLoader); + this.throwIfExceptionPending(); + systemClassLoader = register(this.newGlobalRef(loader)); + this.deleteLocalRef(loader); + } finally { + this.deleteLocalRef(handle); + } + } + return systemClassLoader; +}; + let javaLangObject = null; Env.prototype.javaLangObject = function () { if (javaLangObject === null) { diff --git a/lib/jvm.js b/lib/jvm.js index 0319cdba..fda58e92 100644 --- a/lib/jvm.js +++ b/lib/jvm.js @@ -10,6 +10,7 @@ import VM from './vm.js'; const jsizeSize = 4; const { pointerSize } = Process; +const jvmtiFrameInfoSize = 16; // jmethodID plus jlocation, the latter a jlong on every ABI const JVM_ACC_NATIVE = 0x0100; const JVM_ACC_IS_OLD = 0x00010000; @@ -427,6 +428,54 @@ function parseX64ThreadOffset (insn) { } export function ensureClassInitialized (env, classRef) { + // get_method_id() initializes the class before looking anything up, and every class inherits + // Object.hashCode(), so the lookup succeeds and nothing is thrown. + const method = env.getMethodId(classRef, 'hashCode', '()I'); + if (method.isNull()) { + // Interfaces are the one exception, as lookup_method() and its walk of the superinterfaces + // never reach Object's. They are initialized all the same, before the lookup fails. + env.exceptionClear(); + } +} + +export function findClassWithoutInitializing (env, name) { + const loader = getCallerClassLoader(env); + try { + return env.findClassWithoutInitializing(name, (loader !== null) ? loader : env.systemClassLoader()); + } finally { + if (loader !== null) { + env.deleteLocalRef(loader); + } + } +} + +/* + * jni_FindClass() resolves against the loader of the topmost Java frame on the calling thread, + * falling back to the system one when there is none -- as there is not whenever we are called + * from outside a hook. Class.forName() insists on being told, so work out the same loader. + * + * Returns a local reference, NULL meaning the bootstrap loader, or null when there is no frame + * to take one from. + */ +function getCallerClassLoader (env) { + const { jvmti } = getApi(); + + const frames = Memory.alloc(jvmtiFrameInfoSize); + const numFrames = Memory.alloc(jsizeSize); + jvmti.getStackTrace(NULL, 0, 1, frames, numFrames); + if (numFrames.readS32() === 0) { + return null; + } + + const result = Memory.alloc(pointerSize); + jvmti.getMethodDeclaringClass(frames.readPointer(), result); + const klass = result.readPointer(); + try { + jvmti.getClassLoader(klass, result); + return result.readPointer(); + } finally { + env.deleteLocalRef(klass); + } } class JvmMethodMangler { diff --git a/lib/jvmti.js b/lib/jvmti.js index bcae1a80..d96b7cc7 100644 --- a/lib/jvmti.js +++ b/lib/jvmti.js @@ -24,11 +24,26 @@ EnvJvmti.prototype.deallocate = proxy(47, 'int32', ['pointer', 'pointer'], funct return impl(this.handle, mem); }); +EnvJvmti.prototype.getClassLoader = proxy(57, 'int32', ['pointer', 'pointer', 'pointer'], function (impl, klass, classLoaderPtr) { + const result = impl(this.handle, klass, classLoaderPtr); + checkJniResult('EnvJvmti::getClassLoader', result); +}); + +EnvJvmti.prototype.getMethodDeclaringClass = proxy(65, 'int32', ['pointer', 'pointer', 'pointer'], function (impl, method, declaringClassPtr) { + const result = impl(this.handle, method, declaringClassPtr); + checkJniResult('EnvJvmti::getMethodDeclaringClass', result); +}); + EnvJvmti.prototype.getLoadedClasses = proxy(78, 'int32', ['pointer', 'pointer', 'pointer'], function (impl, classCountPtr, classesPtr) { const result = impl(this.handle, classCountPtr, classesPtr); checkJniResult('EnvJvmti::getLoadedClasses', result); }); +EnvJvmti.prototype.getStackTrace = proxy(104, 'int32', ['pointer', 'pointer', 'int', 'int', 'pointer', 'pointer'], function (impl, thread, startDepth, maxFrameCount, frameBuffer, countPtr) { + const result = impl(this.handle, thread, startDepth, maxFrameCount, frameBuffer, countPtr); + checkJniResult('EnvJvmti::getStackTrace', result); +}); + EnvJvmti.prototype.iterateOverInstancesOfClass = proxy(112, 'int32', ['pointer', 'pointer', 'int', 'pointer', 'pointer'], function (impl, klass, objectFilter, heapObjectCallback, userData) { const result = impl(this.handle, klass, objectFilter, heapObjectCallback, userData); checkJniResult('EnvJvmti::iterateOverInstancesOfClass', result); diff --git a/test/Application.mk b/test/Application.mk index 45ad11f9..d5d4b963 100644 --- a/test/Application.mk +++ b/test/Application.mk @@ -1,6 +1,11 @@ include config.mk +# The NDK aborts when APP_PLATFORM names an API level newer than the headers and stubs +# it ships, so clamp to the newest it knows. Harmless, as APP_PLATFORM is a minimum +# rather than a target: a runner built against an older platform runs on a newer one. +ndk_max_api := $(shell sed -n 's/^[[:space:]]*"max":[[:space:]]*\([0-9]*\).*/\1/p' $(ANDROID_NDK_ROOT)/meta/platforms.json 2>/dev/null) + APP_ABI := $(ANDROID_ABI) -APP_PLATFORM := android-$(ANDROID_API_LEVEL) +APP_PLATFORM := android-$(firstword $(sort $(ANDROID_API_LEVEL) $(ndk_max_api))) APP_STL := c++_static APP_BUILD_SCRIPT := Android.mk diff --git a/test/Makefile b/test/Makefile index ea3bada8..caf19860 100644 --- a/test/Makefile +++ b/test/Makefile @@ -9,7 +9,7 @@ deploy_prefix := /data/local/tmp deploy_data_dir := $(deploy_prefix)/frida-java-bridge-tests deploy_cache_dir := $(deploy_data_dir)/dalvik-cache -frida_version := 16.0.1 +frida_version := 17.17.0 test_sources := $(wildcard re/frida/*.java) test_classes := $(patsubst %.java,%.class,$(test_sources)) @@ -27,7 +27,10 @@ deploy: build/$(ANDROID_ABI)/runner build/tests.dex build/frida-java-bridge.js adb push $^ build/$(ANDROID_ABI)/libartpalette.so $(deploy_data_dir) run: - adb shell "LD_PRELOAD=$(ANDROID_VM) LD_LIBRARY_PATH='$(APEX_LIBDIRS):$(deploy_data_dir)' $(deploy_data_dir)/runner $(RUNNER_ARGS)" + @adb logcat -G 16M >/dev/null 2>&1 || true + @adb logcat -c >/dev/null 2>&1 || true + adb shell "LD_PRELOAD=$(ANDROID_VM) LD_LIBRARY_PATH='$(APEX_LIBDIRS):$(deploy_data_dir)' $(deploy_data_dir)/runner $(RUNNER_ARGS)" \ + || { status=$$?; adb logcat -d -b all; exit $$status; } watch: npm run watch & @@ -101,7 +104,7 @@ build/tests.jar: $(test_sources) build/junit.jar build/hamcrest.jar build/junit.jar: @mkdir -p $(@D) - curl -Ls https://github.com/junit-team/junit4/releases/download/r4.12/junit-4.12.jar > $@ + curl -Ls https://repo1.maven.org/maven2/junit/junit/4.13.2/junit-4.13.2.jar > $@ build/hamcrest.jar: @mkdir -p $(@D) diff --git a/test/bundle.js b/test/bundle.js index 22646687..f1adff7a 100644 --- a/test/bundle.js +++ b/test/bundle.js @@ -1,3 +1,3 @@ import LocalJava from 'frida-java-bridge'; -global.LocalJava = LocalJava; +globalThis.LocalJava = LocalJava; diff --git a/test/re/frida/ClassCreationTest.java b/test/re/frida/ClassCreationTest.java index 4022b6de..146f73c0 100644 --- a/test/re/frida/ClassCreationTest.java +++ b/test/re/frida/ClassCreationTest.java @@ -409,8 +409,8 @@ public void classWithUserConstructorsCanBeImplemented() throws ClassNotFoundExce private Script script = null; private void loadScript(String code) { - Script script = new Script(TestRunner.fridaJavaBundle + - ";\n(function (Java) {" + + Script script = new Script(TestRunner.buildScript( + "(function (Java) {" + "Java.perform(function () {" + "Java.classFactory.cacheDir = '" + TestRunner.getCacheDir() + "';" + @@ -421,7 +421,7 @@ private void loadScript(String code) { "').readPointer(), Java.use('java.lang.ClassLoader'));" + code + "});" + - "})(LocalJava);"); + "})(LocalJava);")); this.script = script; } diff --git a/test/re/frida/ClassInitializerTest.java b/test/re/frida/ClassInitializerTest.java new file mode 100644 index 00000000..2c7ae29d --- /dev/null +++ b/test/re/frida/ClassInitializerTest.java @@ -0,0 +1,105 @@ +package re.frida; + +import static org.junit.Assert.assertEquals; + +import org.junit.After; +import org.junit.Test; + +import java.io.IOException; + +public class ClassInitializerTest { + @Test + public void classInitializerCanBeHooked() { + loadScript("var Lazy = Java.use('re.frida.LazilyInitialized');" + + "Lazy.$clinit.implementation = function () {" + + "send('before');" + + "this.$clinit();" + + "send('after');" + + "};"); + + assertEquals("initialized", LazilyInitialized.greeting); + assertEquals("before", script.getNextMessage()); + assertEquals("after", script.getNextMessage()); + } + + @Test + public void classInitializerCanBeReplaced() { + loadScript("var Lazy = Java.use('re.frida.ReplaceablyInitialized');" + + "Lazy.$clinit.implementation = function () {" + + "send('skipped');" + + "};"); + + assertEquals(null, ReplaceablyInitialized.greeting); + assertEquals("skipped", script.getNextMessage()); + } + + @Test + public void classInitializerIsEnumerable() { + loadScript("Java.use('re.frida.EnumerablyInitialized');" + + "var names = [];" + + "Java.enumerateMethods('re.frida.EnumerablyInitialized!*').forEach(function (group) {" + + "group.classes.forEach(function (klass) {" + + "names = names.concat(klass.methods);" + + "});" + + "});" + + "send(names.indexOf('$clinit') !== -1 ? 'found' : names.join(','));"); + + assertEquals("found", script.getNextMessage()); + } + + @Test + public void classWithoutClassInitializerThrows() { + loadScript("var Bare = Java.use('re.frida.NeverInitialized');" + + "try {" + + "Bare.$clinit;" + + "send('no error');" + + "} catch (e) {" + + "send(e.message);" + + "}"); + + assertEquals("re.frida.NeverInitialized has no class initializer", + script.getNextMessage()); + } + + private Script script = null; + + private void loadScript(String code) { + Script script = new Script(TestRunner.buildScript( + "(function (Java) {" + + "Java.perform(function () {" + + code + + "});" + + "})(LocalJava);")); + this.script = script; + } + + private void unloadScript() throws IOException { + if (script != null) { + script.close(); + script = null; + } + } + + @After + public void tearDown() throws IOException { + unloadScript(); + } +} + +// A non-final static field with an initializer is enough to give a class a , +// and reading it is enough to trigger one. Each test needs its own, as a class is only +// ever initialized once. +class LazilyInitialized { + public static String greeting = "initialized"; +} + +class ReplaceablyInitialized { + public static String greeting = "initialized"; +} + +class EnumerablyInitialized { + public static String greeting = "initialized"; +} + +class NeverInitialized { +} diff --git a/test/re/frida/ClassRegistryTest.java b/test/re/frida/ClassRegistryTest.java index fa402e8a..bd47edec 100644 --- a/test/re/frida/ClassRegistryTest.java +++ b/test/re/frida/ClassRegistryTest.java @@ -158,12 +158,12 @@ public void classWrapperShouldSupportExplicitDispose() { private Script script = null; private void loadScript(String code) { - Script script = new Script(TestRunner.fridaJavaBundle + - ";\n(function (Java) {" + + Script script = new Script(TestRunner.buildScript( + "(function (Java) {" + "Java.perform(function () {" + code + "});" + - "})(LocalJava);"); + "})(LocalJava);")); this.script = script; } diff --git a/test/re/frida/MethodTest.java b/test/re/frida/MethodTest.java index 0ec16525..9d4fe9b8 100644 --- a/test/re/frida/MethodTest.java +++ b/test/re/frida/MethodTest.java @@ -582,12 +582,12 @@ private void loadScript(String code) { } private void loadScript(String code, String performMethodName) { - Script script = new Script(TestRunner.fridaJavaBundle + - ";\n(function (Java) {" + + Script script = new Script(TestRunner.buildScript( + "(function (Java) {" + "Java." + performMethodName + "(function () {" + code + "});" + - "})(LocalJava);"); + "})(LocalJava);")); this.script = script; } diff --git a/test/re/frida/Script.java b/test/re/frida/Script.java index 9fefdbaf..1e3ead38 100644 --- a/test/re/frida/Script.java +++ b/test/re/frida/Script.java @@ -35,7 +35,12 @@ public void close() throws IOException { public String getNextMessage() { try { - return pending.poll(5, TimeUnit.SECONDS); + String message = pending.poll(5, TimeUnit.SECONDS); + if (message == null) { + throw new IllegalStateException("timed out waiting for a message from the" + + " script; see stderr and logcat for errors raised by it"); + } + return message; } catch (InterruptedException e) { return getNextMessage(); } @@ -51,7 +56,8 @@ private void onMessage(String rawMessage) { } else if (type.equals("log")) { System.out.println(message.getString("payload")); } else if (type.equals("error")) { - System.err.println(message.getString("stack")); + // Not every error carries a stack, and getString() would throw right past it. + System.err.println(message.optString("stack", rawMessage)); } else { System.err.println(rawMessage); } diff --git a/test/re/frida/TestRunner.java b/test/re/frida/TestRunner.java index 6866dda6..e608e3ea 100644 --- a/test/re/frida/TestRunner.java +++ b/test/re/frida/TestRunner.java @@ -1,9 +1,12 @@ package re.frida; +import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.File; import java.io.FileInputStream; +import static java.nio.charset.StandardCharsets.UTF_8; + import org.junit.runner.JUnitCore; public class TestRunner { @@ -26,10 +29,86 @@ public static void main(String[] args, String dataDir, String cacheDir, JUnitCore.main( "re.frida.ClassRegistryTest", "re.frida.MethodTest", - "re.frida.ClassCreationTest" + "re.frida.ClassCreationTest", + "re.frida.ClassInitializerTest" ); } + // Archive markers, written as escapes rather than literally, as javac is given + // no -encoding and would otherwise read this file in the platform default charset. + private static final String MAGIC = "\uD83D\uDCE6"; // PACKAGE + private static final String END_OF_HEADER = "\u2704"; // BLACK SCISSORS + private static final String ALIAS_PREFIX = "\u21BB"; // CLOCKWISE OPEN CIRCLE ARROW + private static final String BODY_DELIMITER = "\n" + END_OF_HEADER + "\n"; + private static final byte[] DELIMITER = BODY_DELIMITER.getBytes(UTF_8); + + /** + * Wraps the bundle around a snippet of test code, yielding a loadable script. + * + * frida-compile does not emit a plain script but an archive: a header naming + * each module and the exact number of bytes it occupies, followed by the module + * bodies. Code appended to such a file lands past the last body's declared + * extent, where nothing will ever parse it. It has to go inside the entrypoint + * module instead, with the length the header declares for that module fixed up + * to match. A bundle that is a plain script is handled as one. + */ + public static String buildScript(String code) { + String archive = fridaJavaBundle; + + if (!archive.startsWith(MAGIC)) { + return archive + ";\n" + code; + } + + // The first delimiter ends the header; the rest separate one body from the next. + int headerEnd = archive.indexOf(BODY_DELIMITER); + String[] lines = archive.substring(MAGIC.length() + 1, headerEnd).split("\n"); + byte[] region = archive.substring(headerEnd + BODY_DELIMITER.length()).getBytes(UTF_8); + byte[] addition = ("\n" + code).getBytes(UTF_8); + + // Walk the header as far as the entrypoint -- the first module that is not a source + // map -- summing what the modules up to and including it occupy, which is where its + // body ends. Alias lines belong to the module above them and take up no body. + int entrypoint = -1; + int bodyEnd = 0; + int modules = 0; + for (int i = 0; i != lines.length && entrypoint == -1; i++) { + String line = lines[i]; + if (line.startsWith(ALIAS_PREFIX)) { + continue; + } + + if (modules++ != 0) { + bodyEnd += DELIMITER.length; + } + + int space = line.indexOf(' '); + int length = Integer.parseInt(line.substring(0, space)); + bodyEnd += length; + + if (!line.endsWith(".map")) { + entrypoint = i; + lines[i] = (length + addition.length) + line.substring(space); + } + } + + StringBuilder header = new StringBuilder(MAGIC).append('\n'); + for (String line : lines) { + header.append(line).append('\n'); + } + header.append(END_OF_HEADER).append('\n'); + + byte[] preamble = header.toString().getBytes(UTF_8); + + ByteArrayOutputStream out = + new ByteArrayOutputStream(preamble.length + region.length + addition.length); + out.write(preamble, 0, preamble.length); + out.write(region, 0, bodyEnd); + out.write(addition, 0, addition.length); + out.write(region, bodyEnd, region.length - bodyEnd); + + return new String(out.toByteArray(), UTF_8); + } + public static String getCacheDir() { return dataDir; } diff --git a/test/runner.c b/test/runner.c index 9c41e29f..d7e92987 100644 --- a/test/runner.c +++ b/test/runner.c @@ -2,11 +2,14 @@ #include #include #include +#include #include +#include #include typedef struct _CreateScriptOperation CreateScriptOperation; typedef struct _DestroyScriptOperation DestroyScriptOperation; +typedef struct _SigchainAction SigchainAction; struct _CreateScriptOperation { @@ -31,6 +34,14 @@ struct _DestroyScriptOperation GCond cond; }; +/* Must match art::SigchainAction, as ART hands us these by pointer. */ +struct _SigchainAction +{ + bool (* sc_sigaction) (int, siginfo_t *, void *); + sigset_t sc_mask; + guint64 sc_flags; +}; + static void frida_java_init_vm (JavaVM ** vm, JNIEnv ** env, gboolean enable_optimizations); static void frida_java_register_test_runner_api (JNIEnv * env); static void frida_java_register_script_api (JNIEnv * env); @@ -54,6 +65,8 @@ static void destroy_weak_ref (jweak ref); static guint get_system_api_level (void); +static void on_special_signal (int signo, siginfo_t * info, void * context); + static const JNINativeMethod re_frida_test_runner_methods[] = { { "registerClassLoader", "(Ljava/lang/ClassLoader;)V", re_frida_test_runner_register_class_loader }, @@ -78,6 +91,9 @@ static jobject re_frida_test_runner_class_loader; static jmethodID re_frida_script_on_message_method; +static SigchainAction special_handler[NSIG]; +static struct sigaction previous_action[NSIG]; + int main (int argc, char * argv[]) { @@ -544,14 +560,70 @@ SetSpecialSignalHandlerFn (int signal, gpointer fn) /* g_print ("SetSpecialSignalHandlerFn(signal=%d)\n", signal); */ } +/* + * These two are the only part of the chain ART actually uses, and unlike the rest + * they cannot be stubbed out: our definitions shadow the real libsigchain that + * libart.so pulls in, so a handler we drop is a handler ART never installs. + * + * That was survivable while the only casualty was the SIGSEGV handler behind + * implicit null checks, which the tests never trip. It stopped being survivable + * with the userfaultfd-based GC that became the default in API level 35: ART + * registers the heap with UFFD_FEATURE_SIGBUS and depends on servicing the + * resulting SIGBUS itself, so losing that handler kills the process on the first + * compaction, wherever the mutator happens to be. + * + * The handler goes in through plain sigaction(), which Gum's exceptor intercepts + * and chains to, exactly as it would for any other process hosting a VM. + */ + void -AddSpecialSignalHandlerFn (int signal, gpointer sa) +AddSpecialSignalHandlerFn (int signal, SigchainAction * sa) { - /* g_print ("AddSpecialSignalHandlerFn(signal=%d)\n", signal); */ + struct sigaction action; + + if (signal <= 0 || signal >= NSIG || special_handler[signal].sc_sigaction != NULL) + return; + + special_handler[signal] = *sa; + + memset (&action, 0, sizeof (action)); + action.sa_sigaction = on_special_signal; + action.sa_flags = SA_SIGINFO | SA_ONSTACK; + /* + * The kernel installs this mask around the handler and takes it away again on return, + * which is what libsigchain otherwise does by hand. It also blocks the signal being + * delivered, so a handler that faults on its own account cannot arrive back here + * forever: the kernel forces the default action instead, which is the tombstone we + * want rather than a hang. + */ + action.sa_mask = sa->sc_mask; + + sigaction (signal, &action, &previous_action[signal]); } void RemoveSpecialSignalHandlerFn (int signal, bool (* fn) (int, siginfo_t *, void *)) { - /* g_print ("RemoveSpecialSignalHandlerFn(signal=%d)\n", signal); */ + if (signal <= 0 || signal >= NSIG || special_handler[signal].sc_sigaction != fn) + return; + + special_handler[signal].sc_sigaction = NULL; + + sigaction (signal, &previous_action[signal], NULL); +} + +static void +on_special_signal (int signo, siginfo_t * info, void * context) +{ + bool (* handler) (int, siginfo_t *, void *) = special_handler[signo].sc_sigaction; + + if (handler != NULL && handler (signo, info, context)) + return; + + /* + * Nobody claimed it. Put back whatever was installed before us and return, so + * that the faulting instruction runs again and crashes for real, leaving the + * tombstone we would otherwise have swallowed. + */ + sigaction (signo, &previous_action[signo], NULL); }