Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TypeDescriptor with recursive generics triggers infinite recursion in ResolvableType.equals/hashCode #33932

Closed
juancarrey opened this issue Nov 21, 2024 · 5 comments
Assignees
Labels
in: core Issues in core modules (aop, beans, core, context, expression) type: bug A general bug
Milestone

Comments

@juancarrey
Copy link

juancarrey commented Nov 21, 2024

During SpEL evaluation, if the TypeDescriptor is recursive, the evaluation results in infinite recursion causing stack overflow.

This line:

ObjectUtils.nullSafeEquals(getMapValueTypeDescriptor(), otherDesc.getMapValueTypeDescriptor()));

Evaluated object sample where we have this issue is a map:

public class MyClass implements Map<String, MyClass>

This causes the evaluation of equality to check the type, and recursively the inner types of the map.

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Nov 21, 2024
@sbrannen sbrannen changed the title Infinite recurssion in sPEL for Types whose TypeDefinition is recursive Infinite recursion in SpEL for types with recursive TypeDefinition Nov 21, 2024
@sbrannen sbrannen added the in: core Issues in core modules (aop, beans, core, context, expression) label Nov 21, 2024
@sbrannen sbrannen changed the title Infinite recursion in SpEL for types with recursive TypeDefinition Infinite recursion in SpEL for type with recursive TypeDescriptor Nov 21, 2024
@sbrannen
Copy link
Member

Hi @juancarrey,

Congratulations on submitting your first issue for the Spring Framework! 👍

Unfortunately you have not provided enough information for us to reproduce the issue.

If you would like us to investigate this, please provide a minimal example that we can run -- for example, a stand-alone JUnit test class or a minimal sample application made available via a Git repository or a ZIP file attached to this issue.

Thanks


In any case, it appears that this may be an issue with TypeDescriptor in spring-core and therefore not specific to SpEL.

@sbrannen sbrannen self-assigned this Nov 21, 2024
@sbrannen sbrannen added the status: waiting-for-feedback We need additional information before we can continue label Nov 21, 2024
@sbrannen
Copy link
Member

sbrannen commented Nov 21, 2024

In any case, it appears that this may be an issue with TypeDescriptor in spring-core and therefore not specific to SpEL.

Indeed, the following results in a StackOverflowError.

class RecursiveTypeDescriptorTests {

	@Test
	void recursiveTypeDescriptor() {
		TypeDescriptor typeDescriptor1 =
				TypeDescriptor.map(Map.class,
						TypeDescriptor.valueOf(String.class),
						TypeDescriptor.valueOf(RecursiveMap.class));
		TypeDescriptor typeDescriptor2 =
				TypeDescriptor.map(Map.class,
						TypeDescriptor.valueOf(String.class),
						TypeDescriptor.valueOf(RecursiveMap.class));
		assertThat(typeDescriptor1).isEqualTo(typeDescriptor2);
	}


	static class RecursiveMap extends HashMap<String, RecursiveMap> {
	}

}

That results in a stack trace like this:

java.lang.StackOverflowError
	at org.springframework.util.ObjectUtils.nullSafeEquals(ObjectUtils.java:346)
	at org.springframework.core.ResolvableType.equals(ResolvableType.java:1023)
	at org.springframework.util.ObjectUtils.nullSafeEquals(ObjectUtils.java:342)
	at org.springframework.core.ResolvableType.equals(ResolvableType.java:1023)
	at org.springframework.util.ObjectUtils.nullSafeEquals(ObjectUtils.java:342)
	at org.springframework.core.ResolvableType.equals(ResolvableType.java:1023)
	at org.springframework.util.ObjectUtils.nullSafeEquals(ObjectUtils.java:342)
	at org.springframework.core.ResolvableType.equals(ResolvableType.java:1023)
	at org.springframework.util.ObjectUtils.nullSafeEquals(ObjectUtils.java:342)

But if we change the declaration of RecursiveMap to the following:

static class RecursiveMap extends HashMap<String, RecursiveMap>
	implements Map<String, RecursiveMap> {
}

... we then see a stack trace like this:

java.lang.StackOverflowError
	at org.springframework.util.ObjectUtils.nullSafeHashCode(ObjectUtils.java:452)
	at org.springframework.core.ResolvableType.calculateHashCode(ResolvableType.java:1056)
	at org.springframework.core.ResolvableType.hashCode(ResolvableType.java:1044)
	at org.springframework.util.ObjectUtils.nullSafeHashCode(ObjectUtils.java:452)
	at org.springframework.core.ResolvableType.calculateHashCode(ResolvableType.java:1056)
	at org.springframework.core.ResolvableType.hashCode(ResolvableType.java:1044)
	at org.springframework.util.ObjectUtils.nullSafeHashCode(ObjectUtils.java:452)
	at org.springframework.core.ResolvableType.calculateHashCode(ResolvableType.java:1056)
	at org.springframework.core.ResolvableType.hashCode(ResolvableType.java:1044)

Note the recursion in ResolvableType.equals vs. ResolvableType.hashCode.

@sbrannen sbrannen added type: bug A general bug and removed status: waiting-for-feedback We need additional information before we can continue status: waiting-for-triage An issue we've not yet triaged or decided on labels Nov 21, 2024
@sbrannen sbrannen removed their assignment Nov 21, 2024
@sbrannen sbrannen added this to the 6.2.1 milestone Nov 21, 2024
@sbrannen
Copy link
Member

sbrannen commented Nov 21, 2024

We like to avoid infinite recursion in such scenarios, potentially by checking upfront if the generics are equal without recursing, or potentially by tracking which types have been visited and throwing an exception if a cycle is detected.

Tentatively assigned to 6.2.1, but may be backported to 6.1.x depending on the solution.

Note: there might be recursion issues in TypeDescriptor as well as in ResolvableType. So, we should write tests for recursive generics with both.

@sbrannen sbrannen changed the title Infinite recursion in SpEL for type with recursive TypeDescriptor Infinite recursion for equals() and hashCode() in recursive TypeDescriptor Nov 21, 2024
@sbrannen sbrannen changed the title Infinite recursion for equals() and hashCode() in recursive TypeDescriptor Infinite recursion in equals() and hashCode() for ResolvableType with recursive generics Nov 21, 2024
@zhanyan-Ader1y
Copy link

We like to avoid infinite recursion in such scenarios, potentially by checking upfront if the generics are equal without recursing, or potentially by tracking which types have been visited and throwing an exception if a cycle is detected.

Tentatively assigned to 6.2.1, but may be backported to 6.1.x depending on the solution.

Note: there might be recursion issues in TypeDescriptor as well as in ResolvableType. So, we should write tests for recursive generics with both.

Hi, I used the code in your comment to try to analyze the cause of this problem.
Hope it will be helpful for the next repair.

MyCode:


  /**
   * <pre>
   *  On this test case, stackoverflow start at {@link org.springframework.core.ResolvableType#equals}(ResolvableType.java:1023).
   *  <blockquote><pre>
   *  public boolean equals(@Nullable Object other) {
   *  ...
   *      if (...
   *        !ObjectUtils.nullSafeEquals(
   *            this.variableResolver.getSource(),
   *            otherType.variableResolver.getSource()
   *        ))) {
   * 		  return false;
   * 	  }
   *  }
   *  </pre></blockquote>
   *  The reason for infinite recursion is {@link ResolvableType.VariableResolver#getSource()} return cycle object likes:
   *  <blockquote><pre>
   *      class A extends HashMap<A, B>{}
   *  </pre></blockquote>
   *  or:
   *  <blockquote><pre>
   *      class C extends ArrayList<C>{}
   *  </pre></blockquote>
   * </pre>
   */
  @Test
  void test1() {
    TypeDescriptor typeDescriptor1 =
        TypeDescriptor.map(
            Map.class,
            TypeDescriptor.valueOf(String.class),
            TypeDescriptor.valueOf(RecursiveMap.class));
    TypeDescriptor typeDescriptor2 =
        TypeDescriptor.map(
            Map.class,
            TypeDescriptor.valueOf(String.class),
            TypeDescriptor.valueOf(RecursiveMap.class));
    ObjectUtils.nullSafeEquals(typeDescriptor1, typeDescriptor2);
  }

  /**
   * <pre>
   *  Why does this object implement the Map interface and throw diff stacktrace?
   *  I think this main reason same of test1, but some different in {@link ResolvableType#as(Class)}(call on {@link org.springframework.core.convert.TypeDescriptor#equals} ---> {@link TypeDescriptor#getMapKeyTypeDescriptor()}):
   *  <blockquote><pre>
   *  for (ResolvableType interfaceType : getInterfaces()) {
   * 	ResolvableType interfaceAsType = interfaceType.as(type);
   * 		if (interfaceAsType != NONE) {
   * 			return interfaceAsType;
   * 		}
   * 	}
   *  </pre></blockquote>
   *  focus on {@link ResolvableType#getInterfaces()}
   *  <blockquote><pre>
   *  	public ResolvableType[] getInterfaces() {
   * 		ResolvableType[] interfaces = this.interfaces;
   * 		if (interfaces == null) {
   * 			Type[] genericIfcs = resolved.getGenericInterfaces();
   * 			if (genericIfcs.length > 0) {
   * 				interfaces = new ResolvableType[genericIfcs.length];
   * 				for (int i = 0; i < genericIfcs.length; i++) {
   * 			        //  this forType function will use new ResolvableType(type, typeProvider, variableResolver)
   * 			        //  ResolvableType.calculateHashCode in this constructor
   * 					interfaces[i] = forType(genericIfcs[i], this);
   * 				}
   * 			}
   * 		......
   *  </pre></blockquote>
   *
   *  </pre>
   *
   *
   */
  @Test
  void test2() {
    TypeDescriptor typeDescriptor1 =
        TypeDescriptor.map(
            Map.class,
            TypeDescriptor.valueOf(String.class),
            TypeDescriptor.valueOf(RecursiveMap2.class));
    TypeDescriptor typeDescriptor2 =
        TypeDescriptor.map(
            Map.class,
            TypeDescriptor.valueOf(String.class),
            TypeDescriptor.valueOf(RecursiveMap2.class));
    ObjectUtils.nullSafeEquals(typeDescriptor1, typeDescriptor2);
  }

  static class RecursiveMap extends HashMap<String, RecursiveMap> {}

  static class RecursiveMap2 extends HashMap<RecursiveMap2, String>
      implements Map<RecursiveMap2, String> {}

  //  throw STOF
  static class RecursiveList extends ArrayList<RecursiveList>{

  }

@jhoeller jhoeller self-assigned this Dec 10, 2024
@jhoeller jhoeller changed the title Infinite recursion in equals() and hashCode() for ResolvableType with recursive generics TypeDescriptor triggers infinite recursion in equals() and hashCode() for ResolvableType with recursive generics Dec 10, 2024
@jhoeller jhoeller changed the title TypeDescriptor triggers infinite recursion in equals() and hashCode() for ResolvableType with recursive generics TypeDescriptor with recursive generics triggers infinite recursion in ResolvableType.equals/hashCode Dec 10, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: core Issues in core modules (aop, beans, core, context, expression) type: bug A general bug
Projects
None yet
Development

No branches or pull requests

5 participants