Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 28 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1063,6 +1063,31 @@ jobs:
name: luma-${{ env.LUMA_VERSION }}-ubuntu-${{ matrix.release }}-x86_64.deb
path: luma-${{ env.LUMA_VERSION }}-ubuntu-${{ matrix.release }}-x86_64.deb

appimage:
runs-on: ubuntu-latest
container: ghcr.io/pkgforge-dev/archlinux:latest
needs: [version, ubuntu]
env:
LUMA_VERSION: ${{ needs.version.outputs.version }}
steps:
- uses: actions/checkout@v6

- uses: actions/download-artifact@v8
with:
name: luma-${{ env.LUMA_VERSION }}-ubuntu-26.04-x86_64.deb

- name: Preparing Container
uses: pkgforge-dev/anylinux-setup-action@v2

- name: Make AppImage
run: /bin/sh ./LumaGtk/build-aux/appimage/make-appimage.sh

- name: Upload artifact
uses: actions/upload-artifact@v7
with:
name: luma-${{ env.LUMA_VERSION }}-x86_64.AppImage
path: Luma-${{ env.LUMA_VERSION }}-anylinux-x86_64.AppImage*

flatpak:
runs-on: ubuntu-latest
needs: version
Expand Down Expand Up @@ -1490,8 +1515,7 @@ jobs:
if-no-files-found: error

release:
if: github.ref_type == 'tag'
needs: [macos, ios, fedora, ubuntu, flatpak, snap, windows]
needs: [macos, ios, fedora, ubuntu, appimage, flatpak, snap, windows]
runs-on: ubuntu-latest
permissions:
contents: write
Expand All @@ -1514,6 +1538,8 @@ jobs:
artifacts/luma-*.rpm
artifacts/luma-*.snap
artifacts/luma-*.flatpak
artifacts/Luma-*.AppImage
artifacts/Luma-*.AppImage.zsync
fail_on_unmatched_files: true

- name: Check out main for package.nix bump
Expand Down
245 changes: 245 additions & 0 deletions LumaGtk/build-aux/appimage/make-appimage.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
#!/bin/sh

set -eu

ARCH=$(uname -m)
VERSION=${LUMA_VERSION:-1.0.0}

echo "Installing package dependencies..."
echo "---------------------------------------------------------------"
pacman -Syu --noconfirm libgee libadwaita webkitgtk-6.0 libepoxy libzip libnice patchelf

echo "Installing debloated packages..."
echo "---------------------------------------------------------------"
get-debloated-pkgs --add-common --prefer-nano

mkdir -p ./AppDir/
bsdtar -xOf ./luma-$VERSION-ubuntu-26.04-x86_64.deb data.tar.zst | bsdtar -xf - --strip-components=2 -C ./AppDir/

# Compile memory patch library to resolve Sharun's AT_BASE=0 bug for Frida-gum
cat << 'EOF' > ./AppDir/.patch.cpp
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <elf.h>
#include <link.h>
#include <optional>
#include <span>
#include <string_view>

extern "C" char **environ;

namespace {

using Auxv = ElfW(auxv_t);
using Phdr = ElfW(Phdr);

inline constexpr std::size_t kAuxvMax = 128;

class File {
public:
File(const char *path, const char *mode) noexcept
: f_(std::fopen(path, mode)) {}
~File() {
if (f_)
std::fclose(f_);
}
File(const File &) = delete;
File &operator=(const File &) = delete;
explicit operator bool() const noexcept { return f_ != nullptr; }
[[nodiscard]] FILE *get() const noexcept { return f_; }

private:
FILE *f_;
};

struct Objects {
const Phdr *exe_phdr = nullptr;
ElfW(Half) exe_phnum = 0;
std::optional<std::uintptr_t> interp_base;
};

struct StackRegion {
std::uintptr_t start = 0, end = 0;
};

struct MapsInfo {
StackRegion stack;
std::optional<std::uintptr_t> interp_base;
};

// Locate auxv on the kernel's original stack by scanning backward from stack.end.
// Frida's gummoduleregistry reads the original stack via [stack] in /proc/self/maps.
[[nodiscard]] std::span<Auxv> find_orig_auxv(StackRegion s) noexcept {
if (s.end - s.start < sizeof(Auxv))
return {};

Auxv needle{};
needle.a_type = AT_PHENT;
needle.a_un.a_val = sizeof(Phdr);

std::uintptr_t addr = (s.end - sizeof(needle)) & ~std::uintptr_t{7};
Auxv *last_match = nullptr;

for (; addr >= s.start; addr -= sizeof(void *)) {
if (std::memcmp(reinterpret_cast<void *>(addr), &needle, sizeof(needle)) == 0) {
last_match = reinterpret_cast<Auxv *>(addr);
break;
}
}

if (!last_match)
return {};

constexpr std::size_t kPageSize = 4096;
Auxv *auxv_start = nullptr;
for (Auxv *cur = last_match - 1; reinterpret_cast<std::uintptr_t>(cur) >= s.start; --cur) {
if (cur->a_type >= kPageSize) {
auxv_start = cur + 1;
break;
}
}

if (!auxv_start)
return {};

Auxv *auxv_end = nullptr;
for (Auxv *cur = last_match + 1; reinterpret_cast<std::uintptr_t>(cur + 1) <= s.end; ++cur) {
if (cur->a_type == AT_NULL) {
auxv_end = cur + 1;
break;
}
}

if (!auxv_end)
return {};

return {auxv_start, static_cast<std::size_t>(auxv_end - auxv_start)};
}

// Locate auxv on the live stack pivoted by Sharun via the environ pointer.
[[nodiscard]] Auxv *find_live_auxv() noexcept {
char **p = environ;
if (!p)
return nullptr;
while (*p)
++p;
return reinterpret_cast<Auxv *>(p + 1);
}

// Patch AT_PHDR, AT_PHNUM, AT_PHENT, and AT_BASE in the auxiliary vector.
void patch_auxv(std::span<Auxv> av, const Objects &o) noexcept {
for (auto &e : av) {
if (e.a_type == AT_NULL)
break;
switch (e.a_type) {
case AT_PHDR:
e.a_un.a_val = reinterpret_cast<std::uintptr_t>(o.exe_phdr);
break;
case AT_PHNUM:
e.a_un.a_val = o.exe_phnum;
break;
case AT_PHENT:
e.a_un.a_val = sizeof(Phdr);
break;
case AT_BASE:
if (o.interp_base)
e.a_un.a_val = *o.interp_base;
break;
default:
break;
}
}
}

[[gnu::constructor]] void patch() noexcept {
const MapsInfo maps = []() noexcept {
MapsInfo info;
File f("/proc/self/maps", "r");
if (!f)
return info;

char *line = nullptr;
std::size_t cap = 0;
while (::getline(&line, &cap, f.get()) != -1) {
unsigned long start, end, offset;
int path_off = -1;
if (std::sscanf(line, "%lx-%lx %*s %lx %*s %*s %n", &start, &end, &offset,
&path_off) != 3 ||
path_off < 0)
continue;

const std::string_view path(line + path_off);
if (!info.interp_base && offset == 0 && path.contains("ld-linux"))
info.interp_base = static_cast<std::uintptr_t>(start);
if (path.contains("[stack]"))
info.stack = {static_cast<std::uintptr_t>(start),
static_cast<std::uintptr_t>(end)};
}
std::free(line);
return info;
}();

const Objects o = [&]() noexcept {
Objects tmp;
dl_iterate_phdr(
[](dl_phdr_info *info, std::size_t, void *data) noexcept -> int {
auto *out = static_cast<Objects *>(data);
const std::string_view name = info->dlpi_name ? info->dlpi_name : "";
// The first entry in glibc''s link map is ALWAYS the main program
if (!out->exe_phdr) {
out->exe_phdr = info->dlpi_phdr;
out->exe_phnum = info->dlpi_phnum;
}
if (name.contains("ld-linux")) {
out->interp_base = static_cast<std::uintptr_t>(info->dlpi_addr);
}
return 0;
},
&tmp);
if (!tmp.interp_base)
tmp.interp_base = maps.interp_base;
return tmp;
}();

if (!o.exe_phdr)
return;

// Patch both original kernel stack (for Frida) and live stack (for libc/environ)
Auxv *live = find_live_auxv();
if (maps.stack.start && maps.stack.end) {
std::span<Auxv> orig = find_orig_auxv(maps.stack);
if (!orig.empty())
patch_auxv(orig, o);
if (live && live != orig.data())
patch_auxv({live, kAuxvMax}, o);
} else if (live) {
patch_auxv({live, kAuxvMax}, o);
}
}

} // namespace
EOF
g++ -std=c++23 -O2 -fPIC -shared -fno-exceptions -fno-rtti -Wall -Wextra -static-libstdc++ -static-libgcc ./AppDir/.patch.cpp -o ./AppDir/lib/luma/libpatch.so
patchelf --add-needed libpatch.so ./AppDir/lib/luma/libfrida-core-1.0.so

mv -f ./AppDir/lib/luma/* ./AppDir/bin/
rm -rf ./AppDir/lib

export ARCH VERSION
export OUTPATH=$(pwd)
export ADD_HOOKS="self-updater.hook"
export UPINFO="gh-releases-zsync|${GITHUB_REPOSITORY%/*}|${GITHUB_REPOSITORY#*/}|latest|*$ARCH.AppImage.zsync"
export ICON=./AppDir/share/icons/hicolor/512x512/apps/re.frida.Luma.png
export DESKTOP=./AppDir/share/applications/re.frida.Luma.desktop
export STARTUPWMCLASS=re.frida.Luma
export GTK_CLASS_FIX=1
export LD_LIBRARY_PATH=./AppDir/bin${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}

quick-sharun ./AppDir/bin/*
quick-sharun --make-appimage