- 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)
352 lines
13 KiB
Markdown
352 lines
13 KiB
Markdown
# 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.
|