diff --git a/flake.nix b/flake.nix index ddf1e72aa..1da6f1e53 100644 --- a/flake.nix +++ b/flake.nix @@ -41,6 +41,12 @@ ); pkgUtil = forAllSystems (system: import ./nix/bundle/pkg.nix { pkgs = pkgsBySystem.${system}; }); + portableDrvLldb = + system: + import ./nix/portable.nix { + pkgs = pkgsBySystem.${system}; + pwndbg = self.packages.${system}.pwndbg-lldb; + }; portableDrv = system: import ./nix/portable.nix { @@ -60,6 +66,7 @@ ); tarballDrv = system: { tarball = pkgUtil.${system}.buildPackageTarball { drv = portableDrv system; }; + tarball-lldb = pkgUtil.${system}.buildPackageTarball { drv = portableDrvLldb system; }; }; in { diff --git a/nix/bundle/bundle-linux.sh b/nix/bundle/bundle-linux.sh deleted file mode 100755 index 6d48d313a..000000000 --- a/nix/bundle/bundle-linux.sh +++ /dev/null @@ -1,178 +0,0 @@ -#!/usr/bin/env bash -# Original file copied from https://github.com/3noch/nix-bundle-exe -# But it was modified/patched for pwndbg usecase! - -set -euo pipefail - -out="$1" -binary="$2" - -: "${bin_dir:-}" -: "${lib_dir:-}" -: "${exe_dir:-}" - -# Converts paths like "folder/bin" to "../.." -relative_bin_to_lib=$(echo -n "$bin_dir" | sed 's|[^/]*|..|g') - -clean_path() { - echo -n "$1" | sed 's#//*#/#g' -} - -printNeeded() { - print-needed-elf "$1" | grep '/nix/store/' -} - -finalizeBin() { - nuke-refs "$1" -} - -relpathPathPrint() { - relative-path "$1" "$2" -} - -bundleLib() { - local file="$1" - local install_dir="$out/$2" - mkdir -p $install_dir - - local real_file - real_file=$(realpath "$file") - - local file_name - file_name=$(basename "$file") - - local real_file_name - real_file_name=$(basename "$real_file") - - local copied_file - copied_file="$install_dir/$real_file_name" - - local already_bundled="1" - if [ ! -f "$copied_file" ]; then - already_bundled="0" - cp "$real_file" "$copied_file" - chmod +w "$copied_file" - fi - - if [ "$file_name" != "$real_file_name" ] && [ ! -f "$install_dir/$file_name" ]; then - (cd "$install_dir" && ln -sf "$real_file_name" "$file_name") - chmod +w "$install_dir/$file_name" - fi - - if [ "$already_bundled" = "1" ]; then - return - fi - - echo "Bundling $real_file to $install_dir" - - local linked_libs - linked_libs=$(printNeeded "$real_file" || true) - for linked_lib in $linked_libs; do - bundleLib "$linked_lib" "lib" - done - - if [ -n "$linked_libs" ]; then - relative_any_to_lib=$(relpathPathPrint "$out/$lib_dir" "$copied_file") - rpath=$(clean_path "\$ORIGIN/$relative_any_to_lib/$lib_dir") - patchelf --set-rpath "$rpath" "$copied_file" - fi - - finalizeBin "$copied_file" -} - -bundleExe() { - local exe="$1" - local interpreter="$2" - local exe_name - exe_name=$(basename "$exe") - - local copied_exe="$out/$exe_dir/$exe_name" - cp "$exe" "$copied_exe" - chmod +w "$copied_exe" - local rpath - rpath=$(clean_path "\$ORIGIN/$relative_bin_to_lib/$lib_dir") - patchelf --set-interpreter "$(basename "$interpreter")" --set-rpath "$rpath" "$copied_exe" - finalizeBin "$copied_exe" - - bundleLib "$interpreter" "lib" - - local linked_libs - linked_libs=$(printNeeded "$exe" || true) - for linked_lib in $linked_libs; do - bundleLib "$linked_lib" "lib" - done - - # shellcheck disable=SC2016 - printf '#!/bin/sh -set -eu -dir="$(cd -- "$(dirname "$(dirname "$(realpath "$0")")")" >/dev/null 2>&1 ; pwd -P)" -exec "$dir"/%s "$dir"/%s "$@"' \ - "'$lib_dir/$(basename "$interpreter")'" \ - "'$exe_dir/$exe_name'" \ - > "$out/$bin_dir/$exe_name" - chmod +x "$out/$bin_dir/$exe_name" -} - -remove_prefix() { - local full_path="$1" - local dynamic_prefix="$2" - echo "${full_path#$dynamic_prefix}" -} - -bundleCustom() { - local from_dir="$1" - local to_dir="$out/$2" - local files - - files=$(find -L "$from_dir" -type f -regex '.*\(\.py\|\.pth\|\.asm\|__doc__\)$' || true) - local real_file_dir - local real_file_name - local install_dir - local install_path - - for real_file in $files; do - real_file_dir=$(dirname "$(remove_prefix "$real_file" "$from_dir")") - real_file_name=$(basename "$real_file") - install_dir="$to_dir/$real_file_dir" - install_path="$install_dir/$real_file_name" - - mkdir -p $install_dir - echo "Copy $real_file to $install_dir" - - # TODO: check symlink like in bundleLib - if [ ! -f "$install_path" ]; then - cp "$real_file" "$install_path" - chmod +w "$install_path" - fi - done -} - -bundleDirLib() { - local from_dir="$1" - local linked_libs_root - local path_dir - local linked_libs - - bundleCustom "$from_dir" "lib" - - linked_libs_root=$(find -L "$from_dir" -type f -regex '.*\.so\(\..*\|$\)' || true) - for linked_lib_root in $linked_libs_root; do - path_dir=$(dirname "$(remove_prefix "$linked_lib_root" "$from_dir")") - - bundleLib "$linked_lib_root" "lib/$path_dir" - - linked_libs=$(printNeeded "$linked_lib_root" || true) - for linked_lib in $linked_libs; do - bundleLib "$linked_lib" "lib" - done - done -} - -exe_interpreter=$(patchelf --print-interpreter "$binary" 2> /dev/null || true) -if [ -n "$exe_interpreter" ]; then - mkdir -p "$out/$exe_dir" "$out/$bin_dir" "$out/$lib_dir" - bundleExe "$binary" "$exe_interpreter" -else - mkdir -p "$out/$exe_dir" "$out/$bin_dir" "$out/$lib_dir" - bundleDirLib "$binary" -fi diff --git a/nix/bundle/bundle-macos.sh b/nix/bundle/bundle-macos.sh deleted file mode 100755 index 9fd1451c7..000000000 --- a/nix/bundle/bundle-macos.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env bash -# Original file copied from https://github.com/3noch/nix-bundle-exe -# But it was modified/patched for pwndbg usecase! - -set -euo pipefail - -echo "darwin is not supported TODO, https://github.com/3noch/nix-bundle-exe/blob/main/bundle-macos.sh" -exit 1 -# -#out="$1" -#binary="$2" -# -#: "${bin_dir:-}" -#: "${lib_dir:-}" -# -## Converts paths like "folder/bin" to "../.." -#relative_bin_to_lib=$(echo -n "$bin_dir" | sed 's|[^/]*|..|g') -# -#mkdir -p "$out/$bin_dir" "$out/$lib_dir" -# -#clean_path() { -# echo -n "$1" | sed 's#//*#/#g' -#} -# -#printNeeded() { -# otool -L "$1" | tail -n +2 | grep '/nix/store/' | cut -d '(' -f -1 -#} -# -#finalizeBin() { -# nuke-refs "$1" -# codesign -f -s - "$1" || true -#} -# -#bundleBin() { -# local file="$1" -# local file_type="$2" -# -# local real_file -# real_file=$(realpath "$file") -# local install_dir="$out/$lib_dir" -# local rpath_prefix="@loader_path" -# if [ "$file_type" == "exe" ]; then -# install_dir="$out/$bin_dir" -# rpath_prefix=$(clean_path "@executable_path/$relative_bin_to_lib/$lib_dir") -# fi -# -# local copied_file -# copied_file="$install_dir/$(basename "$real_file")" -# if [ -f "$copied_file" ]; then -# return -# fi -# -# echo "Bundling $real_file to $install_dir" -# cp "$real_file" "$copied_file" -# chmod +w "$copied_file" -# -# local linked_libs -# linked_libs=$(printNeeded "$real_file" || true) -# for linked_lib in $linked_libs; do -# local real_lib -# real_lib=$(realpath "$linked_lib") -# local real_lib_name -# real_lib_name=$(basename "$real_lib") -# install_name_tool -change "$linked_lib" "$rpath_prefix/$real_lib_name" "$copied_file" -# bundleBin "$real_lib" "lib" -# done -# -# finalizeBin "$copied_file" -#} -# -#bundleBin "$binary" "exe" diff --git a/nix/bundle/bundle.py b/nix/bundle/bundle.py new file mode 100755 index 000000000..ac401b399 --- /dev/null +++ b/nix/bundle/bundle.py @@ -0,0 +1,419 @@ +import stat +import subprocess +import shutil +import os +import os.path +import typing +import sys +from pathlib import Path + + +def check_file_type(file_path: Path) -> str | None: + with open(str(file_path), 'rb') as f: + header = f.read(4) + + if header == b'\x7fELF': + return "ELF" + elif header == b'\xfe\xed\xfa\xce': + return "Mach-O 32-bit (Little Endian)" + elif header == b'\xfe\xed\xfa\xcf': + return "Mach-O 64-bit (Little Endian)" + elif header == b'\xce\xfa\xed\xfe': + return "Mach-O 32-bit (Big Endian)" + elif header == b'\xcf\xfa\xed\xfe': + return "Mach-O 64-bit (Big Endian)" + elif header == b'\xca\xfe\xba\xbe': + return "Mach-O Fat Binary (Universal, Little Endian)" + elif header == b'\xbe\xba\xfe\xca': + return "Mach-O Fat Binary (Universal, Big Endian)" + else: + return None + + +def eprint(msg: str): + print(msg, file=sys.stderr) + + +def run(args: typing.List[str], no_error=False) -> str: + result = subprocess.run(args, capture_output=True) + if result.returncode != 0: + if no_error: + eprint(result.stderr) + eprint("WARNING: Command failed with return code {}: {}".format(result.returncode, args)) + return '' + + eprint(result.stderr) + eprint("Command failed with return code {}: {}".format(result.returncode, args)) + sys.exit(result.returncode) + return result.stdout.decode("utf-8") + + +def iter_macho_deps(binary_path: Path) -> typing.Iterator[Path]: + for line in run(["otool", "-L", str(binary_path)]).splitlines(): + line = line.strip() + if not line.startswith('/nix/store/'): + continue + + splited = line.split(' (', 1) + if len(splited) != 2: + continue + + lib_path = Path(splited[0]) + if not lib_path.exists(): + eprint(f'WARNING: skipping not exists file={lib_path}') + continue + + yield lib_path + + +def iter_elf_deps(binary_path: Path) -> typing.Iterator[Path]: + def stripped_strs(strs: typing.Iterable[str]) -> typing.Iterable[str]: + return (cleaned for x in strs for cleaned in [x.strip()] if cleaned != "") + + def get_rpaths(exe: str) -> typing.Iterable[str]: + return stripped_strs(run(["patchelf", "--print-rpath", exe]).split(":")) + + def resolve_origin(origin: str, paths: typing.Iterable[str]) -> typing.Iterable[str]: + return (path.replace("$ORIGIN", origin) for path in paths) + + def get_needed(exe: str) -> typing.Iterable[str]: + return stripped_strs(run(["patchelf", "--print-needed", exe]).splitlines()) + + def resolve_paths(needed: typing.Iterable[str], rpaths: typing.List[str]) -> typing.Iterable[str]: + existing_paths = lambda lib, paths: ( + abs_path for path in paths for abs_path in [os.path.join(path, lib)] + if os.path.exists(abs_path) + ) + for lib in needed: + for found in [next(existing_paths(lib, rpaths), None)]: + if found is None: + eprint(f"WARNING: can't find {lib} in {rpaths}") + continue + + yield found + + dirname = os.path.dirname(str(binary_path)) + rpaths_raw = list(get_rpaths(str(binary_path))) + rpaths_raw = [dirname] if rpaths_raw == [] else rpaths_raw + rpaths = list(resolve_origin(dirname, rpaths_raw)) + for path in (x for x in resolve_paths(get_needed(str(binary_path)), rpaths) if x is not None): + if not path.startswith('/nix/store/'): + continue + yield Path(path) + + +if sys.platform == 'darwin': + iter_deps = iter_macho_deps +else: + iter_deps = iter_elf_deps + + +def iter_deps_recursive(binary_path: Path, depth: int=None, visited: typing.Set[Path]=None) -> typing.Iterator[Path]: + is_first = depth is None + if depth is None: + depth = 0 + if visited is None: + visited = set() + + if depth > 20: + raise ValueError(f'depth exceeded {depth}') + + binary_path = Path(os.path.normpath(binary_path)) + if binary_path in visited: + return + + visited.add(binary_path) + if not is_first: + yield binary_path + + for dep in iter_deps(binary_path): + yield from iter_deps_recursive(dep, depth=depth + 1, visited=visited) + + +def iter_dir_recursive(dir_path: Path, depth: int = None, visited: typing.Set[Path] = None) -> typing.Iterator[ + typing.Tuple[Path, typing.List[Path]]]: + if depth is None: + depth = 0 + if visited is None: + visited = set() + + if depth > 20: + raise ValueError(f'depth exceeded {depth}') + + if dir_path in visited: + return + + visited.add(dir_path) + + stored_dirs = [] + stored_files = [] + + for entry in dir_path.iterdir(): + if entry.is_dir(): + stored_dirs.append(entry) + elif entry.is_file(): + stored_files.append(entry) + else: + eprint(f"WARNING: Unrecognized entry {entry}") + continue + + yield dir_path, stored_files + del stored_files + + for subdir in stored_dirs: + yield from iter_dir_recursive(subdir, depth=depth + 1, visited=visited) + + +def cleanup_nixrefs(binary_path: Path): + # Modify the binary to replace references to actual Nix store paths (e.g., /nix/store/valid-hash) + # with invalid or placeholder paths (e.g., /nix/store/invalid-hash), ensuring the binary + # doesn’t inadvertently depend on specific Nix store contents. + run(['nuke-refs', str(binary_path)]) + + if sys.platform == 'darwin': + # Force an "ad-hoc" code signature on the binary (using '-' as the identity placeholder). + # This is typically used to satisfy macOS code signing requirements without a valid signing certificate. + # The `-f` option forces re-signing if the binary is already signed. + run(['codesign', '-f', '-s', '-', str(binary_path)], no_error=True) + + +def patch_library_macho(binary_path: Path, root_dst: Path, *, is_exe: bool): + lib_dir = root_dst / 'lib' + if is_exe: + # For executable files (e.g., /abs/exe/gdb), replace absolute library paths with paths relative to the executable. + # Example: replace /abs/lib/libLLVM.dylib with @executable_path/../lib/libLLVM.dylib + # This makes the executable locate libraries in its own relative directory structure at runtime. + prefix_lib = '@executable_path/' + else: + # For shared libraries (e.g., /abs/lib/python3.12/capstone/foo.dylib), replace absolute library paths with paths relative to the library. + # Example: replace /abs/lib/libiconv.2.dylib with @loader_path/../../libiconv.2.dylib + # This allows libraries to locate dependencies in a relative directory structure without absolute paths. + prefix_lib = '@loader_path/' + + # When `binary_path` is already patched. `iter_deps` should return empty list + for src_lib_path in iter_deps(binary_path): + dst_lib_path = lib_dir / src_lib_path.name + + rel_path = os.path.relpath(dst_lib_path, binary_path.parent) + print(f'Patching {binary_path.name}: {src_lib_path.name}->{rel_path}') + run(["install_name_tool", "-change", str(src_lib_path), prefix_lib + rel_path, str(binary_path)]) + + cleanup_nixrefs(binary_path) + +def patch_library_elf(binary_path: Path, root_dst: Path, *, is_exe: bool): + # Ensure that $ORIGIN resolves relative to the actual binary's resolved location, + # not the symlink's location. + # + # Using symlinks can cause issues, for example: + # lib/python3.12/site-packages/lldb/_lldb.cpython-312-aarch64-linux-gnu.so -> ../../../liblldb.so.19.1.1 + # + # On Linux, $ORIGIN is resolved based on the location of the symlink itself, + # not the resolved target location of the binary. This behavior can lead to + # runtime errors if the symlink points to a path outside the expected structure. + # + # On macOS, the equivalent mechanism `@loader_path` correctly (sic!) resolves relative + # to the binary's actual location, even when symlinks are involved. + # + # To maintain compatibility and avoid such issues, symlinks should be avoided + # in scenarios where $ORIGIN is used. + + prefix_lib = '$ORIGIN/' + rel_path = Path(os.path.relpath(root_dst, binary_path.parent)) / 'lib' + rpath = prefix_lib + str(rel_path) + + print(f'Patching {binary_path.name}') + + # When `binary_path` is already patched. `iter_deps` should return empty list + # We need to be sure to not patch ld-loader or libc + is_rpath_patch_needed = bool(next(iter_deps(binary_path), None)) + + if is_rpath_patch_needed: + if is_exe: + interpreter_path = Path(run(["patchelf", "--print-interpreter", str(binary_path)]).strip()) + run(["patchelf", "--set-interpreter", interpreter_path.name, "--set-rpath", rpath, str(binary_path)]) + else: + run(["patchelf", "--set-rpath", rpath, str(binary_path)]) + + cleanup_nixrefs(binary_path) + + +if sys.platform == 'darwin': + patch_library = patch_library_macho +else: + patch_library = patch_library_elf + + +def copy_with_chmod(src: Path, dst: Path): + if os.path.isdir(dst): + raise ValueError('only coping file supported ;)') + + if not dst.parent.exists(): + dst.parent.mkdir(parents=True, exist_ok=True) + + shutil.copy(src, dst) + # add writable + dst.lchmod(dst.stat().st_mode | stat.S_IWUSR) + + +def symlink(target: Path | str, dst: Path): + if os.path.isdir(dst): + raise ValueError('only coping file supported ;)') + + if not dst.parent.exists(): + dst.parent.mkdir(parents=True, exist_ok=True) + + dst.symlink_to(str(target)) + + +def copy_with_symlink_normal(src_file_path: Path, root_dir_src: Path, root_dst_dir: Path, is_so: bool=False) -> Path | None: + dst_file_path = root_dst_dir / src_file_path.relative_to(root_dir_src) + if dst_file_path.exists(): + return dst_file_path + + if src_file_path.is_symlink(): + file_resolved = src_file_path.resolve() + is_allowed_symlink = file_resolved.is_relative_to(root_dir_src) + + if is_so and is_allowed_symlink: + # For .so / .dylib files, symlinks are only allowed within the same directory. + # This is because $ORIGIN in the runpath cannot resolve symlinks. + # This issue was specifically encountered with the file: + # lib/python3.12/site-packages/lldb/_lldb.cpython-312-aarch64-linux-gnu.so -> ../../../liblldb.so.19.1.1 + # To avoid such issues, we check if the resolved file's parent directory + # matches the parent directory of the source file. + if file_resolved.relative_to(root_dir_src).parent != src_file_path.parent: + is_allowed_symlink = False + + if is_allowed_symlink: + # symlinked-file.txt should points to relative ../../original-file.txt + # Allowed to create symlink, because they are under same root + + rel_path = os.path.relpath(file_resolved, src_file_path.parent) + print(f'CopyingSym {dst_file_path}->{rel_path}') + symlink(target=rel_path, dst=dst_file_path) + + new_real_dst = root_dst_dir / file_resolved.relative_to(root_dir_src) + if new_real_dst.exists(): + return new_real_dst + + print(f'Copying {src_file_path.name} to {new_real_dst.parent}') + copy_with_chmod(src_file_path, new_real_dst) + return new_real_dst + else: + # hard copy file without symlink, because they are in different root + pass + + print(f'Copying {src_file_path.name} to {dst_file_path.parent}') + copy_with_chmod(src_file_path, dst_file_path) + return dst_file_path + + +def copy_with_symlink_lib(src_path: Path, dst_dir: Path) -> Path | None: + new_file = dst_dir / src_path.name + if new_file.exists(): + return new_file + + if src_path.is_symlink(): + src_resolved_lib_path = src_path.resolve() + is_weird_symlink = src_resolved_lib_path.name == src_path.name + if is_weird_symlink: + eprint(f'WARNING: Shouldn\'t happen? {src_path}->{src_resolved_lib_path}, coping file') + + print(f'Bundling {src_path.name} to {new_file.parent}') + copy_with_chmod(src_path, new_file) + return new_file + + symlink_path = dst_dir / src_path.name + print(f'BundlingSym {symlink_path.name}->{src_resolved_lib_path.name} to {symlink_path.parent}') + symlink(target=src_resolved_lib_path.name, dst=symlink_path) + + new_file = dst_dir / src_resolved_lib_path.name + if new_file.exists(): + return new_file + + print(f'Bundling {src_resolved_lib_path.name} to {new_file.parent}') + copy_with_chmod(src_resolved_lib_path, new_file) + return new_file + else: + print(f'Bundling {src_path.name} to {new_file.parent}') + copy_with_chmod(src_path, new_file) + return new_file + + +def bundle_library(binary_path: Path, root_dst: Path, *, is_exe: bool, dst_path: Path=None): + lib_dir = root_dst / 'lib' + exe_dir = root_dst / 'exe' + + if not binary_path.is_relative_to(root_dst): + # coping required, because src-binary and dst-binary are in different roots + binary_path = copy_with_symlink_lib(binary_path, exe_dir if is_exe else lib_dir) + + # Move file to another place + if is_exe and dst_path: + shutil.move(binary_path, dst_path) + binary_path = dst_path + + # Store all needed libs into {root}/lib/* + for src_lib_path in iter_deps_recursive(binary_path): + real_file = copy_with_symlink_lib(src_lib_path, lib_dir) + if real_file is None: + continue + patch_library(real_file, root_dst, is_exe=False) + + # fix main + patch_library(binary_path, root_dst, is_exe=is_exe) + + +def bundle_python_venv(src_lib_dir: Path, out_lib_dir: Path, root_dst: Path): + bundle_binaries = set() + for _, files in iter_dir_recursive(src_lib_dir): + for src_file_path in files: + # search for so files: + # - /libpython3.12.so.1.0 + # - /libpython3.12.so + # - /libpython3.12.dylib + is_so = any(suffix in src_file_path.suffixes for suffix in ( + '.so', + '.dylib', + )) + + is_good_ext = src_file_path.suffix in ( + '.py', # python script file + '.pyi', '.typed', # python types + '.asm', # pwntools asm templates + ) + is_good_name = src_file_path.name in ( + '__doc__', # pwntools asm templates + ) + + if not (is_so or is_good_ext or is_good_name): + continue + + real_file = copy_with_symlink_normal(src_file_path, src_lib_dir, out_lib_dir, is_so=is_so) + if is_so and real_file: + bundle_binaries.add(real_file) + + for file in bundle_binaries: + bundle_library(file, root_dst, is_exe=False) + + +def main(): + out = Path(sys.argv[1]) + rest_argv = sys.argv[2:] + + for src_path, dst_part in zip(rest_argv[::2], rest_argv[1::2]): + is_dir = str(dst_part).endswith('/') + src_path = Path(src_path) + dst_part = Path(dst_part) + dst_path = out / dst_part + + if is_dir: + bundle_python_venv(src_path, dst_path, out) + else: + if check_file_type(src_path): + bundle_library(src_path, out, is_exe=True, dst_path=dst_path) + else: + copy_with_chmod(src_path, dst_path) + + +main() diff --git a/nix/bundle/default.nix b/nix/bundle/default.nix index b7a5d89f8..1ee82d7b6 100644 --- a/nix/bundle/default.nix +++ b/nix/bundle/default.nix @@ -1,10 +1,7 @@ { pkgs, - bin_dir ? "bin", - exe_dir ? "exe", - lib_dir ? if pkgs.stdenv.isDarwin then "Frameworks/Library.dylib" else "lib", }: -path: +paths: # Original file copied from https://github.com/3noch/nix-bundle-exe # But it was modified/patched for pwndbg usecase! # May be: @@ -12,48 +9,27 @@ path: # 2) a path to a directory containing bin/, or # 3) a path to an executable. let - print-needed-elf = pkgs.writeScriptBin "print-needed-elf" '''${pkgs.python3}'/bin/python ${./print_needed_elf.py} "$@"''; - - relative-path = pkgs.writeScriptBin "relative-path" '''${pkgs.python3}'/bin/python ${./relative-path.py} "$@"''; - - cfg = + deps = if pkgs.stdenv.isDarwin then - { - deps = with pkgs; [ - darwin.binutils - darwin.sigtool - ]; - script = "bash ${./bundle-macos.sh}"; - } + [ + pkgs.darwin.cctools + pkgs.darwin.binutils + pkgs.darwin.sigtool + ] else if pkgs.stdenv.isLinux then - { - deps = [ - pkgs.glibc - print-needed-elf - relative-path - ]; - script = "bash ${./bundle-linux.sh}"; - } + [ + pkgs.patchelf + ] else throw "Unsupported platform: only darwin and linux are supported"; - - name = if pkgs.lib.isDerivation path then path.name else builtins.baseNameOf path; - overrideEnv = name: value: if value == null then "" else "export ${name}='${value}'"; in -pkgs.runCommand "bundle-${name}" { nativeBuildInputs = cfg.deps ++ [ pkgs.nukeReferences ]; } '' +pkgs.runCommand "pwndbg-bundler" { + nativeBuildInputs = deps ++ [ + pkgs.nukeReferences + pkgs.python3 + ]; +} '' set -euo pipefail - export bin_dir='${bin_dir}' - export exe_dir='${exe_dir}' - export lib_dir='${lib_dir}' - ${ - if builtins.pathExists "${path}/bin" then - '' - find '${path}/bin' -type f -executable -print0 | xargs -0 --max-args 1 ${cfg.script} "$out" - '' - else - '' - ${cfg.script} "$out" ${pkgs.lib.escapeShellArg path} - '' - } + python3 ${./bundle.py} "$out" ${pkgs.lib.escapeShellArgs paths} find $out -empty -type d -delete '' diff --git a/nix/bundle/pkg.nix b/nix/bundle/pkg.nix index 26783794d..a649da411 100644 --- a/nix/bundle/pkg.nix +++ b/nix/bundle/pkg.nix @@ -10,6 +10,9 @@ let "armv7l-linux" = "armv7"; "riscv64-linux" = "riscv64"; + + "aarch64-darwin" = "macos_arm64"; + "x86_64-darwin" = "macos_amd64"; }; buildPackagePFPM = diff --git a/nix/bundle/print_needed_elf.py b/nix/bundle/print_needed_elf.py deleted file mode 100755 index 578a216ec..000000000 --- a/nix/bundle/print_needed_elf.py +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env python -# Original file copied from https://github.com/3noch/nix-bundle-exe -# But it was modified/patched for pwndbg usecase! - -# Prints resolved paths to needed libraries for an ELF executable. -# ldd also does this, but it segfaults in some odd scenarios so we avoid it. -import sys -import os -import subprocess -from typing import Any, Iterable, List - - -def eprint(msg: Any): - print(msg, file=sys.stderr) - - -def run(args: List[str]) -> str: - result = subprocess.run(args, capture_output=True) - if result.returncode != 0: - eprint(result.stderr) - eprint("Command failed with return code {}: {}".format(result.returncode, args)) - sys.exit(result.returncode) - return result.stdout.decode("utf-8") - - -def stripped_strs(strs: Iterable[str]) -> Iterable[str]: - return (cleaned for x in strs for cleaned in [x.strip()] if cleaned != "") - - -def get_rpaths(exe: str) -> Iterable[str]: - return stripped_strs(run(["patchelf", "--print-rpath", exe]).split(":")) - - -def resolve_origin(origin: str, paths: Iterable[str]) -> Iterable[str]: - return (path.replace("$ORIGIN", origin) for path in paths) - - -def get_needed(exe: str) -> Iterable[str]: - return stripped_strs(run(["patchelf", "--print-needed", exe]).splitlines()) - - -def resolve_paths(needed: Iterable[str], rpaths: List[str]) -> Iterable[str]: - existing_paths = lambda lib, paths: ( - abs_path for path in paths for abs_path in [os.path.join(path, lib)] - if os.path.exists(abs_path) - ) - return ( - found if found is not None else eprint("Warning: can't find {} in {}".format(lib, rpaths)) - for lib in needed for found in [next(existing_paths(lib, rpaths), None)] - ) - - -def main(exe: str): - dirname = os.path.dirname(exe) - rpaths_raw = list(get_rpaths(exe)) - rpaths_raw = [dirname] if rpaths_raw == [] else rpaths_raw - rpaths = list(resolve_origin(dirname, rpaths_raw)) - for path in (x for x in resolve_paths(get_needed(exe), rpaths) if x is not None): - print(path) - - -if __name__ == "__main__": - main(*sys.argv[1:]) diff --git a/nix/bundle/relative-path.py b/nix/bundle/relative-path.py deleted file mode 100644 index 31ed8b09a..000000000 --- a/nix/bundle/relative-path.py +++ /dev/null @@ -1,4 +0,0 @@ -import os.path -import sys - -print(os.path.relpath(*sys.argv[1:])) diff --git a/nix/portable.nix b/nix/portable.nix index 2bf63b1a5..b9189b76c 100644 --- a/nix/portable.nix +++ b/nix/portable.nix @@ -3,25 +3,80 @@ pwndbg ? import ./pwndbg.nix { }, }: let + isLLDB = pwndbg.meta.isLLDB; + lldb = pwndbg.meta.lldb; gdb = pwndbg.meta.gdb; python3 = pwndbg.meta.python3; pwndbgVenv = pwndbg.meta.pwndbgVenv; - gdbBundledLib = pkgs.callPackage ./bundle { } "${gdb}/bin/gdb"; - pyEnvBundledLib = pkgs.callPackage ./bundle { } "${pwndbgVenv}/lib/"; + bundler = arg: (pkgs.callPackage ./bundle { } arg); ldName = pkgs.lib.readFile ( - pkgs.runCommand "bundle" { nativeBuildInputs = [ pkgs.patchelf ]; } '' - echo -n $(patchelf --print-interpreter "${gdbBundledLib}/exe/gdb") > $out + pkgs.runCommand "pwndbg-bundle-ld-name-IFD" { nativeBuildInputs = [ pkgs.patchelf ]; } '' + echo -n $(basename $(patchelf --print-interpreter "${gdb}/bin/gdb")) > $out '' ); + ldLoader = if pkgs.stdenv.isDarwin then "" else "\"$dir/lib/${ldName}\""; - pwndbgBundleBin = pkgs.writeScript "pwndbg" '' + linuxLldbEnvs = pkgs.lib.optionalString (pkgs.stdenv.isLinux && isLLDB) '' + export LLDB_DEBUGSERVER_PATH="$dir/bin/lldb-server" + ''; + wrapperBinPwndbgGdbinit = pkgs.writeScript "pwndbg-wrapper-bin-gdbinit" '' + #!/bin/sh + dir="$(cd -- "$(dirname "$(dirname "$(realpath "$0")")")" >/dev/null 2>&1 ; pwd -P)" + export PYTHONHOME="$dir" + export PATH="$dir/bin/:$PATH" + exec ${ldLoader} "$dir/exe/gdb" --quiet --early-init-eval-command="set auto-load safe-path /" --command=$dir/exe/gdbinit.py "$@" + ''; + wrapperBinPy = file: pkgs.writeScript "pwndbg-wrapper-bin-py" '' + #!/bin/sh + dir="$(cd -- "$(dirname "$(dirname "$(realpath "$0")")")" >/dev/null 2>&1 ; pwd -P)" + export PYTHONHOME="$dir" + export PATH="$dir/bin/:$PATH" + ${linuxLldbEnvs} + exec ${ldLoader} "$dir/exe/python3" "$dir/${file}" "$@" + ''; + wrapperBin = file: pkgs.writeScript "pwndbg-wrapper-bin" '' #!/bin/sh dir="$(cd -- "$(dirname "$(dirname "$(realpath "$0")")")" >/dev/null 2>&1 ; pwd -P)" + export PATH="$dir/bin/:$PATH" export PYTHONHOME="$dir" - exec "$dir/lib/${ldName}" "$dir/exe/gdb" --quiet --early-init-eval-command="set auto-load safe-path /" --command=$dir/exe/gdbinit.py "$@" + ${linuxLldbEnvs} + exec ${ldLoader} "$dir/${file}" "$@" ''; + skipVenv = pkgs.writeScript "pwndbg-skip-venv" ""; + + pwndbgGdbBundled = bundler [ + "${pkgs.lib.getBin gdb}/bin/gdb" "exe/gdb" + "${pkgs.lib.getBin gdb}/bin/gdbserver" "exe/gdbserver" + "${gdb}/share/gdb/" "share/gdb/" + "${pwndbgVenv}/lib/" "lib/" + + "${pwndbg.src}/pwndbg/" "lib/${python3.libPrefix}/site-packages/pwndbg/" + "${pwndbg.src}/gdbinit.py" "exe/gdbinit.py" + "${skipVenv}" "exe/.skip-venv" + + "${wrapperBinPwndbgGdbinit}" "bin/pwndbg" + "${wrapperBin "exe/gdbserver"}" "bin/gdbserver" + ]; + + pwndbgLldbBundled = bundler [ + "${pkgs.lib.getBin lldb}/bin/.lldb-wrapped" "exe/lldb" + "${pkgs.lib.getBin lldb}/bin/lldb-server" "exe/lldb-server" + "${pkgs.lib.getLib lldb}/lib/" "lib/" + "${pwndbgVenv}/lib/" "lib/" + "${python3}/bin/python3" "exe/python3" + + "${pwndbg.src}/pwndbg/" "lib/${python3.libPrefix}/site-packages/pwndbg/" + "${pwndbg.src}/lldbinit.py" "exe/lldbinit.py" + "${pwndbg.src}/pwndbg-lldb.py" "exe/pwndbg-lldb.py" + "${skipVenv}" "exe/.skip-venv" + + "${wrapperBin "exe/lldb-server"}" "bin/lldb-server" + "${wrapperBin "exe/lldb"}" "bin/lldb" + "${wrapperBinPy "exe/pwndbg-lldb.py"}" "bin/pwndbg-lldb" + ]; + pwndbgBundled = if isLLDB then pwndbgLldbBundled else pwndbgGdbBundled; portable = pkgs.runCommand "portable-${pwndbg.name}" @@ -33,31 +88,18 @@ let }; } '' - mkdir -p $out/pwndbg/bin/ - mkdir -p $out/pwndbg/lib/ - mkdir -p $out/pwndbg/exe/ - mkdir -p $out/pwndbg/share/gdb/ - touch $out/pwndbg/exe/.skip-venv - - cp -rf ${gdbBundledLib}/exe/* $out/pwndbg/exe/ - cp -rf ${gdbBundledLib}/lib/* $out/pwndbg/lib/ - cp -rf ${pyEnvBundledLib}/lib/* $out/pwndbg/lib/ - - cp -rf ${pwndbgVenv}/share/gdb/* $out/pwndbg/share/gdb/ - cp -rf ${gdb}/share/gdb/* $out/pwndbg/share/gdb/ - chmod -R +w $out + mkdir -p $out/pwndbg/ + # copy + cp -rf ${pwndbgBundled}/* $out/pwndbg/ - cp -rf ${pwndbg.src}/pwndbg $out/pwndbg/lib/${python3.libPrefix}/site-packages/ - cp ${pwndbg.src}/gdbinit.py $out/pwndbg/exe/ - - cp ${pwndbgBundleBin} $out/pwndbg/bin/pwndbg + # writable out + chmod -R +w $out # fix python "subprocess.py" to use "/bin/sh" and not the nix'ed version, otherwise "gdb-pt-dump" is broken substituteInPlace $out/pwndbg/lib/${python3.libPrefix}/subprocess.py --replace "'${pkgs.bash}/bin/sh'" "'/bin/sh'" # build pycache - chmod -R +w $out/pwndbg/lib/${python3.libPrefix}/site-packages/pwndbg - SOURCE_DATE_EPOCH=0 ${pwndbgVenv}/bin/python3 -c "import compileall; compileall.compile_dir('$out', stripdir='$out', force=True);" + SOURCE_DATE_EPOCH=0 ${python3}/bin/python3 -c "import compileall; compileall.compile_dir('$out', stripdir='$out', force=True);" ''; in portable diff --git a/nix/pwndbg.nix b/nix/pwndbg.nix index c272e360e..446b7ecf4 100644 --- a/nix/pwndbg.nix +++ b/nix/pwndbg.nix @@ -124,6 +124,8 @@ let pwndbgVenv = pyEnv; python3 = python3; gdb = gdb; + lldb = lldb; + isLLDB = isLLDB; }; }; in