Skip to content

Commit changes#5

Merged
PythonChicken123 merged 4 commits into
memoryfrom
python-minecraft-launcher
May 9, 2026
Merged

Commit changes#5
PythonChicken123 merged 4 commits into
memoryfrom
python-minecraft-launcher

Conversation

@PythonChicken123

@PythonChicken123 PythonChicken123 commented May 9, 2026

Copy link
Copy Markdown
Owner

Summary by Sourcery

Fix Windows PE import table parsing and JVM module-handle handling for memory-resident JVM boot, and harden JVM path and environment setup when launching Minecraft.

Bug Fixes:

  • Correct PE32+ import directory offsets and add bounds-checked parsing to prevent access violations when scanning jvm.dll imports.
  • Ensure GetModuleHandle{Ex}{A,W} calls for the in-memory jvm.dll return a valid handle so java.home and boot class path are correctly initialised, avoiding HotSpot startup failures.
  • Prevent inconsistent JDK selection in memory-resident mode by refusing to auto-switch to a different portablemc-detected JDK.

Enhancements:

  • Introduce a PE import parser with on-disk caching and a wildcard IAT lookup that works with modern Windows api-set proxy DLLs.
  • Add JVM path enforcement that strips conflicting -Djava.home and related properties, recomputes authoritative values from jdk_bin, and validates the runtime image.
  • Scrub JVM-injecting environment variables and augment the DLL search path so auxiliary JDK libraries and child processes use the intended JDK.

v0 and others added 4 commits May 8, 2026 17:37
Override '-Djava.home', '-Dsun.boot.library.path', and '-Djava.library.path' to prevent crashes.

Co-authored-by: PythonChicken123 <101510622+PythonChicken123@users.noreply.github.com>
Prefer on-disk PE parsing and add bounds checks to prevent access violations.

Co-authored-by: PythonChicken123 <101510622+PythonChicken123@users.noreply.github.com>
Co-authored-by: PythonChicken123 <101510622+PythonChicken123@users.noreply.github.com>
Update in-memory walk match condition for wildcard and patch IAT.

Co-authored-by: PythonChicken123 <101510622+PythonChicken123@users.noreply.github.com>
@vercel

vercel Bot commented May 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
minecraft-portable-web Ready Ready Preview, Comment, Open in v0 May 9, 2026 0:14am

@sourcery-ai

sourcery-ai Bot commented May 9, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Refactors and hardens the Windows JvmBootGlue PE/IAT handling and JVM launch path configuration: fixes the manual PE import walk, adds a disk-based import parser and api-set–aware IAT lookup, introduces new GetModuleHandle/GetModuleHandleEx hooks that correctly recognize the in‑memory jvm.dll, and enforces consistent java.home / boot library paths and environment when launching Minecraft in both OS-loader and memory-resident modes.

Sequence diagram for GetModuleHandleEx hooks resolving in-memory jvm.dll

sequenceDiagram
    actor HotSpot
    participant jvm_dll as jvm_dll
    participant IAT_gmhew as IAT_gmhew_hook
    participant IAT_gmfw as IAT_gmfw_hook
    participant Kernel32 as kernel32

    HotSpot->>jvm_dll: call os::init_system_properties_values
    jvm_dll->>IAT_gmhew: GetModuleHandleExW(FLAG_FROM_ADDRESS, &os::init_system_properties_values, &h)
    IAT_gmhew->>IAT_gmhew: check flags & FLAG_FROM_ADDRESS
    IAT_gmhew->>IAT_gmhew: addr in [jvm_lo, jvm_hi)?
    alt address_inside_jvm_range
        IAT_gmhew-->>jvm_dll: return TRUE, *h = fake
    else address_outside_jvm_range
        IAT_gmhew->>Kernel32: real GetModuleHandleExW(flags, name_or_addr, out_h)
        Kernel32-->>IAT_gmhew: result
        IAT_gmhew-->>jvm_dll: propagate result
    end

    HotSpot->>jvm_dll: if (!h) return
    jvm_dll->>IAT_gmfw: GetModuleFileNameW(h, home_path, MAX_PATH)
    IAT_gmfw->>IAT_gmfw: if h == fake
    IAT_gmfw-->>jvm_dll: return on_disk_jvm_path
    jvm_dll-->>HotSpot: java.home initialised from on_disk_jvm_path
Loading

Sequence diagram for launch_minecraft JVM boot paths

sequenceDiagram
    actor Caller
    participant launch as launch_minecraft
    participant Adapter as adapter
    participant Loader as JvmMemoryLoader
    participant JvmBootGlue as JvmBootGlue
    participant JPype as jpype

    Caller->>launch: launch_minecraft(jdk_bin, memory_resident, extra_jvm_flags)
    launch->>Adapter: resolve()
    Adapter-->>launch: jvm_flags, classpath, main_class, game_args

    launch->>launch: maybe override jdk_bin with adapter.detected_jdk_bin

    launch->>launch: merge extra_jvm_flags
    launch->>launch: jvm_flags = _enforce_jvm_path_overrides(jvm_flags, jdk_bin, require_runtime_image)
    launch->>launch: _dll_search_cookies = _augment_dll_search_path(jdk_bin)
    launch->>launch: java_home = _java_home_from_bin(jdk_bin)

    launch->>_ScrubJvmEnv: __enter__(java_home)
    note over launch,_ScrubJvmEnv: JVM env vars scrubbed, JAVA_HOME pinned

    alt memory_resident == false
        launch->>JPype: startJVM(jvm_dll_path, *jvm_flags, classpath)
        JPype-->>launch: JVM started
        launch->>JPype: JClass(main_class).main(game_args)
        JPype-->>launch: game returns
        launch->>launch: _wait_non_daemon_threads()
        launch->>_ScrubJvmEnv: __exit__()
        _ScrubJvmEnv-->>launch: env restored
        launch-->>Caller: return
    else memory_resident == true
        Caller->>Loader: prepare memory-resident jvm.dll
        Loader-->>launch: loader with fake handle
        launch->>JvmBootGlue: __init__(loader, jvm_flags, classpath, main_class, game_args)
        launch->>JvmBootGlue: boot()
        JvmBootGlue-->>launch: JVM started, game finished
        launch->>_ScrubJvmEnv: __exit__()
        _ScrubJvmEnv-->>launch: env restored
        launch-->>Caller: return
    end
Loading

Class diagram for JvmBootGlue helpers and environment scrubbing

classDiagram
    class JvmBootGlue {
        - dict~str, _IATEntry|None~ _iat
        - int _jvm_lo
        - int _jvm_hi
        - dict~str, object~ _hooks
        + _make_hooks() void
        + _patch_iat() void
        + boot() void
    }

    class PEImportCache {
        + dict~tuple~str, bytes~, int~ _PE_IMPORTS_CACHE
        + _imports_for_path(module_path: str) dict~tuple~str, bytes~, int~
        + _parse_pe_imports(data: bytes) dict~tuple~str, bytes~, int~
    }

    class IATLookup {
        + _find_iat(module_base: int, target_dll: str|None, target_fn: str, module_path: str|None) _IATEntry
    }

    class JvmPathEnforcer {
        + tuple~str~ _PINNED_PROPS
        + tuple~str~ _SCRUBBED_ENV_VARS
        + _java_home_from_bin(jdk_bin: Path) Path
        + _verify_runtime_image(java_home: Path, fatal: bool) bool
        + _enforce_jvm_path_overrides(jvm_flags: list~str~, jdk_bin: Path, require_runtime_image: bool) list~str~
        + _augment_dll_search_path(jdk_bin: Path) list~object~
    }

    class _ScrubJvmEnv {
        - str _java_home
        - dict~str, str|None~ _saved
        + __init__(java_home: Path)
        + __enter__() _ScrubJvmEnv
        + __exit__(*exc) void
    }

    class Launcher {
        + launch_minecraft(jdk_bin: Path, memory_resident: bool, extra_jvm_flags: list~str|None~) void
    }

    JvmBootGlue ..> PEImportCache : uses
    JvmBootGlue ..> IATLookup : uses
    Launcher ..> JvmPathEnforcer : uses
    Launcher ..> _ScrubJvmEnv : uses
    Launcher ..> JvmBootGlue : creates
    PEImportCache ..> IATLookup : supplies imports for
    JvmPathEnforcer ..> JvmBootGlue : ensures consistent java.home for
    _ScrubJvmEnv ..> JvmPathEnforcer : pins JAVA_HOME based on
Loading

File-Level Changes

Change Details Files
Fix and harden PE import directory parsing and IAT lookup for Windows modules, including api-set proxy DLLs and malformed images.
  • Introduce a PE32+ on-disk import parser that walks IMAGE_IMPORT_DESCRIPTOR tables safely with bounds checking and returns a dll/function→IAT-RVA map.
  • Add a cache keyed by resolved module path to avoid re-parsing import tables on repeated lookups.
  • Refactor _find_iat to prefer disk-byte parsing when module_path exists, fall back to a corrected and bounds-checked in-memory PE walk, and support wildcard DLL matching when target_dll is None.
  • Fix the historical bug using the wrong OptionalHeader data-directory offsets (previously reading the Resource directory instead of Import), and add aggressive error handling to avoid access violations on malformed headers.
scripts/jvm_boot_glue.py
Extend JvmBootGlue to hook GetModuleHandle*/GetModuleHandleEx* in jvm.dll and correctly treat the in-memory JVM mapping as a real module.
  • Define ctypes prototypes for GetModuleHandleW/A and GetModuleHandleExW/A, and add corresponding entries to the IAT hook map.
  • Track the jvm.dll image address range (lo/hi) from the PE SizeOfImage field to support FROM_ADDRESS lookups.
  • Implement wrapper hooks for GetModuleHandleW/A and GetModuleHandleExW/A that recognize jvm.dll by name or address and return the fake in-memory handle, delegating to the real functions otherwise.
  • Update _patch_iat to resolve IAT entries using wildcard-DLL search, include the new GetModuleHandle-related functions in the optional jvm.dll hooks, and log which hooks are installed.
scripts/jvm_boot_glue.py
Enforce consistent java.home, sun.boot.library.path, and java.library.path based on the chosen JDK, and scrub conflicting JVM-related environment influence.
  • Add helpers to derive java.home from jdk_bin, verify that <java.home>/lib/modules exists (failing fast in memory-resident mode), and augment the DLL search path with jdk_bin and jdk_bin/server using AddDllDirectory.
  • Implement _enforce_jvm_path_overrides to strip conflicting -D path properties from the JVM flag list, rebuild authoritative -Djava.home / -Dsun.boot.library.path / -Djava.library.path, and inject them at both the front and tail of the final flags.
  • Introduce a context manager that temporarily clears JAVA_TOOL_OPTIONS, _JAVA_OPTIONS, and JDK_JAVA_OPTIONS and pins JAVA_HOME to the launcher-selected java.home for the duration of JVM boot.
  • Update launch_minecraft to apply these overrides after all flag merges, to maintain consistency between jvm.dll origin and path properties, and to scope JVM boot inside the environment-scrubbing context.
scripts/jvm_boot_glue.py
Adjust JDK selection logic in launch_minecraft to keep memory-resident jvm.dll and java.home consistent and improve logging.
  • Change auto-detected JDK selection so that, when memory_resident is True, the launcher refuses to switch to portablemc’s detected JDK if it differs from the jdk_bin whose jvm.dll was already mapped, emitting a warning instead.
  • Keep the existing auto-switch behavior for OS-loader mode, but now combined with the new path-enforcement helpers.
  • Add additional logging around runtime-image checks, stripped JVM flags, path overrides, and installed IAT hooks to aid diagnostics.
scripts/jvm_boot_glue.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot 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.

Hey - I've found 2 issues, and left some high level feedback:

  • Some of the section-divider comment lines now contain mojibake characters (e.g. # ═════════════�� / # ════════════��), which makes the file look corrupted; consider fixing these back to plain ASCII or valid Unicode separators for readability.
  • In _safe_cstr, IsBadReadPtr is configured on every call and relies on a deprecated API; consider initialising the function’s argtypes/restype once at module scope (or replacing it with a VirtualQuery/try-read approach) to reduce overhead and avoid repeated global state mutation.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Some of the section-divider comment lines now contain mojibake characters (e.g. `# ═════════════��` / `# ════════════��`), which makes the file look corrupted; consider fixing these back to plain ASCII or valid Unicode separators for readability.
- In `_safe_cstr`, `IsBadReadPtr` is configured on every call and relies on a deprecated API; consider initialising the function’s `argtypes`/`restype` once at module scope (or replacing it with a `VirtualQuery`/try-read approach) to reduce overhead and avoid repeated global state mutation.

## Individual Comments

### Comment 1
<location path="scripts/jvm_boot_glue.py" line_range="480-483" />
<code_context>
-        dname = ctypes.string_at(module_base + nrv).decode("ascii", "replace").upper()
-        if dname == tdll:
+
+        dname = _safe_cstr(module_base + nrv).decode("ascii", "replace").upper()
+        if tdll is None or dname == tdll:
+            thunk = oft or ft        # fall back to FT for bound imports
             i = 0
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Empty strings from _safe_cstr on unreadable memory are treated as valid DLL names when target_dll is None.

Because `_safe_cstr` returns `b""` on unreadable memory, you end up treating an empty DLL name as a match when `target_dll is None` and then walking its thunk table. For malformed images this adds unnecessary work, can surface extra `OSError`s, and makes the matching semantics less clear. Consider skipping descriptors with empty names (e.g. `if not dname: desc += 20; continue`) so only readable, non-empty names are included in the wildcard search.

```suggestion
        raw_dname = _safe_cstr(module_base + nrv)
        if not raw_dname:
            # Skip unreadable or empty DLL names when doing wildcard matches
            desc += 20
            continue

        dname = raw_dname.decode("ascii", "replace").upper()
        if tdll is None or dname == tdll:
            thunk = oft or ft        # fall back to FT for bound imports
            i = 0
```
</issue_to_address>

### Comment 2
<location path="scripts/jvm_boot_glue.py" line_range="1059-1068" />
<code_context>
+def _augment_dll_search_path(jdk_bin: Path) -> list[object]:
</code_context>
<issue_to_address>
**suggestion (bug_risk):** DLL directory cookies are scoped to launch_minecraft; future refactors could accidentally shorten their lifetime.

The docstring says AddDllDirectory cookies must live for the lifetime of the JVM, but they’re only stored in the local `_dll_search_cookies` inside `launch_minecraft`, making that guarantee implicit in the current control flow. To make this resilient to refactors, please store the cookies on a longer‑lived object (e.g., as an attribute on `JvmBootGlue`, the loader, or a module‑level list) so their lifetime is explicitly tied to the JVM/process rather than the function scope.

Suggested implementation:

```python
def _augment_dll_search_path(
    jdk_bin: Path,
    *,
    cookie_sink: list[object] | None = None,
) -> list[object]:
    """
    Make ``jdk_bin`` and ``jdk_bin/server`` discoverable by the OS loader for
    the lifetime of the returned cookies.

    ``cookie_sink``, if provided, is extended with the created AddDllDirectory
    cookies so their lifetime is tied to that longer-lived container (for
    example an attribute on ``JvmBootGlue``) rather than to a local variable in
    the caller.

    Even in memory-resident mode, several auxiliary JDK libraries (jawt.dll,
    sunmscapi.dll, awt.dll, …) are loaded by the JVM through the regular OS
    loader after JNI_CreateJavaVM completes.  AddDllDirectory provides a
    narrowly-scoped search list without polluting PATH.

    Returns

```

To fully implement the longer-lived lifetime for the AddDllDirectory cookies, you’ll also need to:

1. Inside `_augment_dll_search_path`, after you build the list of cookies (whatever variable currently holds them, e.g. `cookies` or `_dll_search_cookies`), add:
   ```python
   if cookie_sink is not None:
       cookie_sink.extend(cookies)
   ```
   immediately before the `return cookies` statement.

2. In the code that calls `_augment_dll_search_path` (likely inside `launch_minecraft`):
   - Replace the current local-only storage:
     ```python
     _dll_search_cookies = _augment_dll_search_path(jdk_bin)
     ```
     with something that uses a longer-lived container, for example on `JvmBootGlue`:
     ```python
     if not hasattr(self, "_dll_search_cookies"):
         self._dll_search_cookies: list[object] = []
     _augment_dll_search_path(jdk_bin, cookie_sink=self._dll_search_cookies)
     ```
     or, if `launch_minecraft` is not a method, use a module-level list instead:
     ```python
     _DLL_SEARCH_COOKIES: list[object] = []
     ...
     _augment_dll_search_path(jdk_bin, cookie_sink=_DLL_SEARCH_COOKIES)
     ```

3. Remove any now-unused local `_dll_search_cookies` variables so that the only strong references to the cookies are on the longer-lived object (e.g. `self._dll_search_cookies` or the module-level `_DLL_SEARCH_COOKIES`).

These changes ensure the AddDllDirectory cookies are explicitly tied to the JVM/process lifetime via a long-lived container, making the guarantee resilient to future refactors of `launch_minecraft`.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread scripts/jvm_boot_glue.py
Comment on lines +480 to 483
dname = _safe_cstr(module_base + nrv).decode("ascii", "replace").upper()
if tdll is None or dname == tdll:
thunk = oft or ft # fall back to FT for bound imports
i = 0

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.

suggestion (bug_risk): Empty strings from _safe_cstr on unreadable memory are treated as valid DLL names when target_dll is None.

Because _safe_cstr returns b"" on unreadable memory, you end up treating an empty DLL name as a match when target_dll is None and then walking its thunk table. For malformed images this adds unnecessary work, can surface extra OSErrors, and makes the matching semantics less clear. Consider skipping descriptors with empty names (e.g. if not dname: desc += 20; continue) so only readable, non-empty names are included in the wildcard search.

Suggested change
dname = _safe_cstr(module_base + nrv).decode("ascii", "replace").upper()
if tdll is None or dname == tdll:
thunk = oft or ft # fall back to FT for bound imports
i = 0
raw_dname = _safe_cstr(module_base + nrv)
if not raw_dname:
# Skip unreadable or empty DLL names when doing wildcard matches
desc += 20
continue
dname = raw_dname.decode("ascii", "replace").upper()
if tdll is None or dname == tdll:
thunk = oft or ft # fall back to FT for bound imports
i = 0

Comment thread scripts/jvm_boot_glue.py
Comment on lines +1059 to +1068
def _augment_dll_search_path(jdk_bin: Path) -> list[object]:
"""
Make ``jdk_bin`` and ``jdk_bin/server`` discoverable by the OS loader for
the lifetime of the returned cookies.

Even in memory-resident mode, several auxiliary JDK libraries (jawt.dll,
sunmscapi.dll, awt.dll, …) are loaded by the JVM through the regular OS
loader after JNI_CreateJavaVM completes. AddDllDirectory provides a
narrowly-scoped search list without polluting PATH.

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.

suggestion (bug_risk): DLL directory cookies are scoped to launch_minecraft; future refactors could accidentally shorten their lifetime.

The docstring says AddDllDirectory cookies must live for the lifetime of the JVM, but they’re only stored in the local _dll_search_cookies inside launch_minecraft, making that guarantee implicit in the current control flow. To make this resilient to refactors, please store the cookies on a longer‑lived object (e.g., as an attribute on JvmBootGlue, the loader, or a module‑level list) so their lifetime is explicitly tied to the JVM/process rather than the function scope.

Suggested implementation:

def _augment_dll_search_path(
    jdk_bin: Path,
    *,
    cookie_sink: list[object] | None = None,
) -> list[object]:
    """
    Make ``jdk_bin`` and ``jdk_bin/server`` discoverable by the OS loader for
    the lifetime of the returned cookies.

    ``cookie_sink``, if provided, is extended with the created AddDllDirectory
    cookies so their lifetime is tied to that longer-lived container (for
    example an attribute on ``JvmBootGlue``) rather than to a local variable in
    the caller.

    Even in memory-resident mode, several auxiliary JDK libraries (jawt.dll,
    sunmscapi.dll, awt.dll, …) are loaded by the JVM through the regular OS
    loader after JNI_CreateJavaVM completes.  AddDllDirectory provides a
    narrowly-scoped search list without polluting PATH.

    Returns

To fully implement the longer-lived lifetime for the AddDllDirectory cookies, you’ll also need to:

  1. Inside _augment_dll_search_path, after you build the list of cookies (whatever variable currently holds them, e.g. cookies or _dll_search_cookies), add:

    if cookie_sink is not None:
        cookie_sink.extend(cookies)

    immediately before the return cookies statement.

  2. In the code that calls _augment_dll_search_path (likely inside launch_minecraft):

    • Replace the current local-only storage:
      _dll_search_cookies = _augment_dll_search_path(jdk_bin)
      with something that uses a longer-lived container, for example on JvmBootGlue:
      if not hasattr(self, "_dll_search_cookies"):
          self._dll_search_cookies: list[object] = []
      _augment_dll_search_path(jdk_bin, cookie_sink=self._dll_search_cookies)
      or, if launch_minecraft is not a method, use a module-level list instead:
      _DLL_SEARCH_COOKIES: list[object] = []
      ...
      _augment_dll_search_path(jdk_bin, cookie_sink=_DLL_SEARCH_COOKIES)
  3. Remove any now-unused local _dll_search_cookies variables so that the only strong references to the cookies are on the longer-lived object (e.g. self._dll_search_cookies or the module-level _DLL_SEARCH_COOKIES).

These changes ensure the AddDllDirectory cookies are explicitly tied to the JVM/process lifetime via a long-lived container, making the guarantee resilient to future refactors of launch_minecraft.

@PythonChicken123
PythonChicken123 merged commit 8829b33 into memory May 9, 2026
5 of 7 checks passed
@PythonChicken123
PythonChicken123 deleted the python-minecraft-launcher branch May 9, 2026 12:21
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.

1 participant