jackson-databind: JDK 16 Illegal reflective access for `Throwable.setCause()` with `PropertyNamingStrategy.UPPER_CAMEL_CASE`

Describe the bug Mapping a json string to an instance of RuntimeException with JDK 16 (which defaults to denying illegal reflective access) while using UPPER_CAMEL_CASE property naming strategy, fails with:

Exception in thread "main" com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Failed to call `setAccess()` on Method 'setCause' due to `java.lang.reflect.InaccessibleObjectException`, problem: Unable to make final void java.lang.Throwable.setCause(java.lang.Throwable) accessible: module java.base does not "opens java.lang" to unnamed module

This manifests in parts of the AWS v1 SDK when an error is received from the AWS API endpoints. Note example report: https://github.com/FasterXML/jackson-databind/issues/2464#issuecomment-856996757

The simplified reproduce case below is based on what the failing AWS SDK code is doing internally in com.amazonaws.transform.JsonErrorUnmarshaller.unmarshall.

Version information 2.12.3, 2.12.5, 2.13.0-rc2

To Reproduce Execute this code without any --add-opens params and with default --illegal-access setting of deny.

public class Test {
	public static void main(String[] args) throws Exception {
		ObjectMapper mapper = new ObjectMapper();
		mapper.setPropertyNamingStrategy(PropertyNamingStrategies.UPPER_CAMEL_CASE);

		String jsonString = "{\"message\":\"This is my runtime exception message\"}";
		JsonNode jsonContent = mapper.readTree(jsonString);

		throw mapper.treeToValue(jsonContent, RuntimeException.class);
	}
}

Expected behavior It is expected that the code above would throw a RuntimeException with a message of “This is my runtime exception message”. This is what happens when working around the issue with java parameter --add-opens java.base/java.lang=ALL-UNNAMED

Additional context Using java parameter --illegal-access=debug results in the following additional info:

WARNING: Illegal reflective access by com.fasterxml.jackson.databind.util.ClassUtil (file:/xxxxxx/jackson-databind-2.13.0-rc2.jar) to method java.lang.Throwable.setCause(java.lang.Throwable)
	at com.fasterxml.jackson.databind.util.ClassUtil.checkAndFixAccess(ClassUtil.java:994)
	at com.fasterxml.jackson.databind.introspect.AnnotatedMember.fixAccess(AnnotatedMember.java:139)
	at com.fasterxml.jackson.databind.deser.impl.MethodProperty.fixAccess(MethodProperty.java:95)
	at com.fasterxml.jackson.databind.deser.BeanDeserializerBuilder._fixAccess(BeanDeserializerBuilder.java:522)
	at com.fasterxml.jackson.databind.deser.BeanDeserializerBuilder.build(BeanDeserializerBuilder.java:373)
	at com.fasterxml.jackson.databind.deser.BeanDeserializerFactory.buildThrowableDeserializer(BeanDeserializerFactory.java:455)
	at com.fasterxml.jackson.databind.deser.BeanDeserializerFactory.createBeanDeserializer(BeanDeserializerFactory.java:112)
	at com.fasterxml.jackson.databind.deser.DeserializerCache._createDeserializer2(DeserializerCache.java:415)
	at com.fasterxml.jackson.databind.deser.DeserializerCache._createDeserializer(DeserializerCache.java:350)
	at com.fasterxml.jackson.databind.deser.DeserializerCache._createAndCache2(DeserializerCache.java:264)
	at com.fasterxml.jackson.databind.deser.DeserializerCache._createAndCacheValueDeserializer(DeserializerCache.java:244)
	at com.fasterxml.jackson.databind.deser.DeserializerCache.findValueDeserializer(DeserializerCache.java:142)
	at com.fasterxml.jackson.databind.DeserializationContext.findRootValueDeserializer(DeserializationContext.java:642)
	at com.fasterxml.jackson.databind.ObjectMapper._findRootDeserializer(ObjectMapper.java:4751)
	at com.fasterxml.jackson.databind.ObjectMapper._readValue(ObjectMapper.java:4596)
	at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:2815)
	at com.fasterxml.jackson.databind.ObjectMapper.treeToValue(ObjectMapper.java:3279)
	at Test.main(Test.java:13)

Note that the issue does not manifest without mapper.setPropertyNamingStrategy(PropertyNamingStrategies.UPPER_CAMEL_CASE);

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Reactions: 9
  • Comments: 16 (9 by maintainers)

Commits related to this issue

Most upvoted comments

@yawkat Ok. Just didn’t see it in JDK javadocs; I guess only public (and maybe protected methods are included).

@gsinghlulu yes, correct on both accounts. I think fix is along the right lines; will see if we could first remove setCause() if it exists (or replace it). I am only hesitant about looking at Cause in case other translations might miss it. But it might be good enough too, either way thank you for providing it!

Ok, yes, JDK 12 added Throwable.setCause() apparently.

This specific issue now fixed in 2.13 (for 2.13.4, then 2.14.0). But I think there is the follow-up issue of PropertyNamingStrategy not working for Throwable, will file separate issue for that.

@gsinghlulu ah! Ok, I should paid closer attention there. Ok I can see how things might go there… hmmh. This does seem not like the optimal way to resolve the problem, but I can see how it would actually fix the issue here. The only (?) thing that is confusing is where the upper-case Cause comes from – I could see it being result of NamingStrategy but not sure if that could yet have been applied.

… but perhaps it is, as you mentioned, from Field that gets renamed (initCause() won’t because we are just adding it).

I think I will have another look here: thank you for providing the potential fix, as well as explaining it – and apologies for misunderstanding it first.

Looking at the original report, the issue is wrt attempts to deserialize an Exception, during which setCause() needs to be accessed; and for that to work, access must be forced.

I am not quite sure how to proceed with this: my first instinct is to try to suppress the failure (well, very first thing is JDK17-specific test case but after that) for this specific setter. But that in turn will probably expose the problem of not being able to chain root cause exceptions on JDK 17 and later…