Skip to content

Latest commit

 

History

History
351 lines (273 loc) · 12.7 KB

File metadata and controls

351 lines (273 loc) · 12.7 KB

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) 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_PRELOADpatchelf 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) 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:

# 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:

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:

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

# 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

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

# 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.

# 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 cps 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 restarts 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.

# 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:

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

# 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:

./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 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:

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. After fixing the patterns, rebuild with --no-cache so the builder stage re-runs:

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.