MigrateSecurityManagerMulticast: only migrate provably equivalent calls - #1193
Draft
martinfrancois wants to merge 3 commits into
Draft
Conversation
The recipe rewrote every `checkMulticast(InetAddress, byte)` call to the one argument overload by dropping the second argument, which broke two contracts. Java evaluates an argument before the call, so the side effects and exceptions of the discarded time to live expression disappeared, silently losing an increment, a method invocation, an array access, or the `NullPointerException` from unboxing a null `Byte`. The two overloads are also separate virtual methods that a `SecurityManager` subclass may override independently, so the rewrite changed which method runs whenever the receiver held a subclass. A call is now migrated only when both hazards are ruled out. The receiver must be a direct `new SecurityManager()` allocation without an anonymous class body, the only shape whose runtime class is known here; for `java.lang.SecurityManager` itself both overloads are specified to call `checkPermission` with the same `SocketPermission`, and the two argument form is documented not to use the time to live. The discarded argument must be a literal, a primitive to primitive cast of one, or a read of a primitive local or parameter. Every other receiver, including `super` calls and anonymous subclasses, and every other argument shape, is left unchanged. The migrated call also references the declared `checkMulticast(InetAddress)` overload resolved from the declaring type, rather than a method type trimmed down from the two argument overload, which carried over the latter's deprecation metadata. Two points for review. Calls through a variable of static type `SecurityManager` are no longer migrated at all, which is deliberate: their runtime class cannot be established, so no check on the discarded argument can make the switch safe. The existing test and the generated example now declare `throws Exception`, because their `InetAddress.getByName` call is checked and both sides of the example have to compile. The recipe description and recipes.csv state the new constraint.
4 tasks
martinfrancois
marked this pull request as draft
August 17, 2026 08:08
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Suggested review order: 22 of 52 (Score: 6)
Review first: openrewrite/rewrite#8446
What's changed?
SecurityManagerdeclares two overloads ofcheckMulticast: the deprecatedcheckMulticast(InetAddress maddr, byte ttl), whose second parameter is a time to live, andcheckMulticast(InetAddress maddr), which has none.MigrateSecurityManagerMulticastrewrites a call to the first into a call to the second, so the time to live argument disappears from the source and is no longer evaluated at run time.The recipe now performs that rewrite only when both of the following hold, and otherwise leaves the call exactly as it is:
new SecurityManager()allocation, with no anonymous class body, so that virtual dispatch cannot reach an override of either overload in a subclass.(byte) 100and(byte) localTtlare accepted, and a cast is never required.A second, smaller change concerns the method type attached to a migrated call. On main that type is synthesised by trimming the two argument overload's parameter names and types down to one, so the call keeps that overload's remaining data, including its deprecation flag, and anything inspecting it later still sees a deprecated method. The recipe now looks the declared
checkMulticast(InetAddress)overload up on the declaring type, the wayStringFormatteddoes in this repository, and leaves the call unchanged when it finds none. It touches the same few lines, so it is bundled here; say the word and I will split it out. The recipe description now states the new limits.What's your motivation?
Recipe:
org.openrewrite.java.migrate.lang.MigrateSecurityManagerMulticast.Before
Actual after the recipe
Expected after the recipe
(unchanged)
The two overloads are different virtual methods, and on main the recipe replaces the first with the second on every match, with no check on the receiver and no check on the argument. The rewritten code still compiles, and the program behaves differently in three ways:
ArrayIndexOutOfBoundsExceptionfrom an array access or aNullPointerExceptionfrom unboxing a nullByte.SecurityManagersubclass that overrides only the two argument form, that override no longer runs, so a project's own security check is skipped.Reproduced on 3.41.0 and on 3.42.0-SNAPSHOT built from main. The recipe ships inside
JavaLangAPIs, so it also runs as part ofJava8toJava11,UpgradeToJava17,UpgradeToJava21andUpgradeToJava25.Confirmed real-world executions
All three executions used
org.openrewrite.recipe:rewrite-migrate-java:3.42.0.MulticastSocket.javaatadf16cc8MulticastSocket.javaatf9d1ef52NetMulticastSocket.javaat67bb3596The released recipe removes the
ttlargument from each two-argument call. These calls use a variable receiver, so the replacement changes overload dispatch and stops evaluating the discarded argument.Anything in particular you'd like reviewers to focus on?
One pre-existing test changed.
migrateCheckMulticast, the@DocumentExample, gainedthrows Exceptionin both its before and after sources: both declaredpublic void method() {while callingInetAddress.getByName, which declares the checked exceptionUnknownHostException, so javac rejects either source compiled on its own. The test framework type attributes these snippets rather than running flow analysis over them, which is why the test passed on main all the same, so that defect is pre-existing in the test source. What the test asserts is otherwise unchanged, and that case still migrates.examples.ymlandrecipes.csvare generated files checked into the repository, and their changed lines are regenerated content: the samethrows Exceptionin the@DocumentExamplesources, and the single row for this recipe, carrying the extended recipe description.Limits this creates, all cases that main migrated and this branch leaves unchanged:
System.getSecurityManager()no longer migrate, since only a directnew SecurityManager()receiver satisfies the first condition.super.checkMulticast(maddr, ttl)call inside aSecurityManagersubclass, covered byretainCallDelegatingToSuper, is never migrated either, because its receiver issuperrather than an allocation. Asupercall is not dispatched virtually, so migrating it would in fact have been safe. That is an accepted cost of the deliberately coarse receiver condition, not a defect in it.Byteand astatic final byteconstant field are both left unmigrated:Byteis not a primitive type, and a field is neither a local variable nor a parameter. Instance fields, includingvolatileones, likewise.Have you considered any alternatives or workarounds?
The receiver condition is what makes the recipe narrow. Three options, any of which I will take:
SecurityManagersubclass thatoverrides only one of the two overloads is too rare to protect against. That is deleting the single
cannotDispatchToAnOverride(m.getSelect()) &&condition atMigrateSecurityManagerMulticast.java:62and the four tests covering it:retainCallWhenReceiverMayBeASubclass,retainCallWhenAnOverloadIsOverridden,retainCallDelegatingToSuperandretainCallOnAnonymousSubclass.SecurityManageris deprecated for removal.Tell me which and I will change this pull request.
Any additional context
Pre-existing tests changed:
MigrateSecurityManagerMulticastTest.java.migrateCheckMulticast(updated).This change adds 6 test methods to
MigrateSecurityManagerMulticastTest, taking the focused class from 1 to 7 executions. They cover 13 scenarios. Without the code change in this pull request, 5 added methods fail and represent 12 failing scenarios. Four methods cover receivers that may dispatch to an override.retainPotentiallyObservableArgumentscontains 8 discarded-argument shapes, including mutation, unboxing, field reads, conditionals, switch expressions, division and allocation.migrateWhenReceiverIsExactAndTimeToLiveIsConstantOrLocalpasses either way and pins down the cases that this change keeps migrating.This change was prepared with AI assistance (Claude Code). I reviewed the code, the tests and this description.
I ran the formatter with the repository's
.editorconfig. It also wanted to re-indent lines that this change does not touch, so I left those alone and kept the diff limited to this change.Checklist
./gradlew buildlocally, and committed any resulting changes torecipes.csv