Skip to content

Fix generics stack leak that poisoned reused Kryo instances#1283

Merged
theigl merged 1 commit into
EsotericSoftware:masterfrom
Icesource:fix/reset-generics-stack-on-exception
Jun 17, 2026
Merged

Fix generics stack leak that poisoned reused Kryo instances#1283
theigl merged 1 commit into
EsotericSoftware:masterfrom
Icesource:fix/reset-generics-stack-on-exception

Conversation

@Icesource

@Icesource Icesource commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Fixes #1281.

Problem

When a serializer throws while (de)serializing a field, entries are left on Kryo's generics stacks:

  • ReflectField skips its popGenericType (it was after the nested read/write, not in a finally).
  • FieldSerializer skips popTypeVariables (it is after the field loop, outside the try/catch).

Kryo.reset() did not clear these stacks, so the leaked state survives autoReset and poisons a reused (thread-local or pooled) Kryo instance — a later, unrelated (de)serialization then mistakes the stale generic for its own and fails with a misleading ArrayIndexOutOfBoundsException. The instance stays broken for the rest of its lifetime.

Fix

Following the approach suggested in the issue:

  1. Generics.reset(), called from Kryo.reset() — centralized so it always runs, even if a finally is forgotten somewhere. It has a fast path that returns immediately when the stacks are already empty (the normal case after a balanced serialization), so the per-serialization cost is negligible. The slow path nulls out the leaked entries and zeroes the sizes. Both the genericTypes stack and the type-variable (arguments) stack are cleared, since both leak on exception. NoGenerics gets a no-op.
  2. popGenericType moved to a finally in ReflectField.read/write — the most likely source of serialization exceptions — so the common case keeps the stack balanced without relying on reset(). popGenericType is already safe to call when nothing was pushed.

Tests

GenericsResetTest covers both directions end-to-end:

  • Deserialization: a read that throws inside a List<Item> field, followed by an unrelated HashMap with two non-empty nested maps. Before the fix the generics stack is left at size 2 and the second read throws ArrayIndexOutOfBoundsException; after the fix the stack is empty and the second read succeeds.
  • Serialization: a write that throws inside the same field, then asserting the stack is empty and a subsequent serialization/deserialization succeeds.

Both fail on master without the fix and pass with it.

Full suite: the only failure is DefaultSerializersTest#testLocaleSerializer (zh_CN vs zh_CN_#Hans), a pre-existing locale-data difference in my environment that also fails on unmodified master.

@Icesource Icesource force-pushed the fix/reset-generics-stack-on-exception branch from 5ef280c to 9519df0 Compare June 17, 2026 02:28
@theigl theigl requested a review from Copilot June 17, 2026 05:41

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.

Pull request overview

Fixes a long-lived state leak in Kryo’s generics tracking where exceptions during field (de)serialization could leave entries on the generics/type-variable stacks, poisoning reused Kryo instances across subsequent operations.

Changes:

  • Add Generics.reset() and invoke it from Kryo.reset() to centrally clear leaked generics/type-variable stack state.
  • Make ReflectField.read/write exception-safe by moving popGenericType() into finally.
  • Add GenericsResetTest to reproduce the failure and assert correct behavior after both read- and write-time exceptions.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
test/com/esotericsoftware/kryo/serializers/GenericsResetTest.java New end-to-end regression tests covering generics stack leaks on exception for both read and write paths.
src/com/esotericsoftware/kryo/util/NoGenerics.java Adds reset() no-op implementation for the no-generics mode.
src/com/esotericsoftware/kryo/util/Generics.java Introduces reset() API for clearing tracked generic info (called from Kryo.reset()).
src/com/esotericsoftware/kryo/util/DefaultGenerics.java Implements stack-clearing reset() for the default generics tracker.
src/com/esotericsoftware/kryo/serializers/ReflectField.java Ensures generics stack is popped via finally even when nested (de)serialization throws.
src/com/esotericsoftware/kryo/Kryo.java Calls generics.reset() as part of Kryo.reset() to prevent stale generics from surviving reuse.

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

Comment thread src/com/esotericsoftware/kryo/util/Generics.java Outdated
An exception thrown while (de)serializing a field left entries on the
generics stacks: ReflectField skipped its popGenericType, and FieldSerializer
skipped popTypeVariables (both are outside a finally). Kryo.reset() did not
clear these stacks, so the leaked state survived autoReset and poisoned a
reused (thread-local or pooled) Kryo instance: a later, unrelated
(de)serialization could fail with an ArrayIndexOutOfBoundsException.

- Add Generics.reset(), called from Kryo.reset(), clearing both the
  genericTypes and the type-variable (arguments) stacks. It has a fast path
  that does nothing when the stacks are already empty, since it runs on every
  serialization.
- Move popGenericType to a finally block in ReflectField read/write, the most
  likely source of serialization exceptions.

Issue: EsotericSoftware#1281
@Icesource Icesource force-pushed the fix/reset-generics-stack-on-exception branch from 9519df0 to a5f5d22 Compare June 17, 2026 06:27
@Icesource

Copy link
Copy Markdown
Contributor Author

Thanks for the review. Addressed both points:

  • Made Generics.reset() a default no-op method so it is not a breaking change for external Generics implementations; DefaultGenerics still overrides it with the real reset.
  • Updated the Javadoc to note it runs after both serialization and deserialization (when auto-reset is enabled).

@theigl

theigl commented Jun 17, 2026

Copy link
Copy Markdown
Collaborator

That looks great! Thank you @Icesource.

@theigl theigl merged commit 90a1c0b into EsotericSoftware:master Jun 17, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

Exception during deserialization leaks generics stack; reset() doesn't clear it, poisoning a reused Kryo instance

3 participants