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