Replace patchelf crack with Freeloader LD_PRELOAD approach
- Multi-stage Dockerfile: discover patterns from PMS binary (capstone), compile .so with zig (musl), layer onto lscr.io/linuxserver/plex - Uses LD_PRELOAD instead of patchelf (which corrupts Plex's musl loader) - Auto-discovery: broad structural patterns with string-anchored fallback (//feature) and relationship-based fallback (BITSET_REF within BS_INIT) - hook.cpp uses __has_include for generated patterns with hardcoded fallbacks - Custom wrapper.sh (no traffic_logger preload) - Vendored Freeloader source (github.com/authrequest/Freeloader, AGPL-3.0) - Removed stale plexmediaserver_crack.so binary - Supports Plex 1.43.3+ (verified against 1.43.2 and 1.43.3)
This commit is contained in:
1 parent
4399a8288d
commit
72f4661bdc
72 files changed
+77927
-17
No files matched your search
@@ -0,0 +1,351 @@
|
||||
# Building & running the Plex patcher in Docker (Linux x86-64)
|
||||
|
||||
The same `LD_PRELOAD`-on-the-PMS-exec trick the native systemd install uses
|
||||
([`BUILD.md`](BUILD.md)) translates to Docker: build a patched Plex image,
|
||||
run it as you would the upstream image, and the (musl) PMS process is
|
||||
preloaded with the crack without affecting the (glibc) s6 init, helper
|
||||
children, or transcoders.
|
||||
|
||||
## How this works (read this first)
|
||||
|
||||
Three constraints are identical to the native install:
|
||||
|
||||
1. **Build target is musl** — Plex bundles and runs against its own musl
|
||||
libc. A glibc `.so` won't load. We cross-compile with `zig` in a
|
||||
multi-stage build (`debian:bookworm-slim` builder, Plex base image
|
||||
runtime).
|
||||
2. **Inject with `LD_PRELOAD`** — `patchelf` corrupts the PIE under musl's
|
||||
loader. The patch is at-rest: drop a `.so` next to PMS in the image and
|
||||
have the s6 service exec PMS through a thin wrapper that exports
|
||||
`LD_PRELOAD` only at exec time.
|
||||
3. **Scope `LD_PRELOAD` to the PMS exec** — the wrapper sets `LD_PRELOAD`
|
||||
*after* any glibc child work and only for the final `exec`. The `.so`'s
|
||||
constructor (see [`../src/main.cpp`](../src/main.cpp)) calls
|
||||
`unsetenv("LD_PRELOAD")`, so PMS's glibc helper children (Tuner, Script
|
||||
Host, transcoders) are unaffected.
|
||||
|
||||
The only difference between the two Dockerfiles is how the s6 service for
|
||||
Plex is replaced — the `.so` and `wrapper.sh` are identical. In both cases
|
||||
the upstream run file is backed up as `run.orig` for forensics.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Docker Engine 20.10+ with BuildKit enabled (`DOCKER_BUILDKIT=1` or
|
||||
BuildKit as the default builder in recent Docker). Both Dockerfiles use
|
||||
`# syntax=docker/dockerfile:1.7`.
|
||||
- An x86_64 Linux host (or any host with `docker buildx` configured for
|
||||
`linux/amd64`).
|
||||
- A place to keep PMS state on the host. We recommend `/srv/plex/config`
|
||||
and `/srv/plex/data`; the Dockerfiles don't bake any of this in.
|
||||
|
||||
## Build
|
||||
|
||||
From the project root:
|
||||
|
||||
```bash
|
||||
# Official (plexinc/pms-docker)
|
||||
docker build -f docker/Dockerfile.plexinc -t plex-crack:plexinc .
|
||||
|
||||
# Community (lscr.io/linuxserver/plex)
|
||||
docker build -f docker/Dockerfile.linuxserver -t plex-crack:lsio .
|
||||
```
|
||||
|
||||
The first build downloads `zig 0.13.0` and the chosen Plex base image.
|
||||
Subsequent builds reuse cached layers until `src/`, `third_party/`, or
|
||||
`build.sh` change.
|
||||
|
||||
### Pinning the base image
|
||||
|
||||
Both Dockerfiles accept a build-arg to pin the upstream Plex image:
|
||||
|
||||
```bash
|
||||
docker build -f docker/Dockerfile.plexinc \
|
||||
--build-arg PLEX_BASE_IMAGE=plexinc/pms-docker:1.42.1.1007-7e0e6c83c \
|
||||
-t plex-crack:plexinc-1.42 .
|
||||
```
|
||||
|
||||
The sanity `RUN` in stage 2 verifies the upstream layout before the patch
|
||||
layers are added — if plexinc or LSIO moves the PMS binary, the `lib/`
|
||||
directory, or the s6 service file, the build fails *here* with a clear
|
||||
message rather than the container failing mysteriously at runtime.
|
||||
|
||||
You can also stamp the patched image with a version:
|
||||
|
||||
```bash
|
||||
docker build -f docker/Dockerfile.plexinc \
|
||||
--build-arg PATCH_VERSION=v1.2.3 \
|
||||
-t plex-crack:plexinc-v1.2.3 .
|
||||
```
|
||||
|
||||
`PATCH_VERSION` ends up in the `plex_patch.version` OCI label and is
|
||||
visible via `docker inspect`.
|
||||
|
||||
## Run
|
||||
|
||||
### docker run
|
||||
|
||||
```bash
|
||||
# Official (plexinc/pms-docker). No special env; plex runs as the upstream
|
||||
# 'plex' user.
|
||||
docker run -d --name plex --network=host \
|
||||
-v /srv/plex/config:/config \
|
||||
-v /srv/plex/data:/data \
|
||||
plex-crack:plexinc
|
||||
|
||||
# Community (linuxserver). Honor the LSIO PUID/PGID convention so /config
|
||||
# and /data are chowned correctly on first start.
|
||||
docker run -d --name plex --network=host \
|
||||
-e PUID=$(id -u) -e PGID=$(id -g) \
|
||||
-e TZ=America/Los_Angeles \
|
||||
-v /srv/plex/config:/config \
|
||||
-v /srv/plex/data:/data \
|
||||
plex-crack:lsio
|
||||
```
|
||||
|
||||
`--network=host` is what Plex expects for direct LAN access; bridge
|
||||
networking works too if you publish `32400/tcp` (and `3005/tcp`, `8324/tcp`,
|
||||
`32469/udp` for Bonjour/avahi, etc.) — see the Plex docs.
|
||||
|
||||
### docker compose
|
||||
|
||||
```yaml
|
||||
services:
|
||||
plex:
|
||||
image: plex-crack:lsio
|
||||
container_name: plex
|
||||
network_mode: host
|
||||
environment:
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=America/Los_Angeles
|
||||
volumes:
|
||||
- /srv/plex/config:/config
|
||||
- /srv/plex/data:/data
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
For `plex-crack:plexinc`, drop the `PUID`/`PGID` lines.
|
||||
|
||||
## Verify
|
||||
|
||||
```bash
|
||||
# 1. PMS is up.
|
||||
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:32400/identity # -> 200
|
||||
|
||||
# 2. The .so is mapped into the PMS process. s6 runs PMS as a child, so
|
||||
# find it first.
|
||||
docker exec plex ps -ef | grep 'Plex Media Server' | grep -v grep
|
||||
# Take the PID (first column) -- call it $PMS_PID.
|
||||
docker exec plex grep -F plexmediaserver_crack.so /proc/$PMS_PID/maps
|
||||
# -> should list the .so with r-xp perms.
|
||||
|
||||
# 3. Dump the live feature bitset. python3 must be available inside the
|
||||
# container (the upstream images include it; if not, `docker exec -u root
|
||||
# plex apk add python3` on alpine or `apt-get install -y python3` on
|
||||
# debian/ubuntu).
|
||||
docker cp scripts/readbitset.py plex:/tmp/readbitset.py
|
||||
docker exec -u root plex python3 /tmp/readbitset.py $PMS_PID
|
||||
# -> all 14 slots should read 0xffffffffffffffff.
|
||||
```
|
||||
|
||||
Step 3 is the strongest check: if any slot is `0x0…0`, the hook didn't
|
||||
fire and the signature patterns in `src/hook.cpp` need to be re-verified
|
||||
against the new PMS binary.
|
||||
|
||||
## Alternative: patch a running container in place
|
||||
|
||||
The `docker build` flow above builds a new patched image and starts a
|
||||
new container. If you already have a Plex container running — and you
|
||||
want to apply the same patch **without** rebuilding, recreating, or
|
||||
pulling a new image — `docker/plex-docker-patch.sh` does it in place.
|
||||
The container's filesystem is modified directly; the upstream image is
|
||||
left untouched. A `.orig` copy of the s6 `run` file is kept for
|
||||
`uninstall`.
|
||||
|
||||
```bash
|
||||
# Apply (default container name: "plex").
|
||||
./docker/plex-docker-patch.sh install
|
||||
|
||||
# Or with a custom container name.
|
||||
./docker/plex-docker-patch.sh install media-plex
|
||||
|
||||
# Verify.
|
||||
./docker/plex-docker-patch.sh status media-plex
|
||||
|
||||
# Revert (restores s6 run file from .orig).
|
||||
./docker/plex-docker-patch.sh uninstall media-plex
|
||||
```
|
||||
|
||||
Requirements on the host: `docker` on `PATH`, `bash 4+`, and either
|
||||
`zig` available (so `build.sh` can cross-compile) or an existing
|
||||
`build/plexmediaserver_crack.so` (the script reuses it if present).
|
||||
|
||||
### What it does, step by step
|
||||
|
||||
1. Verifies the container exists and is running.
|
||||
2. Detects the s6 service `run` file path
|
||||
(`/etc/s6-overlay/s6-rc.d/svc-plex/run` for v3,
|
||||
`/etc/services.d/plex/run` for v2) and reads it to auto-detect the
|
||||
base image (plexinc vs linuxserver — the latter uses
|
||||
`s6-setuidgid abc`).
|
||||
3. Invokes `build.sh` (skipped if `build/plexmediaserver_crack.so` is
|
||||
already present) to produce the musl `.so`.
|
||||
4. `docker cp`s the `.so` and `wrapper.sh` into the container.
|
||||
5. Backs up the s6 `run` file to `.orig` (preserved across re-installs).
|
||||
6. Writes a new `run` file that execs the wrapper (with or without
|
||||
`s6-setuidgid abc`, depending on the base image).
|
||||
7. `docker restart`s the container.
|
||||
8. Waits for `http://127.0.0.1:32400/identity` to return 200.
|
||||
9. Confirms `plexmediaserver_crack.so` is mapped into the PMS process
|
||||
(`/proc/$PID/maps`).
|
||||
10. Prints the `readbitset.py` command for full feature-bitset
|
||||
verification.
|
||||
|
||||
### When to use which
|
||||
|
||||
- **Dockerfile build** — best for repeat deploys, multi-host, CI/CD,
|
||||
immutable images. You commit a patched image and ship it; running
|
||||
instances are disposable.
|
||||
- **In-place patch** — best for one-off patching of an existing
|
||||
container you don't want to touch (complex `docker run` invocation,
|
||||
custom network, volume layout, or a single-node homelab). Modifies
|
||||
the live container's filesystem; fully revertible via `uninstall`.
|
||||
|
||||
Both use the same `docker/wrapper.sh` and the same
|
||||
`LD_PRELOAD`-on-the-PMS-exec mechanism — only the injection plumbing
|
||||
differs.
|
||||
|
||||
### Limitations
|
||||
|
||||
- x86_64 Linux hosts only (the `.so` is `x86_64-linux-musl`).
|
||||
- The host must be able to `docker exec -u root` into the container.
|
||||
On rootless Docker setups this should still work since the
|
||||
container's `root` is mapped to the host's user.
|
||||
- If the upstream image's s6 layout changes (plexinc or LSIO move the
|
||||
service), the script's auto-detection will fail with a clear error.
|
||||
The Dockerfile flow would fail its sanity `RUN` at build time with
|
||||
the same kind of error.
|
||||
- The patch is **per-container**, not per-image. Re-creating the
|
||||
container (e.g., `docker rm` + `docker run` of the upstream image)
|
||||
reverts the patch; you have to re-run `plex-docker-patch.sh install`.
|
||||
Use the Dockerfile flow for image-baked persistence.
|
||||
|
||||
## Uninstall
|
||||
|
||||
Two ways to remove the patch, depending on which flow you used.
|
||||
|
||||
### Dockerfile-built image
|
||||
|
||||
The patch lives entirely in the image — there is no host state to undo
|
||||
beyond the host's PMS data volumes, which the patch never touches.
|
||||
|
||||
```bash
|
||||
# Stop and remove the container.
|
||||
docker rm -f plex
|
||||
|
||||
# Remove the image.
|
||||
docker rmi plex-crack:plexinc # or plex-crack:lsio
|
||||
```
|
||||
|
||||
To revert to the unmodified upstream image:
|
||||
|
||||
```bash
|
||||
docker pull plexinc/pms-docker:latest
|
||||
docker run -d --name plex --network=host \
|
||||
-v /srv/plex/config:/config \
|
||||
-v /srv/plex/data:/data \
|
||||
plexinc/pms-docker:latest
|
||||
```
|
||||
|
||||
The original s6 service `run` file is preserved in the patched image as
|
||||
`/etc/s6-overlay/s6-rc.d/svc-plex/run.orig` — it can be recovered by
|
||||
rebuilding the patched image without the patch layer (just use the
|
||||
upstream image directly).
|
||||
|
||||
### In-place patch
|
||||
|
||||
```bash
|
||||
# Restore the s6 run file from its .orig and restart the container.
|
||||
./docker/plex-docker-patch.sh uninstall plex
|
||||
|
||||
# Optional cleanup of the .so, wrapper, and .orig backup:
|
||||
docker exec -u root plex rm -f \
|
||||
/usr/lib/plexmediaserver/lib/plexmediaserver_crack.so \
|
||||
/usr/lib/plexmediaserver/plex-crack-wrapper.sh \
|
||||
/etc/s6-overlay/s6-rc.d/svc-plex/run.orig
|
||||
```
|
||||
|
||||
To verify the patch is gone:
|
||||
|
||||
```bash
|
||||
./docker/plex-docker-patch.sh status plex
|
||||
# -> "PATCH IS NOT ACTIVE (s6 run file does not point to the wrapper)."
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### `error while loading shared libraries: ...musl...` on PMS start
|
||||
|
||||
A glibc `.so` accidentally got into the image. The build's ABI sanity gate
|
||||
(`grep -E "UND .*(__isoc23_|_chk$|arc4random|_dl_find_object)"` in
|
||||
`build.sh`) should have caught this — if you bypassed `build.sh` and copied
|
||||
in a pre-built artifact, rebuild via the Dockerfile so the gate runs.
|
||||
|
||||
### `patchelf: ...` warnings during build
|
||||
|
||||
We don't use `patchelf`. If you see this, you're running a non-canonical
|
||||
build flow — see [`BUILD.md`](BUILD.md) for why `patchelf` corrupts the
|
||||
PIE under musl's loader.
|
||||
|
||||
### `Plex Media Server` exits 127 immediately
|
||||
|
||||
The s6 service exec'd PMS but PMS can't find a library. Usually this is
|
||||
either the `.so` ABI mismatch (rebuild) or the wrapper is in the wrong
|
||||
location. Check:
|
||||
|
||||
```bash
|
||||
docker exec plex ls -l /usr/lib/plexmediaserver/lib/plexmediaserver_crack.so
|
||||
docker exec plex ls -l /usr/lib/plexmediaserver/plex-crack-wrapper.sh
|
||||
docker exec plex cat /etc/s6-overlay/s6-rc.d/svc-plex/run
|
||||
```
|
||||
|
||||
The last command should print the wrapper path (with or without
|
||||
`s6-setuidgid abc` depending on which image you built).
|
||||
|
||||
### Feature bitset is not all 1s after `readbitset.py`
|
||||
|
||||
The hook didn't fire. Likely cause: PMS has a version the signature
|
||||
patterns in `src/hook.cpp` don't match. Re-verify the signature patterns
|
||||
against the new PMS binary; the project's RE notes are in
|
||||
[`../AGENTS.md`](../AGENTS.md). After fixing the patterns, rebuild with
|
||||
`--no-cache` so the builder stage re-runs:
|
||||
|
||||
```bash
|
||||
docker build --no-cache -f docker/Dockerfile.plexinc -t plex-crack:plexinc .
|
||||
```
|
||||
|
||||
### Container restarts in a loop, s6 logs `script /etc/s6-overlay/s6-rc.d/svc-plex/run exited 1`
|
||||
|
||||
The wrapper is missing or not executable, or the `.so` failed its ABI
|
||||
gate. Check `docker logs plex` and the in-container paths listed above.
|
||||
|
||||
### LSIO: /config or /data ends up owned by root
|
||||
|
||||
PUID/PGID weren't set, or were set to 0. LSIO's perms init runs once and
|
||||
chowns to the configured UID/GID; if PMS is later started as root (which
|
||||
it would be if the s6-setuidgid was dropped from the run file), the
|
||||
subsequent writes will be root-owned. The Dockerfile preserves
|
||||
`s6-setuidgid abc` exactly so this shouldn't happen with a clean build —
|
||||
rebuild without modifying the `cat > "${RUN_SCRIPT}"` block.
|
||||
|
||||
## Notes
|
||||
|
||||
- x86_64 Linux only — the build emits `x86_64-linux-musl` and the base
|
||||
images are amd64. There is no arm64/v3 PMS Docker image today.
|
||||
- Both Dockerfiles emit OCI labels: `plex_patch.base` (the upstream image
|
||||
reference) and `plex_patch.version` (a build-time stamp, default `dev`).
|
||||
- For intro/credit detection: Settings → Library → Marker source → "local
|
||||
detection only".
|
||||
- The patch has no effect on PMS's network behavior, media transcoding,
|
||||
or library scanning — it only forces the in-memory feature bitset on
|
||||
after PMS applies its MyPlex feature list.
|
||||
Reference in new issue
Block a user