diff --git a/apps/brave/Dockerfile b/apps/brave/Dockerfile index 53c4745b..9cd0302c 100644 --- a/apps/brave/Dockerfile +++ b/apps/brave/Dockerfile @@ -12,6 +12,12 @@ RUN set -eux; apt-get update; \ apt-get update; \ apt-get install -y --no-install-recommends brave-browser; \ # + # install widevine + if [ "${ARCH}" = "arm64" ]; then \ + mkdir -p /opt/brave.com/brave/WidevineCdm; \ + find /var/lib/widevine/WidevineCdm/ ! -name "WidevineCdm" -exec cp -RL {} /opt/brave.com/brave/WidevineCdm/ \; ; \ + fi; \ + # # clean up apt-get clean -y; \ rm -rf /var/lib/apt/lists/* /var/cache/apt/* diff --git a/apps/chromium/Dockerfile b/apps/chromium/Dockerfile index 4a26f186..6e71de51 100644 --- a/apps/chromium/Dockerfile +++ b/apps/chromium/Dockerfile @@ -17,6 +17,9 @@ RUN set -eux; \ ./widevine.sh "${CHROMIUM_DIR}/WidevineCdm"; \ rm -f /widevine.sh; \ apt-get --purge autoremove -y xz-utils; \ + elif [ "${ARCH}" = "arm64" ]; then \ + mkdir -p "${CHROMIUM_DIR}/WidevineCdm"; \ + find /var/lib/widevine/WidevineCdm/ ! -name "WidevineCdm" -exec cp -RL {} "${CHROMIUM_DIR}/WidevineCdm/" \; ; \ else \ echo "Widevine is not supported on ${ARCH}"; \ fi; \ diff --git a/apps/vivaldi/Dockerfile b/apps/vivaldi/Dockerfile index d49156bf..90cb4843 100644 --- a/apps/vivaldi/Dockerfile +++ b/apps/vivaldi/Dockerfile @@ -26,6 +26,12 @@ RUN set -eux; apt-get update; \ echo -e "{\n \"external_crx\": \"${EXT_PATH}\",\n \"external_version\": \"${EXT_VERSION}\"\n}" > "${EXTENSIONS_DIR}"/"${EXT_ID}".json; \ done; \ # + # install widevine + if [ "${ARCH}" = "arm64" ]; then \ + mkdir -p /opt/vivaldi/WidevineCdm; \ + find /var/lib/widevine/WidevineCdm/ ! -name "WidevineCdm" -exec cp -RL {} /opt/vivaldi/WidevineCdm/ \; ; \ + fi; \ + # # clean up apt-get --purge autoremove -y xz-utils jq; \ apt-get clean -y; \ diff --git a/runtime/Dockerfile b/runtime/Dockerfile index 6088b8f3..e67ea95b 100644 --- a/runtime/Dockerfile +++ b/runtime/Dockerfile @@ -7,6 +7,10 @@ ARG USERNAME=neko ARG USER_UID=1000 ARG USER_GID=$USER_UID +# +# copy widevine installer +COPY widevine-installer/ /widevine/ + # # install dependencies ENV DEBIAN_FRONTEND=noninteractive @@ -50,6 +54,14 @@ RUN set -eux; \ chown $USERNAME /var/log/neko/ /tmp/runtime-$USERNAME; \ chown -R $USERNAME:$USERNAME /home/$USERNAME; \ # + # install widevine for arm64 + ARCH=$(dpkg --print-architecture); \ + if [ "${ARCH}" = "arm64" ]; then \ + apt-get install -y --no-install-recommends squashfs-tools; \ + chmod +x /widevine/widevine*; \ + printf "\n\n" | /widevine/widevine-installer; \ + apt-get --purge autoremove -y squashfs-tools; \ + fi; \ # install fonts apt-get install -y --no-install-recommends \ # Emojis @@ -68,7 +80,8 @@ RUN set -eux; \ # # clean up apt-get clean -y; \ - rm -rf /var/lib/apt/lists/* /var/cache/apt/* + rm -rf /var/lib/apt/lists/* /var/cache/apt/*; \ + rm -rf /widevine # # copy runtime configs diff --git a/runtime/widevine-installer/LICENSE b/runtime/widevine-installer/LICENSE new file mode 100644 index 00000000..67f61c24 --- /dev/null +++ b/runtime/widevine-installer/LICENSE @@ -0,0 +1,23 @@ +MIT License + +Copyright (c) 2023 David Buchanan +Copyright The Asahi Linux Contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/runtime/widevine-installer/widevine-installer b/runtime/widevine-installer/widevine-installer new file mode 100755 index 00000000..1274ed6a --- /dev/null +++ b/runtime/widevine-installer/widevine-installer @@ -0,0 +1,132 @@ +#!/bin/sh +# SPDX-License-Identifier: MIT + +set -e + +# Set to 0 if packaged and the configs are pre-installed +: ${DESTDIR:=/} +: ${CONFDIR:=/etc} +: ${LIBDIR:=/usr/lib64} +: ${BINDIR:=/usr/bin} +: ${LIBEXECDIR:=/usr/libexec} +: ${STATEDIR:=/var/lib} +: ${ENVIRONMENTDIR:=/usr/lib/environment.d} +: ${SCRIPT_BASE:=$LIBEXECDIR/widevine-installer} +: ${INSTALL_BASE:=$STATEDIR/widevine} +: ${DISTFILES_BASE:=https://commondatastorage.googleapis.com/chromeos-localmirror/distfiles} +: ${LACROS_NAME:=chromeos-lacros-arm64-squash-zstd} +: ${LACROS_VERSION:=120.0.6098.0} +: ${WIDEVINE_VERSION:=4.10.2662.3} +: ${CHROME_WIDEVINE_BASE:=$LIBDIR/chromium-browser} +: ${MOZ_PREF_BASE:=$LIBDIR/firefox/defaults/pref} + +[ -e $(dirname "$0")/widevine_fixup.py ] && SCRIPT_BASE="$(realpath "$(dirname "$0")")" + +if [ "$1" = "--distinstall" ]; then + install_configs + install -d -m 0755 "$DESTDIR/$LIBEXECDIR/widevine-installer" + install -p -m 0644 -t "$DESTDIR/$LIBEXECDIR/widevine-installer" ./widevine_fixup.py + install -d -m 0755 "$DESTDIR/$BINDIR" + install -p -m 0755 -t "$DESTDIR/$BINDIR" ./widevine-installer + exit 0 +fi + +if [ "$(uname -m)" != "aarch64" ]; then + echo "This tool is only supported on aarch64 (ARM64) systems." + exit 1 +fi + +if [ "$(whoami)" != "root" ]; then + echo "This tool needs to be run as root." + exit 1 +fi + +verchk() { + ( + echo "2.36" + ldd --version | head -1 | cut -d" " -f4 + ) | sort -CV +} + +if ! verchk; then + echo "Your glibc version is too old. Widevine requires glibc 2.36 or newer." + exit 1 +fi + +echo "This script will download, adapt, and install a copy of the Widevine" +echo "Content Decryption Module for aarch64 systems." +echo +echo "Widevine is a proprietary DRM technology developed by Google." +echo "This script uses ARM64 builds intended for ChromeOS images and is" +echo "not supported nor endorsed by Google. The Asahi Linux community" +echo "also cannot provide direct support for using this proprietary" +echo "software, nor any guarantees about its security, quality," +echo "functionality, nor privacy. You assume all responsibility for" +echo "usage of this script and of the installed CDM." +echo +echo "This installer will only adapt the binary file format of the CDM" +echo "for interoperability purposes, to make it function on vanilla" +echo "ARM64 systems (instead of just ChromeOS). The CDM software" +echo "itself will not be modified in any way." +echo +echo "Widevine version to be installed: $WIDEVINE_VERSION" +echo +echo "Press enter to proceed, or Ctrl-C to cancel." +read dummy + +workdir="$(mktemp -d /tmp/widevine-installer.XXXXXXXX)" +[ -z "$workdir" ] && exit 1 # sanity check +[ ! -d "$workdir" ] && exit 1 # sanity check + +cd "$workdir" + +echo "Downloading LaCrOS (Chrome) image..." +URL="$DISTFILES_BASE/$LACROS_NAME-$LACROS_VERSION" +echo "URL: $URL" +curl -# -o lacros.squashfs "$URL" + +echo +echo "Extracting Widevine..." +unsquashfs -q lacros.squashfs 'WidevineCdm/*' + +echo +echo "The Widevine license agreement follows:" +echo +echo =================================================================================== +cat squashfs-root/WidevineCdm/LICENSE +echo =================================================================================== +echo +echo "Press enter to proceed, or Ctrl-C to cancel." +read dummy + +python3 "$SCRIPT_BASE/widevine_fixup.py" squashfs-root/WidevineCdm/_platform_specific/cros_arm64/libwidevinecdm.so libwidevinecdm.so + +echo +echo "Installing..." +install -d -m 0755 "$INSTALL_BASE" +install -p -m 0755 -t "$INSTALL_BASE" libwidevinecdm.so +install -p -m 0644 -t "$INSTALL_BASE" squashfs-root/WidevineCdm/manifest.json +install -p -m 0644 -t "$INSTALL_BASE" squashfs-root/WidevineCdm/LICENSE + +echo "Setting up plugin for Firefox and Chromium-based browsers..." +mkdir -p "$INSTALL_BASE"/gmp-widevinecdm/system-installed +ln -sf ../../manifest.json "$INSTALL_BASE"/gmp-widevinecdm/system-installed/ +ln -sf ../../libwidevinecdm.so "$INSTALL_BASE"/gmp-widevinecdm/system-installed/ +mkdir -p "$INSTALL_BASE"/WidevineCdm/_platform_specific/linux_arm64 +mkdir -p "$INSTALL_BASE"/WidevineCdm/_platform_specific/linux_x64 +# Hack because Chromium hardcodes a check for this right now... +touch "$INSTALL_BASE"/WidevineCdm/_platform_specific/linux_x64/libwidevinecdm.so +ln -sf ../manifest.json "$INSTALL_BASE"/WidevineCdm +ln -sf ../../../libwidevinecdm.so "$INSTALL_BASE"/WidevineCdm/_platform_specific/linux_arm64/ + +echo "Cleaning up..." +cd / +rm -rf "$workdir" + +echo +echo "Installation complete!" +if [ "$COPY_CONFIGS" = 1 ]; then + echo "You may need to log out and back in for the changes to take effect." +else + echo "Please restart your browser for the changes to take effect." +fi diff --git a/runtime/widevine-installer/widevine_fixup.py b/runtime/widevine-installer/widevine_fixup.py new file mode 100755 index 00000000..4c52acc8 --- /dev/null +++ b/runtime/widevine-installer/widevine_fixup.py @@ -0,0 +1,443 @@ +#!/usr/bin/python3 + +""" +MIT License + +Copyright (c) 2023 David Buchanan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +Original script: +https://gist.github.com/DavidBuchanan314/c6b97add51b97e4c3ee95dc890f9e3c8 +""" + +import sys + +verbose = False +args = sys.argv[1:] +if len(args) >= 1 and args[0] == "-v": + verbose = True + args = args[1:] + +if len(args) != 2: + print(f"Usage: {sys.argv[0]} [-v] input.so output.so") + exit() + +def log(s): + if verbose: + print(s) + +""" +Widevine fixup tool for aarch64 systems + +Old aarch64 widevine builds currently only support 4k page sizes. + +This script fixes that, by pre-padding the LOAD segments so that they meet +the alignment constraints required by the loader, and then fixing up the +relevant header offsets to keep the file valid. + +It also injects two functions that are not exported from typical libgccs, into +the empty space at the end of the .text segment. This avoids any LD_PRELOAD +workarounds. (The injected functions are __aarch64_ldadd4_acq_rel +and __aarch64_swp4_acq_rel) + +IMPORTANT NOTE: On systems with >4k page size (e.g. Apple Silicon devices), +using the resulting binary *significantly* weakens the security of your web +browser, in two ways. Firstly, it disables the RELRO security mitigation, and +secondly it creates a RWX mapping. + +This script also adds the necessary GLIBC_ABI_DT_RELR version tag so that +current glibc versions can load the library without requiring any patches. + +Newer Widevine versions do have 64K aligned segments, and do not need the +padding process. They also do not have the same security implications, so its +use is recommended. However, we still adjust the segment offsets to open up +space for adding the missing functions, and to insert the GLIBC_ABI_DT_RELR +version. + +This process is fragile, and may not work as-is on future revisions of widevine. +""" + +import ctypes + +class Elf64_Ehdr(ctypes.Structure): + _fields_ = [ + ('e_ident', ctypes.c_ubyte * 16), + ('e_type', ctypes.c_uint16), + ('e_machine', ctypes.c_uint16), + ('e_version', ctypes.c_uint32), + ('e_entry', ctypes.c_uint64), + ('e_phoff', ctypes.c_uint64), + ('e_shoff', ctypes.c_uint64), + ('e_flags', ctypes.c_uint32), + ('e_ehsize', ctypes.c_uint16), + ('e_phentsize', ctypes.c_uint16), + ('e_phnum', ctypes.c_uint16), + ('e_shentsize', ctypes.c_uint16), + ('e_shnum', ctypes.c_uint16), + ('e_shstrndx', ctypes.c_uint16), + ] + +class Elf64_Phdr(ctypes.Structure): + _fields_ = [ + ('p_type', ctypes.c_uint32), + ('p_flags', ctypes.c_uint32), + ('p_offset', ctypes.c_uint64), + ('p_vaddr', ctypes.c_uint64), + ('p_paddr', ctypes.c_uint64), + ('p_filesz', ctypes.c_uint64), + ('p_memsz', ctypes.c_uint64), + ('p_align', ctypes.c_uint64), + ] + +class P_FLAGS: + """ Flag values for the p_flags field of program headers + """ + PF_X=0x1 + PF_W=0x2 + PF_R=0x4 + PF_MASKOS=0x00FF0000 + PF_MASKPROC=0xFF000000 + +class PT: + PT_NULL=0 + PT_LOAD=1 + PT_DYNAMIC=2 + PT_INTERP=3 + PT_NOTE=4 + PT_SHLIB=5 + PT_PHDR=6 + PT_TLS=7 + PT_LOOS=0x60000000 + PT_HIOS=0x6fffffff + + PT_GNU_EH_FRAME=0x6474e550 + PT_GNU_STACK=0x6474e551 + PT_GNU_RELRO=0x6474e552 + PT_GNU_PROPERTY=0x6474e553 + +class Elf64_Shdr(ctypes.Structure): + _fields_ = [ + ('sh_name', ctypes.c_uint32), + ('sh_type', ctypes.c_uint32), + ('sh_flags', ctypes.c_uint64), + ('sh_addr', ctypes.c_uint64), + ('sh_offset', ctypes.c_uint64), + ('sh_size', ctypes.c_uint64), + ('sh_link', ctypes.c_uint32), + ('sh_info', ctypes.c_uint32), + ('sh_addralign', ctypes.c_uint64), + ('sh_entsize', ctypes.c_uint64), + ] + +class Elf64_Sym(ctypes.Structure): + _fields_ = [ + ('st_name', ctypes.c_uint32), + ('st_info', ctypes.c_ubyte), + ('st_other', ctypes.c_ubyte), + ('st_shndx', ctypes.c_uint16), + ('st_value', ctypes.c_uint64), + ('st_size', ctypes.c_uint64), + ] + +class Elf64_Dyn(ctypes.Structure): + _fields_ = [ + ('d_tag', ctypes.c_uint64), + ('d_val', ctypes.c_uint64), # union with d_ptr + ] + +class D_TAG: # XXX: this is very incomplete + DT_NULL=0 + DT_NEEDED=1 + DT_STRTAB=5 + DT_SONAME=14 + DT_VERNEED=0x6ffffffe + +class Elf64_Rela(ctypes.Structure): + _fields_ = [ + ('r_offset', ctypes.c_uint64), + #('r_info', ctypes.c_uint64), + ('r_type', ctypes.c_uint32), + ('r_symbol', ctypes.c_uint32), + ('r_addend', ctypes.c_int64), + ] + +class Elf64_Verneed(ctypes.Structure): + _fields_ = [ + ('vn_version', ctypes.c_uint16), + ('vn_cnt', ctypes.c_uint16), + ('vn_file', ctypes.c_uint32), + ('vn_aux', ctypes.c_uint32), + ('vn_next', ctypes.c_uint32), + ] + +class Elf64_Vernaux(ctypes.Structure): + _fields_ = [ + ('vna_hash', ctypes.c_uint32), + ('vna_flags', ctypes.c_uint16), + ('vna_other', ctypes.c_uint16), + ('vna_name', ctypes.c_uint32), + ('vna_next', ctypes.c_uint32), + ] + +import mmap +TARGET_PAGE_SIZE = mmap.PAGESIZE +WEAKEN_SECURITY = mmap.PAGESIZE > 0x1000 +inject_addr = None + +weakened_security = False + +""" +0000000000000b24 <__aarch64_ldadd4_acq_rel>: +b24: 2a0003e2 mov w2, w0 +b28: 885ffc20 ldaxr w0, [x1] +b2c: 0b020003 add w3, w0, w2 +b30: 8804fc23 stlxr w4, w3, [x1] +b34: 35ffffa4 cbnz w4, b28 <__aarch64_ldadd4_acq_rel+0x4> +b38: d65f03c0 ret + +0000000000000b3c <__aarch64_swp4_acq_rel>: +b3c: 2a0003e2 mov w2, w0 +b40: 885ffc20 ldaxr w0, [x1] +b44: 8803fc22 stlxr w3, w2, [x1] +b48: 35ffffc3 cbnz w3, b40 <__aarch64_swp4_acq_rel+0x4> +b4c: d65f03c0 ret +""" + +injected_code = bytes.fromhex("e203002a20fc5f880300020b23fc0488a4ffff35c0035fd6e203002a20fc5f8822fc0388c3ffff35c0035fd6") + +with open(args[0], "rb") as infile: + elf = bytearray(infile.read()) + +print(f"Fixing up ChromeOS Widevine CDM module for Linux compatibility...") + +elf_length = len(elf) +elf += bytearray(0x100000) # pre-expand the buffer by more than enough + +ehdr = Elf64_Ehdr.from_buffer(elf) + +phdrs = [ + Elf64_Phdr.from_buffer(memoryview(elf)[ehdr.e_phoff + i * ehdr.e_phentsize:]) + for i in range(ehdr.e_phnum) +] + +adjustments = [] + +def adjust_offset(x): + for index, delta in adjustments: + if x >= index: + x += delta + return x + +def align(a, b): + return (a + b - 1) & ~(b - 1) + +prev = None +remove_relro = False +for phdr in phdrs: + phdr.p_offset = adjust_offset(phdr.p_offset) + if phdr.p_type == PT.PT_DYNAMIC: + phdr_dynamic = phdr + if phdr.p_type == PT.PT_LOAD: + if phdr.p_align < TARGET_PAGE_SIZE: + phdr.p_align = TARGET_PAGE_SIZE + delta_needed = (phdr.p_vaddr - phdr.p_offset) % phdr.p_align + skip_perms_hack = False + if phdr.p_vaddr != phdr.p_offset and not inject_addr: + # Newer CDM versions use 64K alignment, so no longer require this hack. + # However, we still need space to inject the code & modified headers, so + # we might as well still do it. + delta_needed = phdr.p_vaddr - phdr.p_offset + skip_perms_hack = True + if delta_needed: + log(f" Inserting {hex(delta_needed)} bytes at offset {hex(phdr.p_offset)}") + if not inject_addr: + pad_bytes = injected_code + bytes(delta_needed - len(injected_code)) + inject_addr = phdr.p_offset + log(f" Also injecting code at offset {hex(phdr.p_offset)}") + else: + pad_bytes = bytes(delta_needed) + elf[phdr.p_offset:] = pad_bytes + elf[phdr.p_offset:-delta_needed] + adjustments.append((phdr.p_offset, delta_needed)) + elf_length += delta_needed + phdr.p_offset += delta_needed + + # Load the injected bytes up to the align size as part of the previous phdr + align_off = align(prev.p_vaddr + prev.p_filesz, prev.p_align) - prev.p_vaddr - prev.p_filesz + # This could fail if we get unlucky, let's hope not + assert align_off >= len(injected_code) + prev.p_filesz += min(delta_needed, align_off) + prev.p_memsz += min(delta_needed, align_off) + + if WEAKEN_SECURITY and not skip_perms_hack: + phdr.p_flags |= P_FLAGS.PF_X # XXX: this is a hack!!! (at the very least, we should apply it only to the mappings that need it) + remove_relro = True + weakened_security = True + prev = phdr + + if WEAKEN_SECURITY and remove_relro and phdr.p_type == PT.PT_GNU_RELRO: + print(" Neutering relro") # XXX: relro is a security mechanism + phdr.p_type = PT.PT_NOTE + weakened_security = True + +if inject_addr is None: + inject_addr = (elf_length + 3) & ~3 + elf[inject_addr: inject_addr + len(injected_code)] = injected_code + elf_length += 0x10000 + + +free_addr = inject_addr + len(injected_code) +# the section headers have moved +ehdr.e_shoff = adjust_offset(ehdr.e_shoff) + +shdrs = [ + Elf64_Shdr.from_buffer(memoryview(elf)[ehdr.e_shoff + i * ehdr.e_shentsize:]) + for i in range(ehdr.e_shnum) +] + +for shdr in shdrs: + shdr.sh_offset = adjust_offset(shdr.sh_offset) + +strtab = shdrs[ehdr.e_shstrndx] + +def resolve_string(elf, strtab, stridx, count=False): + if count: + str_start = strtab.sh_offset + for _ in range(stridx): + str_start = elf.index(b"\0", str_start) + 1 + else: + str_start = strtab.sh_offset + stridx + str_end = elf.index(b"\0", str_start) + return bytes(elf[str_start:str_end]) + +shdr_by_name = { + resolve_string(elf, strtab, shdr.sh_name): shdr + for shdr in shdrs +} + +# XXX: unfortunately this does not do anything useful! +# It doesn't hurt either, so I'm leaving it here just in case. +dynsym = shdr_by_name[b".dynsym"] +dynstr = shdr_by_name[b".dynstr"] +for i in range(0, dynsym.sh_size, dynsym.sh_entsize): + sym = Elf64_Sym.from_buffer(memoryview(elf)[dynsym.sh_offset + i:]) + name = resolve_string(elf, dynstr, sym.st_name) + if name in [b"__aarch64_ldadd4_acq_rel", b"__aarch64_swp4_acq_rel"]: + log(f" Weak binding {name}") + sym.st_info = (sym.st_info & 0x0f) | (2 << 4) # STB_WEAK + +""" +dynamic = shdr_by_name[b".dynamic"] +for i in range(0, dynamic.sh_size, dynamic.sh_entsize): + dyn = Elf64_Dyn.from_buffer(memoryview(elf)[dynamic.sh_offset + i:]) + if dyn.d_tag == D_TAG.DT_SONAME: + print("hijacking SONAME tag to point to NEEDED libgcc_hide.so") + dyn.d_tag = D_TAG.DT_NEEDED + dyn.d_val = inject_addr - dynstr.sh_offset + dynstr.sh_size = (inject_addr - dynstr.sh_offset) + len(PATH_TO_INJECT) + 1 +""" + +rela_plt = shdr_by_name[b".rela.plt"] +for i in range(0, rela_plt.sh_size, rela_plt.sh_entsize): + rela = Elf64_Rela.from_buffer(memoryview(elf)[rela_plt.sh_offset + i:]) + sym = resolve_string(elf, dynstr, rela.r_symbol, count=True) + if sym in [b"__aarch64_ldadd4_acq_rel", b"__aarch64_swp4_acq_rel"]: + log(f" Modifying {sym} plt reloc to point into injected code") + rela.r_type = 1027 # R_AARCH64_RELATIVE + rela.r_addend = inject_addr + if sym == b"__aarch64_swp4_acq_rel": + rela.r_addend += 6*4 + +# Move the dynstr section to the hole and add the missing GLIBC_ABI_DT_RELR +log(" Moving .dynstr to free space and adding GLIBC_ABI_DT_RELR...") +free_addr = (free_addr + 3) & ~3 +dynstr = shdr_by_name[b".dynstr"] +dynstr_data = elf[dynstr.sh_offset:dynstr.sh_offset + dynstr.sh_size] +abi_dt_relr_off = len(dynstr_data) +dynstr_data += b"GLIBC_ABI_DT_RELR\0" +dynstr.sh_offset = free_addr +dynstr.sh_addr = free_addr +dynstr.sh_size = len(dynstr_data) +elf[free_addr:free_addr + dynstr.sh_size] = dynstr_data +free_addr += dynstr.sh_size + +log(" Moving .gnu.version_r to free space and adding GLIBC_ABI_DT_RELR...") +ver_r = shdr_by_name[b".gnu.version_r"] +ver_r_data = elf[ver_r.sh_offset:ver_r.sh_offset + ver_r.sh_size] +# We need one more vernaux entry +ver_r_data += bytes(16) + +p = 0 +offset = 0 +while True: + need = Elf64_Verneed.from_buffer(memoryview(ver_r_data)[p: p + 16]) + filename = resolve_string(elf, dynstr, need.vn_file) + need.vn_aux += offset + if filename == b'libc.so.6': + q = p + need.vn_aux + for i in range(need.vn_cnt): + aux = Elf64_Vernaux.from_buffer(memoryview(ver_r_data)[q: q + 16]) + ver = resolve_string(elf, dynstr, aux.vna_name) + q += aux.vna_next + need.vn_cnt += 1 + aux.vna_next = 16 + q += 16 + # Make space here + ver_r_data[q + 16:] = ver_r_data[q:-16] + aux = Elf64_Vernaux.from_buffer(memoryview(ver_r_data)[q: q + 16]) + aux.vna_hash = 0xfd0e42 + aux.vna_name = abi_dt_relr_off + aux.vna_other = 3 + + # Shift the rest of the aux offsets + offset = 16 + + if need.vn_next == 0: + break + p += need.vn_next + +free_addr = (free_addr + 3) & ~3 +ver_r.sh_offset = free_addr +ver_r.sh_addr = free_addr +ver_r.sh_size = len(ver_r_data) +elf[free_addr:free_addr + ver_r.sh_size] = ver_r_data +free_addr += ver_r.sh_size + +# Now fix the DYNAMIC section +log(" Fixing up DYNAMIC section...") +for p in range(phdr_dynamic.p_offset, phdr_dynamic.p_offset + phdr_dynamic.p_filesz, 16): + dyn = Elf64_Dyn.from_buffer(memoryview(elf)[p: p + 16]) + if dyn.d_tag == D_TAG.DT_VERNEED: + dyn.d_val = ver_r.sh_offset + if dyn.d_tag == D_TAG.DT_STRTAB: + dyn.d_val = dynstr.sh_offset + +if not weakened_security: + print() + print("Good news! This CDM version supports your page size, so we didn't have") + print("to weaken memory permissions. Rejoice!") +else: + print() + print("It looks like you're running Asahi, or some other device with >4k page size.") + print("This CDM only supports smaller page sizes, so we had to weaken memory") + print("permissions to make it work.") + +with open(args[1], "wb") as outfile: + outfile.write(memoryview(elf)[:elf_length]) + diff --git a/webpage/docs/customization/browsers.md b/webpage/docs/customization/browsers.md index fefea5d7..8c69925d 100644 --- a/webpage/docs/customization/browsers.md +++ b/webpage/docs/customization/browsers.md @@ -234,3 +234,50 @@ The ID of the extension can be found in the URL of the extension in the Chrome W ] } ``` + +## DRM for ARM64 {#arm64-drm} + +To stream protected contents, Google’s content protection system, Widevine, is required. However, its support for AArch64 systems is limited and it does not come natively during browser installation. To workaround, a copy of Widevine for ARM64 is obtained and installed from ChromeOS. However, further configuration is needed to function properly. + +### Firefox {#firefox-arm64-drm} + +Add the following command in your `docker-compose.yml`: + +```yaml title="docker-compose.yaml" +services: + neko: + ... + command: sh -c "MOZ_GMP_PATH=/var/lib/widevine/gmp-widevinecdm/system-installed exec /usr/bin/supervisord -c /etc/neko/supervisord.conf" +``` + +In your [policies.json](#policy-files), add the following: + +```json title="policies.json" +{ + "policies": { + ... + "Preferences": { + ... + "media.gmp-widevinecdm.enabled": true, + "media.gmp-widevinecdm.visible": true, + "media.gmp-widevinecdm.version": "system-installed", + "media.gmp-widevinecdm.abi": "aarch64-gcc3", + "media.gmp-widevinecdm.autoupdate": false, + "media.eme.enabled": true, + "media.eme.encrypted-media-encryption-scheme.enabled": true + }, + ... + } +} +``` + +For some streaming sites, you also need to use [a user agent switcher extension](https://addons.mozilla.org/firefox/addon/user-agent-string-switcher/) and set it to: `Mozilla/5.0 (X11; CrOS aarch64 15662.0.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6098.0 Safari/537.36` + +### Chromium-based Browsers {#chromium-arm64-drm} + +:::note +- For Brave, you have to go to `brave://settings/extensions` and enable Widevine. +- For Vivaldi, exit the browser and let it restart. +::: + +For some streaming sites, you also need to use [a user agent switcher extension](https://chromewebstore.google.com/detail/bhchdcejhohfmigjafbampogmaanbfkg) and set it to: `Mozilla/5.0 (X11; CrOS aarch64 15662.0.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6098.0 Safari/537.36` diff --git a/webpage/docs/developer-guide/repository-structure.md b/webpage/docs/developer-guide/repository-structure.md index b8b6db78..366c404c 100644 --- a/webpage/docs/developer-guide/repository-structure.md +++ b/webpage/docs/developer-guide/repository-structure.md @@ -24,6 +24,7 @@ This project uses a monorepo structure with the following directories: - `runtime/fonts/`: Empty directory placeholder to include custom fonts in the neko runtime. - `runtime/icon-theme/`: Empty directory placeholder to include a custom icon theme in the neko runtime. - `runtime/intel/` or `runtime/nvidia/`: Directories with files for flavor-specific releases of the neko runtime. + - `runtime/widevine-installer/`: Directories with installer scripts from [AsahiLinux/widevine-installer](https://github.com/AsahiLinux/widevine-installer) - `apps/`: Contains the applications that run in the neko, such as Firefox, Chrome, etc. - `apps//Dockerfile`: Dockerfile extending `BASE_IMAGE` for the application used by the build script. diff --git a/webpage/docs/installation/docker-images.md b/webpage/docs/installation/docker-images.md index 97b70084..3558b99f 100644 --- a/webpage/docs/installation/docker-images.md +++ b/webpage/docs/installation/docker-images.md @@ -282,7 +282,7 @@ The availability of applications for ARM architecture is limited due to the lack | [Remmina](#remmina) | ✅ | ✅ | - | | [VLC](#vlc) | ✅ | ✅ | - | -\* No DRM support. +\* requires extra steps to enable DRM, see instructions [here](/docs/v3/customization/browsers#arm64-drm). :::tip [Oracle Cloud ARM free tier](https://www.oracle.com/cloud/free/) is a great way to test Neko on ARM architecture for free. You can use the `ghcr.io/m1k1o/neko/xfce` image to run a full desktop environment with Xfce and test the applications.