mirror of
https://git.sanhost.net/sanasol/hytale-f2p
synced 2026-02-26 18:41:48 -03:00
Root cause: glibc 2.41 has stricter heap validation that catches a
pre-existing race condition triggered by binary patching.
Changes:
- Add jemalloc auto-detection and usage on Linux
- Add auto-install via pkexec (graphical sudo prompt)
- Clean up clientPatcher.js (remove debug env vars)
- Add null-padding fix for shorter domain replacements
- Document investigation and solution
The launcher now:
1. Auto-detects jemalloc if installed
2. Offers to auto-install if missing (password prompt)
3. Falls back to MALLOC_CHECK_=0 if jemalloc unavailable
Install manually: sudo pacman -S jemalloc (Arch/Steam Deck)
sudo apt install libjemalloc2 (Debian/Ubuntu)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
202 lines
6.3 KiB
Markdown
202 lines
6.3 KiB
Markdown
# Steam Deck / Ubuntu LTS Crash Investigation
|
|
|
|
## Problem Summary
|
|
|
|
The Hytale F2P launcher's client patcher causes crashes on Steam Deck and Ubuntu LTS with the error:
|
|
```
|
|
free(): invalid pointer
|
|
```
|
|
|
|
The crash occurs after successful authentication, specifically right after "Finished handling RequiredAssets".
|
|
|
|
**Affected Systems:**
|
|
- Steam Deck (glibc 2.41)
|
|
- Ubuntu LTS
|
|
|
|
**Working Systems:**
|
|
- macOS
|
|
- Windows
|
|
- Arch Linux
|
|
|
|
**Critical Finding:** The UNPATCHED original binary works fine on Steam Deck. The crash is caused by our patching.
|
|
|
|
---
|
|
|
|
## String Occurrences Found
|
|
|
|
### UTF-16LE Format (3 occurrences)
|
|
Found by `HYTALE_PATCH_MODE=utf16le`:
|
|
|
|
| Index | Offset | Before Context | After Context | Likely URL |
|
|
|-------|--------|----------------|---------------|------------|
|
|
| 0 | 0x1bc5ad7 | `try.` | `/2i3...` | `sentry.hytale.com/2...` |
|
|
| 1 | 0x1bc5b3f | `s://` | `/hel` | `https://hytale.com/help...` |
|
|
| 2 | 0x1bc5bc9 | `ore.` | `/?up` | `store.hytale.com/?up...` |
|
|
|
|
### Length-Prefixed Format (1 occurrence)
|
|
Found by default `length-prefixed` mode:
|
|
|
|
| Offset | Before | After | Notes |
|
|
|--------|--------|-------|-------|
|
|
| 0x1bc5d63 | `5933b8` | `89338807` | **Surrounded by what looks like x86 code!** |
|
|
|
|
---
|
|
|
|
## Critical Finding: Binary Diff Analysis
|
|
|
|
When patching with length-prefixed mode (single occurrence):
|
|
|
|
```
|
|
< 01bc5d60: 5933 b80a 0000 0068 0079 0074 0061 006c Y3.....h.y.t.a.l
|
|
< 01bc5d70: 0065 002e 0063 006f 006d 8933 8807 0000 .e...c.o.m.3....
|
|
---
|
|
> 01bc5d60: 5933 b80a 0000 0073 0061 006e 0061 0073 Y3.....s.a.n.a.s
|
|
> 01bc5d70: 006f 006c 002e 0077 0073 8933 8807 0000 .o.l...w.s.3....
|
|
```
|
|
|
|
**Structure at 0x1bc5d60:**
|
|
```
|
|
5933 b8 | 0a000000 | 68007900740061006c0065002e0063006f006d | 8933 8807 0000
|
|
???????? | len=10 | h.y.t.a.l.e...c.o.m | mov [rbx],esi?
|
|
```
|
|
|
|
- `5933 b8` before the string - could be code or metadata
|
|
- `0a 00 00 00` - .NET length prefix (10 characters)
|
|
- String content in UTF-16LE
|
|
- `89 33` after - this is `mov [rbx], esi` in x86-64!
|
|
|
|
**The string appears to be embedded near executable code, not in a clean data section.**
|
|
|
|
---
|
|
|
|
## Test Results Summary
|
|
|
|
| Test | Occurrences Patched | Auth Works | Crashes |
|
|
|------|---------------------|------------|---------|
|
|
| Length-prefixed (default) | 1 at 0x1bc5d63 | YES | YES |
|
|
| UTF-16LE mode | 3 at 0x1bc5ad7, 0x1bc5b3f, 0x1bc5bc9 | YES | YES |
|
|
| Skip all UTF-16LE | 0 (but legacy fallback patched 4!) | YES | YES |
|
|
| Original unpatched | 0 | NO (wrong issuer) | NO |
|
|
|
|
**Key Insight:** Even patching just ONE string (the length-prefixed one) causes the crash, yet authentication succeeds before the crash.
|
|
|
|
---
|
|
|
|
## GDB Stack Trace
|
|
|
|
```
|
|
#0 0x00007ffff7d3f5a4 in ?? () from /usr/lib/libc.so.6
|
|
#1 raise () from /usr/lib/libc.so.6
|
|
#2 abort () from /usr/lib/libc.so.6
|
|
#3-#4 ?? () from /usr/lib/libc.so.6
|
|
#5 free () from /usr/lib/libc.so.6
|
|
#6 ?? () from libzstd.so <-- CRASH POINT
|
|
#7-#24 HytaleClient code (asset decompression)
|
|
```
|
|
|
|
Crash occurs in `libzstd.so` during `free()` after "Finished handling RequiredAssets".
|
|
|
|
---
|
|
|
|
## Hypotheses
|
|
|
|
### 1. .NET String Interning
|
|
.NET AOT may have precomputed hashes or metadata for interned strings. Modifying the string content breaks the hash, causing memory corruption when the runtime tries to use it.
|
|
|
|
### 2. Code/Data Boundary Issue
|
|
The string at 0x1bc5d63 appears to be embedded near x86 code (`89 33` = `mov [rbx], esi`). Modifying it might corrupt instruction decoding or memory layout calculations.
|
|
|
|
### 3. Checksums/Integrity
|
|
The binary may have checksums for certain data sections that we're invalidating.
|
|
|
|
### 4. Memory Alignment
|
|
glibc 2.41's stricter heap validation may catch alignment issues that older versions ignore.
|
|
|
|
---
|
|
|
|
## Debug Environment Variables
|
|
|
|
| Variable | Description | Example |
|
|
|----------|-------------|---------|
|
|
| `HYTALE_AUTH_DOMAIN` | Target domain | `sanasol.ws` |
|
|
| `HYTALE_PATCH_MODE` | `utf16le` or `length-prefixed` | `utf16le` |
|
|
| `HYTALE_SKIP_SENTRY_PATCH` | Skip sentry URL patch | `1` |
|
|
| `HYTALE_SKIP_SUBDOMAIN_PATCH` | Skip subdomain patches | `1` |
|
|
| `HYTALE_PATCH_LIMIT` | Max patches to apply | `1` |
|
|
| `HYTALE_PATCH_SKIP` | Comma-separated indices to skip | `0,2` |
|
|
| `HYTALE_NO_LEGACY_FALLBACK` | Disable legacy fallback | `1` |
|
|
| `HYTALE_NOOP_TEST` | Read/write without patching | `1` |
|
|
|
|
---
|
|
|
|
## Files & Offsets Reference
|
|
|
|
**Binary:** `HytaleClient` (ELF 64-bit, ~39.9 MB)
|
|
|
|
| Offset | Format | Content |
|
|
|--------|--------|---------|
|
|
| 0x1bc5ad7 | UTF-16LE | `sentry.hytale.com/...` |
|
|
| 0x1bc5b3f | UTF-16LE | `https://hytale.com/help...` |
|
|
| 0x1bc5bc9 | UTF-16LE | `store.hytale.com/?...` |
|
|
| 0x1bc5d63 | Length-prefixed | Main session URL (surrounded by code?) |
|
|
|
|
---
|
|
|
|
## SOLUTION FOUND ✓
|
|
|
|
### Root Cause
|
|
The crash is caused by **glibc 2.41's stricter heap validation** catching a pre-existing race condition in the .NET AOT runtime or asset decompression code. Our binary patching triggers this timing-dependent bug, but the patching itself is correct.
|
|
|
|
### Evidence
|
|
- Valgrind showed NO memory corruption errors
|
|
- Game ran successfully under Valgrind (slower execution avoids the race)
|
|
- Game was manually killed (SIGINT), not crashed
|
|
- 1.4M allocations with no "Invalid free" detected
|
|
|
|
### Fix: Use jemalloc allocator
|
|
jemalloc handles the race condition gracefully. The launcher now auto-detects and uses jemalloc on Linux.
|
|
|
|
**Install jemalloc:**
|
|
```bash
|
|
# Steam Deck / Arch Linux
|
|
sudo pacman -S jemalloc
|
|
|
|
# Ubuntu / Debian
|
|
sudo apt install libjemalloc2
|
|
|
|
# Fedora / RHEL
|
|
sudo dnf install jemalloc
|
|
```
|
|
|
|
The launcher automatically uses jemalloc if found at:
|
|
- `/usr/lib/libjemalloc.so.2` (Arch, Steam Deck)
|
|
- `/usr/lib/x86_64-linux-gnu/libjemalloc.so.2` (Debian/Ubuntu)
|
|
- `/usr/lib64/libjemalloc.so.2` (Fedora/RHEL)
|
|
|
|
**Manual workaround (if launcher doesn't detect):**
|
|
```bash
|
|
LD_PRELOAD=/usr/lib/libjemalloc.so.2 ./Client/HytaleClient ...
|
|
```
|
|
|
|
**Disable jemalloc (for testing):**
|
|
```bash
|
|
HYTALE_NO_JEMALLOC=1 npm start
|
|
```
|
|
|
|
---
|
|
|
|
## Previous Investigation (for reference)
|
|
|
|
### Next Steps (COMPLETED)
|
|
|
|
1. ~~Try runtime hooking instead of binary patching~~ - Not needed, jemalloc fixes the issue
|
|
2. ~~Investigate .NET AOT string metadata~~ - Not the root cause
|
|
3. ~~Test on different glibc versions~~ - Confirmed glibc 2.41 specific
|
|
4. ~~Examine libzstd interaction~~ - libzstd's free() was just where the corruption manifested
|
|
|
|
---
|
|
|
|
## Branch
|
|
|
|
`fix/patcher-memory-corruption-v2`
|