Skip to content

Fix ExecutableMatcher to enforce invocation constraints for constructors#10

Merged
Howard20181 merged 6 commits intodocsfrom
copilot/sub-pr-2
Feb 10, 2026
Merged

Fix ExecutableMatcher to enforce invocation constraints for constructors#10
Howard20181 merged 6 commits intodocsfrom
copilot/sub-pr-2

Conversation

Copy link
Copy Markdown

Copilot AI commented Feb 10, 2026

ExecutableMatcher applies to both methods and constructors, but setInvokedMethods() and setInvokedConstructors() constraints were only enforced when matching methods. Constructor matches silently ignored these constraints.

Root Cause

Three related issues:

  • Data structures keyed by Method: Maps used Method as keys instead of Executable, preventing constructor data storage
  • DEX analysis skipped constructors: Analysis always called getDeclaredMethod(), failing to resolve <init> methods as Constructor objects
  • Constraint checks gated on instanceof Method: doMatch() only checked constraints when reflect instanceof Method

Changes

  • Changed methodInvocationsMap and constructorInvocationsMap to key by Executable
  • Updated DEX analysis to detect <init> and call getDeclaredConstructor() appropriately
  • Removed instanceof Method guard from constraint checks in doMatch()
  • Updated getInvokedMethods() and getInvokedConstructors() to handle both executable types
  • Extracted asExecutable() helper to eliminate type-casting duplication

Example

Before: constraints silently ignored for constructors

// This would match ANY constructor, ignoring the constraint
builder.constructor()
    .setInvokedMethods(builder.exactMethod("...").observe())

After: constraints properly enforced for both

// Now correctly filters constructors that invoke the specified method
builder.constructor()
    .setInvokedMethods(builder.exactMethod("...").observe())

💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot AI and others added 5 commits February 10, 2026 12:13
…ods and constructors

Co-authored-by: Howard20181 <40033067+Howard20181@users.noreply.github.com>
Co-authored-by: Howard20181 <40033067+Howard20181@users.noreply.github.com>
…ly once

Co-authored-by: Howard20181 <40033067+Howard20181@users.noreply.github.com>
Co-authored-by: Howard20181 <40033067+Howard20181@users.noreply.github.com>
Co-authored-by: Howard20181 <40033067+Howard20181@users.noreply.github.com>
Copilot AI changed the title [WIP] Address feedback on Gradle and AGP upgrade implementation Fix ExecutableMatcher to enforce invocation constraints for constructors Feb 10, 2026
Copilot AI requested a review from Howard20181 February 10, 2026 12:19
@Howard20181 Howard20181 marked this pull request as ready for review February 10, 2026 13:29
Copilot AI review requested due to automatic review settings February 10, 2026 13:29
@Howard20181 Howard20181 merged commit 6ebaca5 into docs Feb 10, 2026
2 checks passed
@Howard20181 Howard20181 deleted the copilot/sub-pr-2 branch February 10, 2026 13:29
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes ExecutableMatcher so that invocation constraints (setInvokedMethods() / setInvokedConstructors()) are enforced consistently for both methods and constructors, by ensuring DEX invocation relationships can be stored and queried for either executable type.

Changes:

  • Switched invocation relationship maps to be keyed by Executable instead of Method.
  • Updated DEX analysis reflection resolution to treat "<init>" as a constructor (getDeclaredConstructor()).
  • Removed method-only gating so invocation constraints are evaluated for constructors as well.
Comments suppressed due to low confidence (2)

helper/src/main/java/io/github/libxposed/helper/HookBuilderImpl.java:667

  • The invocation-collection block after resolving currentExecutable is indented as if it were still inside the inner try/catch, which makes the control flow hard to follow. Consider reformatting / adjusting indentation (and possibly extracting the resolution + collection into small helpers) so the outer vs inner try scopes are visually clear.
                                        // Try to resolve current method or constructor via reflection
                                        Executable currentExecutable;
                                        try {
                                            var currentClass = reflector.loadClass(currentClassName);
                                            var currentParamTypes = getParameterTypesFromProto(currentProto);
                                            if (currentParamTypes == null) {
                                                return;
                                            }
                                            // Check if it's a constructor
                                            if ("<init>".equals(currentMethodName)) {
                                                currentExecutable = currentClass.getDeclaredConstructor(currentParamTypes);
                                            } else {
                                                currentExecutable = currentClass.getDeclaredMethod(currentMethodName, currentParamTypes);
                                            }
                                        } catch (ClassNotFoundException | NoSuchMethodException e) {
                                            return;
                                        }

                                            // Collect all methods and constructors invoked by this method
                                            Set<Method> invokedMethodsSet = new HashSet<>();
                                            Set<Constructor<?>> invokedConstructorsSet = new HashSet<>();

helper/src/main/java/io/github/libxposed/helper/HookBuilderImpl.java:1748

  • Potentially confusing name: method doMatch also refers to field parameterCount (as this.parameterCount).
            final int parameterCount;

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1760 to 1771
// Cast once for constraint checking (only needed if constraints are present)
// At this point, reflect is guaranteed to be either Method or Constructor
// because line 1754 returns false if it's neither
Executable currentExecutable = null;
if (invokedMethods != null || invokedConstructors != null) {
currentExecutable = asExecutable(reflect);
}

// Check invoked methods constraint
if (invokedMethods != null && reflect instanceof Method) {
var currentMethod = (Method) reflect;
var invokedMethodsSet = methodInvocationsMap.get(currentMethod);
if (invokedMethods != null) {
var invokedMethodsSet = methodInvocationsMap.get(currentExecutable);
if (invokedMethodsSet == null || invokedMethodsSet.isEmpty()) {
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

currentExecutable is nullable and is later used as the key for ConcurrentHashMap.get(...), which will throw if it ever becomes null (CHM disallows null keys). Since this method already proves reflect is a Method or Constructor, consider assigning Executable currentExecutable = (Executable) reflect; (or make asExecutable() non-null / assert) to remove the nullable path and avoid future refactors accidentally introducing an NPE.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants