Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## 1.0.10 — Late-binding race during Java Premium login

- Fix the v1.0.9 startup race where `NetworkManagerInjector.getTemporaryInjector` refuses to associate a valid active PacketEvents pre-login Channel with the generated `TemporaryPlayer` if native `packet_handler` has not appeared yet. FastLoginPlus thus loses its Channel for the whole verification session and aborts as disconnected.
- Bind **active Channel first**, without requiring the native Minecraft Connection to exist yet; then refresh/resolve the genuine NMS Connection lazily every time FastLoginPlus calls `TemporaryPlayerFactory.getInjectorFromPlayer`. A non-active or non-Netty channel remains rejected.
- Do not change the authentication policy or inject encryption into incomplete channels. The native Connection is still required when the AES setter is called; missing Connection cannot silently allow a cracked bypass.
- Add regression coverage for active pre-login channels before native `packet_handler` is present and late handler attachment.

## 1.0.9 — FastLoginPlus native login channel association

- Associate the ProtocolLib `TemporaryPlayer` generated during PacketEvents pre-login callbacks with its actual Netty injector instead of leaving `TemporaryPlayerFactory.getInjectorFromPlayer()` null. FastLoginPlus uses that accessor during Mojang session verification.
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

<groupId>com.comphenix.protocol</groupId>
<artifactId>ProtocolLib2PacketEvents</artifactId>
<version>1.0.9</version>
<version>1.0.10</version>
<packaging>jar</packaging>

<name>ProtocolLib2PacketEvents</name>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,12 @@ public Injector getTemporaryInjector(com.github.retrooper.packetevents.protocol.
return null;
}
netty.refreshNativeConnection();
// A stale/partial channel cannot authenticate a login. Fail closed:
// don't give FastLoginPlus an injector whose networkManager is null.
if (!netty.hasNativeConnection()) return null;
// The PacketEvents LOGIN_START callback can precede the native
// packet_handler's installation. Channel access is independently safe:
// bind now so FastLoginPlus can track a still-connected pre-login user.
// The native Connection is resolved lazily when FastLoginPlus asks for
// the injector again during premium verification/AES initialization.
// No new encryption or authentication decision is made here.
netty.setPlayer(temporary);
return netty;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,28 @@ private TemporaryPlayerFactory() {
}

public static Injector getInjectorFromPlayer(Player player) {
return player instanceof TemporaryPlayer
? ((TemporaryPlayer) player).getInjector()
: null;
if (!(player instanceof TemporaryPlayer temporary)) return null;
Injector injector = temporary.getInjector();
// FastLoginPlus retrieves this adapter at multiple asynchronous login
// phases. When LOGIN_START ran before the PacketEvents/native pipeline
// was fully initialized, retry on the same live User+Channel rather
// than permanently treating a legitimate connection as disconnected.
com.github.retrooper.packetevents.protocol.player.User user =
TemporaryPlayerAdapter.getUser(player);
if (user != null) {
org.bukkit.plugin.Plugin plugin = com.comphenix.protocol.ProtocolLibrary.getPlugin();
if (plugin instanceof com.comphenix.protocol.ProtocolLib bridge) {
Injector resolved = bridge.getTemporaryInjector(user, player);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid rebinding a promoted injector to the temporary player

If a delayed asynchronous callback invokes this accessor after onPlayerJoin has promoted the channel injector to the real Bukkit player, bridge.getTemporaryInjector(...) reaches NetworkManagerInjector.getTemporaryInjector() and unconditionally calls netty.setPlayer(temporary). The injector cached under the real player's UUID is then left pointing at the temporary facade because InjectionFactory.fromPlayer() returns UUID hits without restoring the player, causing subsequent post-login packet events and player-dependent lookups to use the wrong identity. Only retry the bridge binding while the temporary player has no injector; the existing injector can be refreshed directly.

Useful? React with 👍 / 👎.

if (resolved != null) {
if (resolved != injector) temporary.setInjector(resolved);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Make concurrent late binding idempotent

When the initial TemporaryPlayerAdapter.create() call cannot bind because the user's channel is not active yet, two asynchronous login callbacks can both read a null injector, resolve the same newly available channel, and execute this line. TemporaryPlayer.setInjector() rejects any second assignment and is not synchronized, so depending on interleaving one callback can receive an uncaught IllegalStateException and abort authentication. The late-binding operation should atomically install an injector or accept an identical injector that another caller has already installed.

Useful? React with 👍 / 👎.

injector = resolved;
}
}
}
if (injector instanceof com.comphenix.protocol.injector.netty.channel.NettyChannelInjector netty) {
netty.refreshNativeConnection();
}
return injector;
}

public static void setInjectorForPlayer(Player player, Injector injector) {
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/plugin.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ main: com.comphenix.protocol.ProtocolLib
version: 5.4.0
author: CyoriaSMP Team
description: A drop-in ProtocolLib compatibility layer powered by PacketEvents.
p2p-version: 1.0.9
p2p-version: 1.0.10
protocol-lib-api-version: 5.4.0
depend: [packetevents]
provides: [ProtocolLib]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.comphenix.protocol.injector.netty.manager;

import com.comphenix.protocol.injector.netty.channel.NettyChannelInjector;
import com.comphenix.protocol.injector.temporary.TemporaryPlayerFactory;
import com.comphenix.protocol.reflect.ObjectAllocator;
import com.github.retrooper.packetevents.protocol.player.User;
import io.netty.channel.embedded.EmbeddedChannel;
import java.lang.reflect.Field;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

class PreLoginInjectorBindingTest {
@Test
void activeChannelBindsBeforeNativeConnectionAndCanRefreshLater() throws Exception {
EmbeddedChannel channel = new EmbeddedChannel();
User user = ObjectAllocator.allocate(User.class);
Field field = User.class.getDeclaredField("channel");
field.setAccessible(true);
field.set(user, channel);
var player = TemporaryPlayerFactory.createTemporaryPlayer();
try (NetworkManagerInjector manager = new NetworkManagerInjector(null, null, null)) {
var early = manager.getTemporaryInjector(user, player);
assertInstanceOf(NettyChannelInjector.class, early,
"valid channel must bind before native Connection exists");
assertFalse(((NettyChannelInjector) early).hasNativeConnection());
TemporaryPlayerFactory.setInjectorForPlayer(player, early);
assertSame(early, TemporaryPlayerFactory.getInjectorFromPlayer(player));
channel.pipeline().addLast("packet_handler", new net.minecraft.network.Connection());
var late = manager.getTemporaryInjector(user, player);
assertSame(early, late, "must not replace channel injector mid-login");
assertTrue(((NettyChannelInjector) late).hasNativeConnection());
assertSame(late, TemporaryPlayerFactory.getInjectorFromPlayer(player));
} finally {
channel.finishAndReleaseAll();
}
}
}
Loading