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,116 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
#
|
||||
# Patch lscr.io/linuxserver/plex with the feature-unlock shared library.
|
||||
#
|
||||
# docker build -f docker/Dockerfile.linuxserver -t plex-crack:lsio .
|
||||
# docker run -d --name plex --network=host \
|
||||
# -e PUID=$(id -u) -e PGID=$(id -g) \
|
||||
# -v /srv/plex/config:/config -v /srv/plex/data:/data \
|
||||
# plex-crack:lsio
|
||||
#
|
||||
# Two stages (mirrors Dockerfile.plexinc):
|
||||
# 1. builder -- zig cross-compile plexmediaserver_crack.so (musl)
|
||||
# 2. runtime -- layer onto lscr.io/linuxserver/plex + override the s6 plex
|
||||
# service so PMS is exec'd (as user 'abc' via s6-setuidgid,
|
||||
# preserving LSIO's permission model) with LD_PRELOAD.
|
||||
#
|
||||
# Why s6-setuidgid is preserved: LSIO's run file drops to the 'abc' user
|
||||
# (uid 911) before exec'ing the PMS binary. If we don't keep that, PMS
|
||||
# would run as root, which (a) breaks LSIO's permission model, (b) prevents
|
||||
# writes to /config and /data which are chowned to abc on first start,
|
||||
# (c) makes the container harder to use (root-owned media library).
|
||||
|
||||
# ── Build args (global -- visible to every FROM) ──────────────────────────
|
||||
ARG PLEX_BASE_IMAGE=lscr.io/linuxserver/plex:latest
|
||||
|
||||
# ── Stage 1: build the musl .so (identical to plexinc) ────────────────────
|
||||
FROM debian:bookworm-slim AS builder
|
||||
|
||||
ARG ZIG_VERSION=0.13.0
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
ca-certificates curl xz-utils \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /src
|
||||
RUN mkdir -p /src/toolchain \
|
||||
&& curl -fsSL \
|
||||
"https://ziglang.org/download/${ZIG_VERSION}/zig-linux-x86_64-${ZIG_VERSION}.tar.xz" \
|
||||
-o /tmp/zig.tar.xz \
|
||||
&& tar -C /src/toolchain --strip-components=1 -xf /tmp/zig.tar.xz \
|
||||
&& rm /tmp/zig.tar.xz
|
||||
ENV PATH="/src/toolchain:${PATH}"
|
||||
|
||||
COPY build.sh ./
|
||||
COPY src ./src
|
||||
COPY third_party ./third_party
|
||||
RUN bash build.sh
|
||||
|
||||
# ── Stage 2: runtime -- patch lscr.io/linuxserver/plex ────────────────────
|
||||
FROM ${PLEX_BASE_IMAGE} AS runtime
|
||||
|
||||
ARG PLEX_BASE_IMAGE
|
||||
ARG PATCH_VERSION=dev
|
||||
LABEL org.opencontainers.image.title="plexmediaserver-crack (linuxserver)" \
|
||||
org.opencontainers.image.source="https://github.com/authrequest/Freeloader" \
|
||||
org.opencontainers.image.licenses="AGPL-3.0-or-later" \
|
||||
plex_patch.base="${PLEX_BASE_IMAGE}" \
|
||||
plex_patch.version="${PATCH_VERSION}"
|
||||
|
||||
# Sanity: refuse to build on an unfamiliar upstream layout.
|
||||
RUN set -eux; \
|
||||
PMS="/usr/lib/plexmediaserver/Plex Media Server"; \
|
||||
PMS_LIB="/usr/lib/plexmediaserver/lib"; \
|
||||
RUN_SCRIPT="/etc/s6-overlay/s6-rc.d/svc-plex/run"; \
|
||||
[ -x "${PMS}" ] || { echo "patcher: missing ${PMS} in ${PLEX_BASE_IMAGE}"; exit 1; }; \
|
||||
[ -d "${PMS_LIB}" ] || { echo "patcher: missing ${PMS_LIB}/ in ${PLEX_BASE_IMAGE}"; exit 1; }; \
|
||||
[ -f "${RUN_SCRIPT}" ] || { echo "patcher: missing ${RUN_SCRIPT} in ${PLEX_BASE_IMAGE}"; exit 1; }
|
||||
|
||||
COPY --from=builder /src/build/plexmediaserver_crack.so \
|
||||
/usr/lib/plexmediaserver/lib/plexmediaserver_crack.so
|
||||
COPY --from=builder /src/build/plexmediaserver_traffic_logger.so \
|
||||
/usr/lib/plexmediaserver/lib/plexmediaserver_traffic_logger.so
|
||||
COPY docker/wrapper.sh /usr/lib/plexmediaserver/plex-crack-wrapper.sh
|
||||
RUN chmod 0755 /usr/lib/plexmediaserver/plex-crack-wrapper.sh
|
||||
|
||||
# Override the s6 plex service run file. We preserve LSIO's s6-setuidgid
|
||||
# abc so PMS still runs as user 'abc' (uid 911) -- otherwise /config and
|
||||
# /data would be created as root and break the LSIO permission model.
|
||||
RUN set -eux; \
|
||||
RUN_SCRIPT="/etc/s6-overlay/s6-rc.d/svc-plex/run"; \
|
||||
cp "${RUN_SCRIPT}" "${RUN_SCRIPT}.orig"; \
|
||||
printf '#!/usr/bin/with-contenv bash\nexec s6-setuidgid abc /usr/lib/plexmediaserver/plex-crack-wrapper.sh\n' \
|
||||
> "${RUN_SCRIPT}"; \
|
||||
chmod 0755 "${RUN_SCRIPT}"
|
||||
|
||||
# ── Patch the PMS binary in-place to disable transcode session limits ──────
|
||||
# Patches 6 conditional-jump instructions so the limit-checking code always
|
||||
# takes the normal (no-error) path. See AGENTS.md for full RE notes.
|
||||
RUN set -eux; \
|
||||
PMS="/usr/lib/plexmediaserver/Plex Media Server"; \
|
||||
\
|
||||
# Read N bytes at a file offset as hex string (no separator). \
|
||||
rd() { dd if="${PMS}" bs=1 skip="$1" count="$2" 2>/dev/null | od -A n -t x1 | tr -d ' \n'; }; \
|
||||
\
|
||||
# sanity: verify expected bytes at each patch site (guards against version drift) \
|
||||
echo "=== verifying patch-site bytes ==="; \
|
||||
[ "$(rd 18624607 2)" = "7e0c" ] || { echo "sanity FAIL at 0x11C305F"; exit 1; }; \
|
||||
[ "$(rd 18624781 2)" = "7e08" ] || { echo "sanity FAIL at 0x11C310D"; exit 1; }; \
|
||||
[ "$(rd 18624793 2)" = "7e08" ] || { echo "sanity FAIL at 0x11C3119"; exit 1; }; \
|
||||
[ "$(rd 18624674 6)" = "0f8fc2f6ffff" ] || { echo "sanity FAIL at 0x11C30A2"; exit 1; }; \
|
||||
[ "$(rd 18624682 6)" = "0f8f1ef7ffff" ] || { echo "sanity FAIL at 0x11C30AA"; exit 1; }; \
|
||||
[ "$(rd 18624630 6)" = "0f8f9f000000" ] || { echo "sanity FAIL at 0x11C3076"; exit 1; }; \
|
||||
echo "sanity: expected bytes match, applying patches"; \
|
||||
\
|
||||
# apply patches \
|
||||
# jle -> jmp (2-byte: \353=0xEB \14=0x0C \10=0x08) \
|
||||
printf '\353\014' | dd of="${PMS}" bs=1 seek=18624607 conv=notrunc 2>/dev/null; \
|
||||
printf '\353\010' | dd of="${PMS}" bs=1 seek=18624781 conv=notrunc 2>/dev/null; \
|
||||
printf '\353\010' | dd of="${PMS}" bs=1 seek=18624793 conv=notrunc 2>/dev/null; \
|
||||
# jg -> NOP (6-byte: \220=0x90) \
|
||||
printf '\220\220\220\220\220\220' | dd of="${PMS}" bs=1 seek=18624674 conv=notrunc 2>/dev/null; \
|
||||
printf '\220\220\220\220\220\220' | dd of="${PMS}" bs=1 seek=18624682 conv=notrunc 2>/dev/null; \
|
||||
printf '\220\220\220\220\220\220' | dd of="${PMS}" bs=1 seek=18624630 conv=notrunc 2>/dev/null; \
|
||||
\
|
||||
echo "binary patch applied: transcode session limits disabled"
|
||||
@@ -0,0 +1,91 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
#
|
||||
# Patch plexinc/pms-docker with the feature-unlock shared library.
|
||||
#
|
||||
# docker build -f docker/Dockerfile.plexinc -t plex-crack:plexinc .
|
||||
# docker run -d --name plex --network=host \
|
||||
# -v /srv/plex/config:/config -v /srv/plex/data:/data \
|
||||
# plex-crack:plexinc
|
||||
#
|
||||
# Two stages:
|
||||
# 1. builder -- zig 0.13.0 cross-compile plexmediaserver_crack.so (musl)
|
||||
# 2. runtime -- layer it onto plexinc/pms-docker + override the s6 plex
|
||||
# service so PMS is exec'd with LD_PRELOAD=...crack.so.
|
||||
# The .so's constructor (src/main.cpp) calls unsetenv, so
|
||||
# PMS's glibc helper children (Tuner, Script Host) are
|
||||
# unaffected.
|
||||
#
|
||||
# Patch invariants are enforced at build time (RUN sanity): the upstream
|
||||
# layout must match what the wrapper assumes. If plexinc/pms-docker
|
||||
# restructures, the build fails here rather than the container failing
|
||||
# mysteriously at runtime.
|
||||
|
||||
# ── Build args (global -- visible to every FROM) ──────────────────────────
|
||||
ARG PLEX_BASE_IMAGE=plexinc/pms-docker:latest
|
||||
|
||||
# ── Stage 1: build the musl .so ────────────────────────────────────────────
|
||||
FROM debian:bookworm-slim AS builder
|
||||
|
||||
ARG ZIG_VERSION=0.13.0
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
ca-certificates curl xz-utils \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
# Toolchain layer (cached across source-only changes).
|
||||
RUN mkdir -p /src/toolchain \
|
||||
&& curl -fsSL \
|
||||
"https://ziglang.org/download/${ZIG_VERSION}/zig-linux-x86_64-${ZIG_VERSION}.tar.xz" \
|
||||
-o /tmp/zig.tar.xz \
|
||||
&& tar -C /src/toolchain --strip-components=1 -xf /tmp/zig.tar.xz \
|
||||
&& rm /tmp/zig.tar.xz
|
||||
ENV PATH="/src/toolchain:${PATH}"
|
||||
|
||||
# Build sources. The .dockerignore at the repo root whitelists these.
|
||||
COPY build.sh ./
|
||||
COPY src ./src
|
||||
COPY third_party ./third_party
|
||||
RUN bash build.sh
|
||||
# build.sh writes /src/build/plexmediaserver_crack.so (musl).
|
||||
|
||||
# ── Stage 2: runtime -- patch plexinc/pms-docker ──────────────────────────
|
||||
FROM ${PLEX_BASE_IMAGE} AS runtime
|
||||
|
||||
ARG PLEX_BASE_IMAGE
|
||||
ARG PATCH_VERSION=dev
|
||||
LABEL org.opencontainers.image.title="plexmediaserver-crack (plexinc)" \
|
||||
org.opencontainers.image.source="https://github.com/authrequest/Freeloader" \
|
||||
org.opencontainers.image.licenses="AGPL-3.0-or-later" \
|
||||
plex_patch.base="${PLEX_BASE_IMAGE}" \
|
||||
plex_patch.version="${PATCH_VERSION}"
|
||||
|
||||
# Sanity: refuse to build on an unfamiliar upstream layout.
|
||||
RUN set -eux; \
|
||||
PMS="/usr/lib/plexmediaserver/Plex Media Server"; \
|
||||
PMS_LIB="/usr/lib/plexmediaserver/lib"; \
|
||||
RUN_SCRIPT="/etc/s6-overlay/s6-rc.d/svc-plex/run"; \
|
||||
[ -x "${PMS}" ] || { echo "patcher: missing ${PMS} in ${PLEX_BASE_IMAGE}"; exit 1; }; \
|
||||
[ -d "${PMS_LIB}" ] || { echo "patcher: missing ${PMS_LIB}/ in ${PLEX_BASE_IMAGE}"; exit 1; }; \
|
||||
[ -f "${RUN_SCRIPT}" ] || { echo "patcher: missing ${RUN_SCRIPT} in ${PLEX_BASE_IMAGE}"; exit 1; }
|
||||
|
||||
# Drop the .so and the in-container launcher.
|
||||
COPY --from=builder /src/build/plexmediaserver_crack.so \
|
||||
/usr/lib/plexmediaserver/lib/plexmediaserver_crack.so
|
||||
COPY --from=builder /src/build/plexmediaserver_traffic_logger.so \
|
||||
/usr/lib/plexmediaserver/lib/plexmediaserver_traffic_logger.so
|
||||
COPY docker/wrapper.sh /usr/lib/plexmediaserver/plex-crack-wrapper.sh
|
||||
RUN chmod 0755 /usr/lib/plexmediaserver/plex-crack-wrapper.sh
|
||||
|
||||
# Override the s6 plex service run file. The original is kept as .orig for
|
||||
# forensics / downgrade (rebuild against the unpatched image to revert).
|
||||
# plexinc's upstream service already runs as the 'plex' user, so we do not
|
||||
# add s6-setuidgid here -- LSIO is the image that needs it (see its Dockerfile).
|
||||
RUN set -eux; \
|
||||
RUN_SCRIPT="/etc/s6-overlay/s6-rc.d/svc-plex/run"; \
|
||||
cp "${RUN_SCRIPT}" "${RUN_SCRIPT}.orig"; \
|
||||
printf '#!/usr/bin/with-contenv bash\nexec /usr/lib/plexmediaserver/plex-crack-wrapper.sh\n' \
|
||||
> "${RUN_SCRIPT}"; \
|
||||
chmod 0755 "${RUN_SCRIPT}"
|
||||
@@ -0,0 +1,113 @@
|
||||
# Docker support for plexmediaserver_crack
|
||||
|
||||
Patches Plex Media Server running in Docker — both the official
|
||||
[`plexinc/pms-docker`](https://hub.docker.com/r/plexinc/pms-docker) image
|
||||
and the community [`lscr.io/linuxserver/plex`](https://hub.docker.com/r/linuxserver/plex)
|
||||
image — using the same `LD_PRELOAD`-on-the-PMS-exec mechanism as the native
|
||||
systemd install. See the top-level [README](../README.md) and the full guide
|
||||
[`docs/DOCKER.md`](../docs/DOCKER.md) for the why and the troubleshooting.
|
||||
|
||||
## Build
|
||||
|
||||
From the project root:
|
||||
|
||||
```bash
|
||||
# Official image (plexinc/pms-docker)
|
||||
docker build -f docker/Dockerfile.plexinc -t plex-crack:plexinc .
|
||||
|
||||
# Community image (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. Pin the base with `--build-arg PLEX_BASE_IMAGE=...` if
|
||||
you need reproducibility across PMS updates.
|
||||
|
||||
## Run
|
||||
|
||||
```bash
|
||||
# plexinc (no PUID/PGID; the image runs PMS 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
|
||||
|
||||
# linuxserver (honor PUID/PGID so /config and /data chown 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
|
||||
```
|
||||
|
||||
Then:
|
||||
|
||||
```bash
|
||||
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:32400/identity # -> 200
|
||||
```
|
||||
|
||||
## Patch in place (no rebuild)
|
||||
|
||||
If you already have a Plex container running and don't want to rebuild
|
||||
the image or recreate the container, `plex-docker-patch.sh` applies the
|
||||
same patch to a live container — no `docker build` needed, original
|
||||
image untouched, original container + its volumes preserved. Revertible
|
||||
via `uninstall` (a `.orig` copy of the s6 `run` file is kept).
|
||||
|
||||
```bash
|
||||
# Default container name: "plex"
|
||||
./docker/plex-docker-patch.sh install
|
||||
|
||||
# Custom container name
|
||||
./docker/plex-docker-patch.sh install my-plex
|
||||
|
||||
# Revert (restores the s6 run file from its .orig)
|
||||
./docker/plex-docker-patch.sh uninstall my-plex
|
||||
|
||||
# Status
|
||||
./docker/plex-docker-patch.sh status my-plex
|
||||
```
|
||||
|
||||
The script auto-detects the base image (plexinc vs LSIO) by reading the
|
||||
s6 `run` file content inside the container, so the same `.so` and
|
||||
`wrapper.sh` are used in both cases. Zig must be available on the host
|
||||
(the script invokes `build.sh`); a pre-existing
|
||||
`build/plexmediaserver_crack.so` is reused.
|
||||
|
||||
### When to use which
|
||||
|
||||
- **Dockerfile build** (the `docker build` flow above) — best for
|
||||
repeat deploys, multi-host, CI/CD, immutable images. You commit a
|
||||
patched image and ship it.
|
||||
- **`plex-docker-patch.sh`** — best for one-off patching of a running
|
||||
container you don't want to touch. Modifies the live container's
|
||||
filesystem; fully revertible via `uninstall`.
|
||||
|
||||
## What's where
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `Dockerfile.plexinc` | Multi-stage build → patched `plexinc/pms-docker` |
|
||||
| `Dockerfile.linuxserver` | Multi-stage build → patched `lscr.io/linuxserver/plex` |
|
||||
| `wrapper.sh` | In-container launcher (env → `LD_PRELOAD` last → `exec` PMS) |
|
||||
| `plex-docker-patch.sh` | In-place patcher for a running container (`install` / `uninstall` / `status`) |
|
||||
|
||||
For docker-compose, signature drift, verification with
|
||||
`scripts/readbitset.py`, uninstall, and troubleshooting, see
|
||||
[`../docs/DOCKER.md`](../docs/DOCKER.md).
|
||||
|
||||
### Quick troubleshooting (in-place patcher)
|
||||
|
||||
| Symptom | Likely cause | First check |
|
||||
|---|---|---|
|
||||
| `install` says `ERROR: docker not on PATH` | docker CLI not installed or user not in `docker` group | `docker version` (must run as you) |
|
||||
| `install` says `could not find s6 svc-plex run file` | upstream image changed its s6 layout | `docker exec <name> ls -la /etc/s6-overlay/s6-rc.d/svc-plex/ /etc/services.d/plex/` — open an issue with output |
|
||||
| `install` succeeds but `.so is NOT in /proc/$PID/maps` | PMS exited 127 (loader failure) | `docker logs <name> \| tail -50` — usually a glibc `.so` got in (rebuild with `build.sh`) |
|
||||
| `status` shows `PATCH IS NOT ACTIVE` after `install` | run file wasn't rewritten (e.g., readonly layer) or container wasn't restarted | `docker exec <name> cat /etc/s6-overlay/s6-rc.d/svc-plex/run` — should print `exec .../plex-crack-wrapper.sh` |
|
||||
| `uninstall` says `no run.orig found` | `.orig` was deleted, or the run file was never backed up (e.g., you ran an older version of the script) | restore manually: `docker cp <upstream-image>:/etc/s6-overlay/s6-rc.d/svc-plex/run <name>:/etc/s6-overlay/s6-rc.d/svc-plex/run` |
|
||||
|
||||
For deeper diagnostics (PMS exit 127, glibc vs musl ABI, LSIO `/config`
|
||||
ownership, signature drift on PMS updates), see
|
||||
[`../docs/DOCKER.md`](../docs/DOCKER.md#troubleshooting).
|
||||
@@ -0,0 +1,551 @@
|
||||
#!/usr/bin/env bash
|
||||
# docker/plex-docker-patch.sh
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
#
|
||||
# Patch a running Plex Media Server container in place.
|
||||
#
|
||||
# ── Overview ──────────────────────────────────────────────────────────────
|
||||
# Same LD_PRELOAD mechanism as the Dockerfile-based approach: the s6
|
||||
# `svc-plex` `run` file is rewritten to exec the in-container wrapper,
|
||||
# which sets LD_PRELOAD last and execs the PMS binary. The .so's
|
||||
# constructor (src/main.cpp) calls unsetenv, so glibc helper children
|
||||
# (Tuner, Script Host, transcoders) are unaffected.
|
||||
#
|
||||
# No `docker build`, no container recreate, original image untouched.
|
||||
# Revertible via `uninstall` (a `.orig` copy of the s6 `run` file is kept).
|
||||
#
|
||||
# ── Usage ──────────────────────────────────────────────────────────────────
|
||||
# plex-docker-patch.sh [flags] <subcommand> [container-name]
|
||||
#
|
||||
# Subcommands:
|
||||
# install [name] Apply the patch in place (default if omitted)
|
||||
# uninstall [name] Restore the s6 run file from its .orig
|
||||
# status [name] Report whether the patch is active
|
||||
# help Show usage
|
||||
#
|
||||
# Flags (can appear before or after the subcommand):
|
||||
# --name <name> Container name (alternative to positional)
|
||||
# --no-build Use existing build/plexmediaserver_crack.so; do not invoke build.sh
|
||||
# --force-rebuild Delete build/plexmediaserver_crack.so and rebuild from scratch
|
||||
# --dry-run Print the actions that would be taken without executing them
|
||||
# --verbose, -v Trace every docker/build command to stderr before execution
|
||||
# --quiet, -q Suppress non-essential output (only the final report)
|
||||
# --version Print the script version and exit
|
||||
#
|
||||
# Container name defaults to "plex". Both plexinc/pms-docker and
|
||||
# lscr.io/linuxserver/plex are auto-detected by reading the s6 run file:
|
||||
# LSIO uses `s6-setuidgid abc`; plexinc does not.
|
||||
#
|
||||
# The .so is built locally via the project's build.sh, so a zig-capable
|
||||
# toolchain is required on the host (or an existing build/plexmediaserver_crack.so
|
||||
# is reused). See docs/DOCKER.md for the full guide.
|
||||
#
|
||||
# ── Requirements ──────────────────────────────────────────────────────────
|
||||
# - docker on PATH and accessible to the current user
|
||||
# - bash 4+ (or bash 3.2+ on macOS; arrays + $'...' are used)
|
||||
# - curl + xz-utils (for build.sh) if no prebuilt .so
|
||||
#
|
||||
# ── Idempotency ────────────────────────────────────────────────────────────
|
||||
# install: safe to re-run. The .orig is preserved across re-installs, the
|
||||
# .so and wrapper are overwritten with the latest build, the
|
||||
# run file is rewritten, the container is restarted.
|
||||
# uninstall: safe to re-run. The run file is re-copied from .orig each time.
|
||||
# status: read-only, always safe.
|
||||
#
|
||||
# ── Exit codes ─────────────────────────────────────────────────────────────
|
||||
# 0 success
|
||||
# 1 runtime error (docker missing, container not found, install failed)
|
||||
# 2 usage error (unknown subcommand or flag)
|
||||
#
|
||||
# ── Design notes ──────────────────────────────────────────────────────────
|
||||
# - `set -euo pipefail` for fail-fast. All transient failures must surface
|
||||
# as non-zero exits so callers can detect them.
|
||||
# - `docker exec sh -c '...' _ "${var}"` is the only safe pattern for
|
||||
# passing host-side paths into the container's shell: single-quote the
|
||||
# command so the host bash does NOT interpolate, then pass the path as
|
||||
# a positional arg. Without this, a path containing `;` or `$()` would
|
||||
# be a command-injection vector. (Currently safe because all paths come
|
||||
# from a hardcoded candidate list, but the safe pattern is enforced
|
||||
# throughout for future-proofing.)
|
||||
# - PMS PID lookup scans /proc/*/comm (not ps -ef | grep), which avoids
|
||||
# the "Plex" pattern matching the scanning command itself, and works
|
||||
# regardless of which process-listing tools the container provides.
|
||||
# - All destructive operations route through `run`, which respects
|
||||
# --dry-run and --verbose. This is the single place to look to see what
|
||||
# side effects the script has.
|
||||
# - Logging: stdout is for human-readable status and the final report;
|
||||
# stderr is for warnings, errors, --verbose traces, and --dry-run plans.
|
||||
# This makes the script CI-friendly (pipe stdout, capture stderr).
|
||||
# - `DEBUG=1` env var enables `set -x` for full command tracing.
|
||||
#
|
||||
# ── Version ────────────────────────────────────────────────────────────────
|
||||
SCRIPT_VERSION='1.0.0'
|
||||
|
||||
set -euo pipefail
|
||||
[[ -n "${DEBUG:-}" ]] && set -x
|
||||
|
||||
# ── Paths / constants ─────────────────────────────────────────────────────
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
|
||||
# In-container paths (must match what the Dockerfiles copy to).
|
||||
SO_PATH_INSIDE="/usr/lib/plexmediaserver/lib/plexmediaserver_crack.so"
|
||||
WRAPPER_PATH_INSIDE="/usr/lib/plexmediaserver/plex-crack-wrapper.sh"
|
||||
PMS_COMM_NAME="Plex Media Server" # prctl(PR_SET_NAME) sets this
|
||||
|
||||
# s6-overlay v3 / v2 candidate paths, in preference order. If the upstream
|
||||
# image's s6 layout changes, this is the single place to update.
|
||||
S6_RUN_CANDIDATES=(
|
||||
"/etc/s6-overlay/s6-rc.d/svc-plex/run"
|
||||
"/etc/services.d/plex/run"
|
||||
)
|
||||
|
||||
# Operational defaults.
|
||||
DEFAULT_CONTAINER="plex"
|
||||
MAX_WAIT_ITERATIONS=30
|
||||
WAIT_INTERVAL_SECONDS=2
|
||||
PMS_HTTP_PORT=32400
|
||||
|
||||
# ── Arg-parser state ──────────────────────────────────────────────────────
|
||||
SUBCOMMAND=""
|
||||
CONTAINER_NAME=""
|
||||
DRY_RUN=false
|
||||
VERBOSE=false
|
||||
QUIET=false
|
||||
SKIP_BUILD=false
|
||||
FORCE_REBUILD=false
|
||||
|
||||
# ── Logging ───────────────────────────────────────────────────────────────
|
||||
# stdout -> human-readable status, success messages, final report
|
||||
# stderr -> warnings, errors, --verbose traces, --dry-run plans
|
||||
log() { [ "${QUIET}" != "true" ] && printf '%s\n' "$*"; }
|
||||
warn() { printf 'WARNING: %s\n' "$*" >&2; }
|
||||
die() { printf 'ERROR: %s\n' "$*" >&2; exit 1; }
|
||||
trace() { [ "${VERBOSE}" = "true" ] && printf '+ %s\n' "$*" >&2 || true; }
|
||||
|
||||
# ── Command runner: respects --dry-run and --verbose ──────────────────────
|
||||
# All destructive operations go through this wrapper. In dry-run mode
|
||||
# the command is echoed to stderr and skipped. In verbose mode the
|
||||
# command is echoed to stderr before execution. Stdout is captured by
|
||||
# the caller as usual.
|
||||
run() {
|
||||
if [ "${DRY_RUN}" = "true" ]; then
|
||||
printf '[dry-run]'
|
||||
local arg
|
||||
for arg in "$@"; do
|
||||
printf ' %s' "${arg}"
|
||||
done
|
||||
printf '\n' >&2
|
||||
else
|
||||
trace "$*"
|
||||
"$@"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Usage ─────────────────────────────────────────────────────────────────
|
||||
print_usage() {
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") [flags] <subcommand> [container-name]
|
||||
|
||||
Subcommands:
|
||||
install [name] Apply the patch in place (default)
|
||||
uninstall [name] Revert the s6 run file from its .orig
|
||||
status [name] Report whether the patch is active
|
||||
help Show this message
|
||||
|
||||
Flags (can appear before or after the subcommand):
|
||||
--name <name> Container name (alternative to positional)
|
||||
--no-build Use existing build/plexmediaserver_crack.so; do not invoke build.sh
|
||||
--force-rebuild Delete build/plexmediaserver_crack.so and rebuild from scratch
|
||||
--dry-run Print the actions that would be taken without executing them
|
||||
--verbose, -v Trace every docker/build command to stderr
|
||||
--quiet, -q Suppress non-essential output (only the final report)
|
||||
--version Print the script version and exit
|
||||
|
||||
Container name defaults to "${DEFAULT_CONTAINER}".
|
||||
|
||||
Examples:
|
||||
$(basename "$0") install # default container
|
||||
$(basename "$0") install my-plex # custom name
|
||||
$(basename "$0") --name my-plex install # flag form
|
||||
$(basename "$0") install --dry-run # preview only
|
||||
$(basename "$0") --verbose status my-plex # trace every docker call
|
||||
$(basename "$0") uninstall my-plex
|
||||
|
||||
The script auto-detects the base image (plexinc vs linuxserver) by
|
||||
reading the s6 svc-plex run file inside the container. The same .so and
|
||||
docker/wrapper.sh are used in both cases; only the run file content
|
||||
differs (LSIO preserves s6-setuidgid abc).
|
||||
EOF
|
||||
}
|
||||
|
||||
# ── Arg parsing ───────────────────────────────────────────────────────────
|
||||
parse_args() {
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
install|uninstall|status)
|
||||
if [ -n "${SUBCOMMAND}" ]; then
|
||||
die "subcommand already specified: ${SUBCOMMAND}"
|
||||
fi
|
||||
SUBCOMMAND="$1"
|
||||
shift
|
||||
;;
|
||||
help|-h|--help) print_usage; exit 0 ;;
|
||||
--name) [ "$#" -ge 2 ] || die "--name requires an argument"
|
||||
CONTAINER_NAME="$2"; shift 2 ;;
|
||||
--name=*) CONTAINER_NAME="${1#--name=}"; shift ;;
|
||||
--no-build) SKIP_BUILD=true; shift ;;
|
||||
--force-rebuild) FORCE_REBUILD=true; shift ;;
|
||||
--dry-run) DRY_RUN=true; shift ;;
|
||||
--verbose|-v) VERBOSE=true; shift ;;
|
||||
--quiet|-q) QUIET=true; shift ;;
|
||||
--version) printf '%s\n' "${SCRIPT_VERSION}"; exit 0 ;;
|
||||
--) shift; break ;;
|
||||
-*) die "unknown flag: $1 (try --help)" ;;
|
||||
*)
|
||||
if [ -z "${CONTAINER_NAME}" ]; then
|
||||
CONTAINER_NAME="$1"
|
||||
else
|
||||
die "unexpected positional argument: $1"
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
SUBCOMMAND="${SUBCOMMAND:-install}"
|
||||
CONTAINER_NAME="${CONTAINER_NAME:-${DEFAULT_CONTAINER}}"
|
||||
|
||||
# Mutual-exclusion checks.
|
||||
if [ "${SKIP_BUILD}" = "true" ] && [ "${FORCE_REBUILD}" = "true" ]; then
|
||||
die "--no-build and --force-rebuild are mutually exclusive"
|
||||
fi
|
||||
if [ "${QUIET}" = "true" ] && [ "${VERBOSE}" = "true" ]; then
|
||||
die "--quiet and --verbose are mutually exclusive"
|
||||
fi
|
||||
|
||||
# Make state available to subcommand functions.
|
||||
export SUBCOMMAND CONTAINER_NAME DRY_RUN VERBOSE QUIET SKIP_BUILD FORCE_REBUILD
|
||||
}
|
||||
|
||||
# ── Pre-flight checks ────────────────────────────────────────────────────
|
||||
require_docker() {
|
||||
command -v docker >/dev/null 2>&1 || die "docker not on PATH"
|
||||
}
|
||||
|
||||
require_container_exists() {
|
||||
require_docker
|
||||
if ! docker inspect "${CONTAINER_NAME}" >/dev/null 2>&1; then
|
||||
die "container '${CONTAINER_NAME}' not found. Start one with: docker run -d --name ${CONTAINER_NAME} ..."
|
||||
fi
|
||||
}
|
||||
|
||||
require_container_running() {
|
||||
require_container_exists
|
||||
local state
|
||||
state="$(docker inspect --format '{{.State.Running}}' "${CONTAINER_NAME}" 2>/dev/null || echo unknown)"
|
||||
if [ "${state}" != "true" ]; then
|
||||
die "container '${CONTAINER_NAME}' is not running (state: ${state}). Start it with: docker start ${CONTAINER_NAME}"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Detection ─────────────────────────────────────────────────────────────
|
||||
|
||||
# Echoes the s6 svc-plex run file path on stdout, or returns 1.
|
||||
# Two candidates are tried in preference order: s6-overlay v3 path, then
|
||||
# the legacy v2 path.
|
||||
detect_run_script() {
|
||||
local p
|
||||
for p in "${S6_RUN_CANDIDATES[@]}"; do
|
||||
if run docker exec -u root "${CONTAINER_NAME}" test -f "${p}" 2>/dev/null; then
|
||||
printf '%s\n' "${p}"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# Echoes one of: plexinc, linuxserver, unknown
|
||||
# Detection: LSIO's run file content includes `s6-setuidgid abc`; plexinc's
|
||||
# does not. If neither marker is found, return `unknown` (the install
|
||||
# flow will refuse to proceed).
|
||||
detect_base_image() {
|
||||
local run_script="$1"
|
||||
local content
|
||||
content="$(run docker exec -u root "${CONTAINER_NAME}" cat "${run_script}" 2>/dev/null || true)"
|
||||
if printf '%s' "${content}" | grep -q 's6-setuidgid abc'; then
|
||||
printf 'linuxserver\n'
|
||||
elif printf '%s' "${content}" | grep -qE 'Plex Media Server|start\.sh|with-contenv'; then
|
||||
printf 'plexinc\n'
|
||||
else
|
||||
printf 'unknown\n'
|
||||
fi
|
||||
}
|
||||
|
||||
# Echoes the PMS PID on stdout, or empty.
|
||||
# Strategy: scan /proc/*/comm for the PMS comm name (set via
|
||||
# prctl(PR_SET_NAME)). This avoids the "ps -ef | grep | grep -v grep"
|
||||
# pattern, which has a tendency to match its own command line, and works
|
||||
# regardless of which process-listing tools are in the container.
|
||||
find_pms_pid() {
|
||||
run docker exec "${CONTAINER_NAME}" sh -c '
|
||||
for d in /proc/[0-9]*; do
|
||||
[ -r "$d/comm" ] || continue
|
||||
if [ "$(cat "$d/comm" 2>/dev/null)" = "$1" ]; then
|
||||
basename "$d"
|
||||
exit 0
|
||||
fi
|
||||
done
|
||||
exit 1
|
||||
' _ "${PMS_COMM_NAME}" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# ── Build ─────────────────────────────────────────────────────────────────
|
||||
build_so() {
|
||||
local so_path="${PROJECT_ROOT}/build/plexmediaserver_crack.so"
|
||||
|
||||
if [ "${FORCE_REBUILD}" = "true" ] && [ -f "${so_path}" ]; then
|
||||
log "[*] --force-rebuild: removing existing .so"
|
||||
run rm -f "${so_path}"
|
||||
fi
|
||||
|
||||
if [ "${SKIP_BUILD}" = "true" ]; then
|
||||
if [ ! -f "${so_path}" ]; then
|
||||
die "--no-build specified but ${so_path} does not exist. Build it first with: bash build.sh"
|
||||
fi
|
||||
log "[*] --no-build: reusing existing .so (build.sh not invoked)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ -f "${so_path}" ]; then
|
||||
log "[*] Reusing existing .so (rm it or pass --force-rebuild to rebuild)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log "[*] Building .so via build.sh (this may take 1-2 min on first run)..."
|
||||
( cd "${PROJECT_ROOT}" && run bash build.sh )
|
||||
}
|
||||
|
||||
# ── Filesystem ops ────────────────────────────────────────────────────────
|
||||
# Build the new s6 run file content locally and docker cp it in. We do
|
||||
# this locally (rather than heredoc-over-docker-exec) to avoid quoting
|
||||
# hell and command-injection risk.
|
||||
write_new_run_file() {
|
||||
local base="$1" out="$2"
|
||||
{
|
||||
printf '#!/usr/bin/with-contenv bash\n'
|
||||
if [ "${base}" = "linuxserver" ]; then
|
||||
printf 'exec s6-setuidgid abc %s\n' "${WRAPPER_PATH_INSIDE}"
|
||||
else
|
||||
printf 'exec %s\n' "${WRAPPER_PATH_INSIDE}"
|
||||
fi
|
||||
} > "${out}"
|
||||
chmod 0755 "${out}"
|
||||
}
|
||||
|
||||
# Back up the s6 run file to .orig. Idempotent: if .orig exists, leave it.
|
||||
# Uses the safe `sh -c '...' _ "${path}"` pattern: single-quoted command
|
||||
# + positional arg, so the host bash never interpolates the path.
|
||||
backup_run_file() {
|
||||
local run_script="$1"
|
||||
if run docker exec -u root "${CONTAINER_NAME}" test -f "${run_script}.orig" 2>/dev/null; then
|
||||
log "[*] ${run_script}.orig already present, leaving it"
|
||||
return 0
|
||||
fi
|
||||
run docker exec -u root "${CONTAINER_NAME}" \
|
||||
sh -c 'cp "$1" "$1".orig' _ "${run_script}"
|
||||
log "[*] Backed up ${run_script} -> ${run_script}.orig"
|
||||
}
|
||||
|
||||
# Copy the .so and wrapper into the container and chmod the wrapper.
|
||||
copy_artifacts() {
|
||||
local so_host="${PROJECT_ROOT}/build/plexmediaserver_crack.so"
|
||||
local wrapper_host="${SCRIPT_DIR}/wrapper.sh"
|
||||
|
||||
if [ ! -f "${so_host}" ]; then
|
||||
die "${so_host} does not exist. Build it first with: bash build.sh"
|
||||
fi
|
||||
if [ ! -f "${wrapper_host}" ]; then
|
||||
die "${wrapper_host} does not exist. Re-clone the project or restore docker/wrapper.sh."
|
||||
fi
|
||||
|
||||
log "[*] Copying .so and wrapper into the container..."
|
||||
run docker cp "${so_host}" "${CONTAINER_NAME}:${SO_PATH_INSIDE}"
|
||||
run docker cp "${wrapper_host}" "${CONTAINER_NAME}:${WRAPPER_PATH_INSIDE}"
|
||||
run docker exec -u root "${CONTAINER_NAME}" chmod 0755 "${WRAPPER_PATH_INSIDE}"
|
||||
}
|
||||
|
||||
# ── Verification ───────────────────────────────────────────────────────────
|
||||
# Polls PMS /identity until it returns 200 or the timeout expires.
|
||||
wait_for_pms_ready() {
|
||||
log "[*] Waiting for PMS /identity (up to $((MAX_WAIT_ITERATIONS * WAIT_INTERVAL_SECONDS))s)..."
|
||||
local i code
|
||||
for i in $(seq 1 "${MAX_WAIT_ITERATIONS}"); do
|
||||
code="$(curl -fsS -o /dev/null -w '%{http_code}' "http://127.0.0.1:${PMS_HTTP_PORT}/identity" 2>/dev/null || echo 000)"
|
||||
if [ "${code}" = "200" ]; then
|
||||
log "[*] PMS is up (HTTP 200)."
|
||||
return 0
|
||||
fi
|
||||
sleep "${WAIT_INTERVAL_SECONDS}"
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# Confirms the .so is mapped into the PMS process.
|
||||
verify_so_mapped() {
|
||||
local pms_pid="$1"
|
||||
if [ -z "${pms_pid}" ]; then
|
||||
warn "could not find PMS pid inside container"
|
||||
return 1
|
||||
fi
|
||||
if run docker exec "${CONTAINER_NAME}" grep -F plexmediaserver_crack.so "/proc/${pms_pid}/maps" >/dev/null 2>&1; then
|
||||
log "[*] OK: plexmediaserver_crack.so is mapped into PMS (pid ${pms_pid})"
|
||||
return 0
|
||||
fi
|
||||
warn "PMS running (pid ${pms_pid}) but .so is NOT in /proc/${pms_pid}/maps"
|
||||
warn " This usually means PMS exited 127 (loader failure). Check: docker logs ${CONTAINER_NAME} | tail -50"
|
||||
return 1
|
||||
}
|
||||
|
||||
# ── Subcommands ──────────────────────────────────────────────────────────
|
||||
do_install() {
|
||||
require_container_running
|
||||
log "[*] Container: ${CONTAINER_NAME}"
|
||||
|
||||
local run_script
|
||||
if ! run_script="$(detect_run_script)"; then
|
||||
die "could not find s6 svc-plex run file in container. Tried: ${S6_RUN_CANDIDATES[*]}"
|
||||
fi
|
||||
log "[*] s6 run file: ${run_script}"
|
||||
|
||||
local base
|
||||
base="$(detect_base_image "${run_script}")"
|
||||
case "${base}" in
|
||||
linuxserver) log "[*] Detected base: lscr.io/linuxserver/plex (s6-setuidgid abc will be preserved)" ;;
|
||||
plexinc) log "[*] Detected base: plexinc/pms-docker" ;;
|
||||
*)
|
||||
die "could not detect base image from s6 run file content. Please file an issue with the file contents."
|
||||
;;
|
||||
esac
|
||||
|
||||
build_so
|
||||
copy_artifacts
|
||||
backup_run_file "${run_script}"
|
||||
|
||||
# Write the new run file locally and copy it in. The mktemp + trap
|
||||
# pattern ensures we never leak the temp file, even on error paths.
|
||||
local tmp_run
|
||||
tmp_run="$(mktemp)"
|
||||
trap 'rm -f "${tmp_run}"' EXIT
|
||||
write_new_run_file "${base}" "${tmp_run}"
|
||||
run docker cp "${tmp_run}" "${CONTAINER_NAME}:${run_script}"
|
||||
rm -f "${tmp_run}"
|
||||
trap - EXIT
|
||||
|
||||
log "[*] Restarting ${CONTAINER_NAME} (s6 will exec the new run file)..."
|
||||
run docker restart "${CONTAINER_NAME}" >/dev/null
|
||||
|
||||
if ! wait_for_pms_ready; then
|
||||
warn "PMS did not return 200 within $((MAX_WAIT_ITERATIONS * WAIT_INTERVAL_SECONDS))s. Check: docker logs ${CONTAINER_NAME} | tail -50"
|
||||
fi
|
||||
|
||||
local pms_pid
|
||||
pms_pid="$(find_pms_pid)"
|
||||
verify_so_mapped "${pms_pid}"
|
||||
|
||||
cat <<EOF
|
||||
|
||||
Patch applied. For full verification (all 14 feature bits ON):
|
||||
docker cp ${PROJECT_ROOT}/scripts/readbitset.py ${CONTAINER_NAME}:/tmp/readbitset.py
|
||||
docker exec -u root ${CONTAINER_NAME} python3 /tmp/readbitset.py ${pms_pid:-<PMS_PID>}
|
||||
|
||||
To revert: $(basename "$0") uninstall ${CONTAINER_NAME}
|
||||
To re-check: $(basename "$0") status ${CONTAINER_NAME}
|
||||
EOF
|
||||
}
|
||||
|
||||
do_uninstall() {
|
||||
require_container_running
|
||||
|
||||
local run_script
|
||||
if ! run_script="$(detect_run_script)"; then
|
||||
die "could not find s6 svc-plex run file. Tried: ${S6_RUN_CANDIDATES[*]}"
|
||||
fi
|
||||
|
||||
if ! run docker exec -u root "${CONTAINER_NAME}" test -f "${run_script}.orig" 2>/dev/null; then
|
||||
die "no ${run_script}.orig found -- the patch may not be installed, or the .orig was deleted. Manual restore: docker cp <upstream-image>:${run_script} ${CONTAINER_NAME}:${run_script}"
|
||||
fi
|
||||
|
||||
log "[*] Restoring ${run_script} from .orig..."
|
||||
run docker exec -u root "${CONTAINER_NAME}" \
|
||||
sh -c 'cp "$1" "$2"' _ "${run_script}.orig" "${run_script}"
|
||||
run docker exec -u root "${CONTAINER_NAME}" chmod 0755 "${run_script}"
|
||||
|
||||
log "[*] Restarting ${CONTAINER_NAME}..."
|
||||
run docker restart "${CONTAINER_NAME}" >/dev/null
|
||||
|
||||
cat <<EOF
|
||||
|
||||
Patch removed. PMS is back to upstream behavior. Optional cleanup:
|
||||
docker exec -u root ${CONTAINER_NAME} rm -f ${SO_PATH_INSIDE} ${WRAPPER_PATH_INSIDE}
|
||||
docker exec -u root ${CONTAINER_NAME} rm -f ${run_script}.orig
|
||||
EOF
|
||||
}
|
||||
|
||||
do_status() {
|
||||
require_container_exists
|
||||
|
||||
local run_script
|
||||
if ! run_script="$(detect_run_script)"; then
|
||||
warn "s6 svc-plex run file not found (tried: ${S6_RUN_CANDIDATES[*]})"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local run_content has_orig has_so has_wrapper pms_pid
|
||||
run_content="$(run docker exec -u root "${CONTAINER_NAME}" cat "${run_script}" 2>/dev/null || true)"
|
||||
if run docker exec -u root "${CONTAINER_NAME}" test -f "${run_script}.orig" 2>/dev/null; then
|
||||
has_orig="yes"
|
||||
else
|
||||
has_orig="no"
|
||||
fi
|
||||
if run docker exec -u root "${CONTAINER_NAME}" test -f "${SO_PATH_INSIDE}" 2>/dev/null; then
|
||||
has_so="yes"
|
||||
else
|
||||
has_so="no"
|
||||
fi
|
||||
if run docker exec -u root "${CONTAINER_NAME}" test -x "${WRAPPER_PATH_INSIDE}" 2>/dev/null; then
|
||||
has_wrapper="yes"
|
||||
else
|
||||
has_wrapper="no"
|
||||
fi
|
||||
pms_pid="$(find_pms_pid)"
|
||||
|
||||
log "Container: ${CONTAINER_NAME}"
|
||||
log " s6 run file: ${run_script}"
|
||||
log " .orig present: ${has_orig}"
|
||||
log " .so present: ${has_so} (${SO_PATH_INSIDE})"
|
||||
log " wrapper: ${has_wrapper} (${WRAPPER_PATH_INSIDE})"
|
||||
log " PMS pid: ${pms_pid:-<not running>}"
|
||||
log " run file content:"
|
||||
printf '%s\n' "${run_content}" | sed 's/^/ /'
|
||||
log ""
|
||||
|
||||
if printf '%s' "${run_content}" | grep -q 'plex-crack-wrapper.sh'; then
|
||||
log "[*] PATCH IS ACTIVE (s6 run file points to the wrapper)."
|
||||
if [ -n "${pms_pid}" ] && run docker exec "${CONTAINER_NAME}" grep -F plexmediaserver_crack.so "/proc/${pms_pid}/maps" >/dev/null 2>&1; then
|
||||
log "[*] .so is mapped into PMS (pid ${pms_pid})."
|
||||
else
|
||||
warn "PMS running (pid ${pms_pid:-?}) but .so is NOT in its maps; check docker logs."
|
||||
fi
|
||||
else
|
||||
log "[*] PATCH IS NOT ACTIVE (s6 run file does not point to the wrapper)."
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Main ──────────────────────────────────────────────────────────────────
|
||||
parse_args "$@"
|
||||
case "${SUBCOMMAND}" in
|
||||
install) do_install ;;
|
||||
uninstall) do_uninstall ;;
|
||||
status) do_status ;;
|
||||
esac
|
||||
@@ -0,0 +1,36 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
#
|
||||
# In-container launcher for Plex Media Server. Used by the patched
|
||||
# plexinc/pms-docker and lscr.io/linuxserver/plex images; both Dockerfiles
|
||||
# replace the upstream s6 service run file so it execs this script.
|
||||
#
|
||||
# Mirrors scripts/plex-crack-wrapper.sh from the native systemd install, with
|
||||
# one key invariant:
|
||||
#
|
||||
# LD_PRELOAD is exported *last*, immediately before `exec`, so the glibc
|
||||
# shell helpers spawned for the PLEX_MEDIA_SERVER_INFO_* assignments
|
||||
# (grep/awk/uname/tr) are NOT preloaded. The .so's constructor
|
||||
# (src/main.cpp) calls unsetenv("LD_PRELOAD") when it loads into the
|
||||
# (musl) PMS process, so PMS's glibc helper children (Tuner Service,
|
||||
# Script Host, transcoders) are unaffected as well.
|
||||
#
|
||||
# `exec` is mandatory so s6-supervise sees PMS as the supervised process
|
||||
# (no fork). "$@" preserves any args the upstream invocation might add.
|
||||
|
||||
set -eu
|
||||
|
||||
# 1. Platform strings PMS echoes in /identity. Mirrors upstream start.sh.
|
||||
export PLEX_MEDIA_SERVER_INFO_VENDOR="$(grep ^NAME= /etc/os-release | awk -F= '{print $2}' | tr -d '"')"
|
||||
export PLEX_MEDIA_SERVER_INFO_MODEL="$(uname -m)"
|
||||
export PLEX_MEDIA_SERVER_INFO_PLATFORM_VERSION="$(grep ^VERSION= /etc/os-release | awk -F= '{print $2}' | tr -d '"')"
|
||||
|
||||
# 2. Set LD_PRELOAD only now. The .so paths match where the Dockerfiles
|
||||
# copy them. Do NOT change this without updating both Dockerfiles.
|
||||
PRELOADS=""
|
||||
PRELOADS="${PRELOADS}:/usr/lib/plexmediaserver/lib/plexmediaserver_crack.so"
|
||||
PRELOADS="${PRELOADS}:/usr/lib/plexmediaserver/lib/plexmediaserver_traffic_logger.so"
|
||||
export LD_PRELOAD="${PRELOADS#:}"
|
||||
|
||||
# 3. Hand off to PMS.
|
||||
exec "/usr/lib/plexmediaserver/Plex Media Server" "$@"
|
||||
Reference in new issue
Block a user