Skip to content

Java 24/25 support: recompute stale StackMapTables + JEP 486 launch#193

Open
GribanovIvan wants to merge 3 commits into
CrucibleMC:stagingfrom
GribanovIvan:feature/java25-support
Open

Java 24/25 support: recompute stale StackMapTables + JEP 486 launch#193
GribanovIvan wants to merge 3 commits into
CrucibleMC:stagingfrom
GribanovIvan:feature/java25-support

Conversation

@GribanovIvan

Copy link
Copy Markdown
Contributor

Java 24/25 support

Lets Crucible launch and run a Mixin-heavy modpack on Java 24/25. Verified by building this branch (Java 8) and booting a large GTNH-style pack (~62 mods, incl. unimixins + lwjgl3ify + Galacticraft + Dynmap) to Done on Oracle GraalVM 25 with the bytecode verifier on (0 VerifyErrors).

Commit 1 — Recompute stale StackMapTables

A SpongePowered Mixin (via unimixins) rewrites a net.minecraft method with COMPUTE_MAXS only, leaving its StackMapTable stale. The split verifier then rejects it with VerifyError: Expecting a stackmap frame at branch target N — e.g. ChunkProviderServer.func_73153_a when Dynmap reflectively scans its fields in postInit — so the server can't start. Measured on every modern JDK tested (21–25); Java 8 is unaffected (its verifier fails over to the old type-inference verifier).

-Xverify:none also works but drops verification for every class. This is the surgical alternative: a transformer (AsmFrameSafetyTransformer, registered last on the LaunchClassLoader via CrucibleCoremodHook) force-recomputes frames for net.minecraft.* with a deobf-aware, never-throwing SafeAsmClassWriter, keeping the verifier on. RFB's rfb-asm-safety only helps transformers that already recompute; it doesn't force a recompute on a class whose stale frame a COMPUTE_MAXS-only pass baked in.

Toggles: -Dcrucible.frameSafety=false to disable; -Dcrucible.frameSafety.scope=all (or a prefix list) to widen beyond net.minecraft.

Commit 2 — Java 24+ launch plumbing

  • FMLTweaker: on Java 24+ System.setSecurityManager() always throws UnsupportedOperationException (JEP 486); FMLTweaker only caught SecurityException and crashed. Catch it, log, continue (the FML SM only trapped rogue System.exit()/SM-replacement).
  • java24args.txt: java9args.txt minus -Djava.security.manager=allow (makes a Java 24+ JVM refuse to start) and the obsolete --illegal-access=warn.
  • README: supported range 8–218–25.

🤖 Sorry for generating it with Claude Code, one day I shall learn java

GribanovIvan and others added 2 commits June 25, 2026 16:11
…JDKs

When a SpongePowered Mixin (e.g. via unimixins) rewrites a net.minecraft method,
it recomputes only the operand-stack/local sizes (COMPUTE_MAXS) and leaves the
method's StackMapTable stale. The split bytecode verifier rejects that with
"VerifyError: Expecting a stackmap frame at branch target N" the moment the class
is verified -- e.g. net.minecraft.world.gen.ChunkProviderServer.func_73153_a when
Dynmap reflectively scans its fields during postInit -- so the server cannot
start.

Measured on the lwjgl3ify server path with a large GTNH-style pack: this happens
on every modern JDK tested (21, 22, 23, 24, 25). Java 8 is unaffected because its
verifier fails over to the old type-inference verifier for these class files. A
bare server is unaffected; only packs whose Mixins touch net.minecraft hit it.

Disabling the verifier (-Xverify:none) also lets the pack start but drops bytecode
verification for every class. This is the surgical alternative: repair the
affected frames and keep the verifier on.

RFB's rfb-asm-safety plugin only makes getCommonSuperClass safe for transformers
that already recompute frames; it does not force a recompute on a class whose
stale frame was baked in by a COMPUTE_MAXS-only pass.

Add AsmFrameSafetyTransformer, registered last on the LaunchClassLoader via
CrucibleCoremodHook, which recomputes frames (COMPUTE_FRAMES) for net.minecraft.*
classes at load time using a deobf-aware, never-throwing ClassWriter
(SafeAsmClassWriter). Implementation note: ClassWriter(reader, flags) copies
unchanged methods verbatim and skips COMPUTE_FRAMES; super(flags) without the
reader forces a real recompute.

Toggles: -Dcrucible.frameSafety=false disables it;
-Dcrucible.frameSafety.scope=all (or a comma-separated prefix list) widens beyond
net.minecraft.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three small changes that let Crucible launch on Java 24/25, complementing the
StackMapTable fix:

* FMLTweaker: on Java 24+ System.setSecurityManager() always throws
  UnsupportedOperationException (JEP 486). FMLTweaker only caught
  SecurityException, so it crashed at startup. Catch the new exception, log it,
  and continue -- the FML SecurityManager only trapped rogue System.exit() and SM
  replacement, neither essential.

* java24args.txt: a Java 24+ launch-args file. It is java9args.txt without
  -Djava.security.manager=allow (which makes a Java 24+ JVM refuse to start) and
  the obsolete --illegal-access=warn (ignored since Java 17).

* README: bump the advertised supported range from "Java 8-21" to "Java 8-25".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 25, 2026 16:14

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@gravit0

gravit0 commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

May be we can move "-Dcrucible.frameSafety=false" and "-Dcrucible.frameSafety.scope=all" to Crucible config?

Per review feedback (gravit0): expose the StackMapTable-recompute toggle and its
class scope as Crucible.yml options instead of system-properties-only.

- crucible.asm.frameSafety (bool, default true) — enable the recompute transformer.
- crucible.asm.frameSafetyScope (string, default "net.minecraft.") — prefix filter;
  "all" recomputes everything.

The system properties still work and OVERRIDE the config when set, so CI/launch
scripts can flip them without editing the yml. Debug-only props
(crucible.frameSafety.debug/.debugClass) stay system-property-only.

CrucibleCoremodHook reads the config the same way the rest of the bootstrap
package does (Lwjgl3ifyIntegration) — a direct CrucibleConfigs.configs reference;
crucible.jar is the launch -jar so the class resolves. The transformer reads its
scope the same way.

@gravit0 gravit0 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'd like to know what problem PR solves (specifically, working with ASM). I remember there was a problematic asjcore mod with a similar issue to the one described above: https://pastebin.com/TTGVr223

@@ -9,5 +9,15 @@
+ sm.defman = System.getSecurityManager();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

On Java 8 this code will execute correctly, and on Java 9+ (lwjgl3ify) lwjgl3ify will redirect this call to itself and the problem will not occur.


@SuppressWarnings("unchecked")
private void ensureRunsLast() {
final Field f = TRANSFORMERS_FIELD;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is quite risky code with reflection. Even though we guarantee that the launchwrapper implementation is RFB and we can change this code if it changes, it's still a dirty hack.

if (dbg) dump(transformedName + ".pre", basicClass);
try {
ClassReader reader = new ClassReader(basicClass);
SafeAsmClassWriter writer = new SafeAsmClassWriter(reader, ClassWriter.COMPUTE_FRAMES);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We can use com.gtnewhorizons.retrofuturabootstrap.asm.SafeAsmClassWriter. Why do we need to re-implement it here?

// path: the full pack fails this way on every modern JDK tested (21-25); Java 8 is unaffected
// because its verifier fails over to the old type-inference verifier for these class files.
// -Dcrucible.frameSafety=true|false overrides the Crucible.yml value when set.
String frameSafetyProp = System.getProperty("crucible.frameSafety");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why do we need to use getProperty instead of CrucibleConfig?

Comment thread java24args.txt
@@ -0,0 +1,36 @@
-Dfile.encoding=UTF-8

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think we should just remove "-Djava.security.manager=allow" from java9args.txt without creating a new file. lwjgl3ify will already patch the setSecurityManager call.

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