Revive the build: CI, Apple sign-in, iOS 26, and wireless refresh - #138
Open
Ben-Diehlci wants to merge 96 commits into
Open
Ben-Diehlci wants to merge 96 commits into
Ben-Diehlci wants to merge 96 commits into
Conversation
The scheduled build has failed on every run for a long time, dying at the "Set up job" step -- the runner phase that resolves `uses:`, before any of our own scripts execute. The cause is `gautamkrishnar/keepalive-workflow@master`: its action manifest no longer resolves at any ref. Because `build`, `release` and `update_submodule` all gate on `check` via `needs:`, the whole workflow has been skipping -- no binaries since 2025-03-16, and no upstream_repo sync, which is what NyaMisty#121 is actually reporting. - Remove the dead keepalive step. - Bump five actions off GitHub's retired node12/node16 runtimes: checkout v2->v4, setup-qemu-action v1->v3, login-action (pinned node12 SHA)->v3, download-artifact v2->v4, action-gh-release v1->v2. download-artifact must be v4 specifically: the uploader (NyaMisty/upload-artifact-as-is) depends on @actions/artifact ^2.2.1, the v4 backend, so a v2 downloader cannot see the artifacts it produces. - Replace `::set-output` (removed by GitHub in 2023) with $GITHUB_OUTPUT. - Add per-job `permissions`. `release` and `update_submodule` need contents:write and would 403 under today's read-only default token. - workflow_dispatch: `github.event.inputs.sync_upstream` is always a string, so "false" evaluated truthy and manual sync fired unconditionally. Declare `type: boolean` and read it from the typed `inputs` context instead. - build_docker.yml: drop `ref: master`; this fork has no master branch. Verified without pushing: both files parse; all 8 action refs resolve on supported runtimes; the four ghcr.io builder images are still publicly pullable; and the rewritten check-step shell was executed against stubbed git across all three paths (gate closed -> updated=0, unchanged -> updated=0, upstream moved -> updated=1), matching the old ::set-output behaviour. Not fixed here: `chmod +x` in the build step is a no-op, since artifact upload does not preserve file permissions. Left in place. It is the likely root of NyaMisty#126, which needs a packaging or documentation fix rather than a CI one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
GitHub now forces node20 actions onto the node24 runtime and annotates every run with "Node.js 20 is deprecated". These are pure version-string bumps; no inputs, defaults or behaviour change. actions/checkout v4 -> v7 (4 sites) docker/setup-qemu-action v3 -> v4 (2 sites) docker/login-action v3 -> v4 (2 sites) softprops/action-gh-release v2 -> v3 (1 site) Each target was verified by fetching its action.yml and confirming `using: node24`, rather than assuming the newest tag is node24 -- notably actions/download-artifact v5 and v6 are still node20, so "newest major" and "node24" are not the same question. Three v5-v7 checkout changes were checked and are inert here: v6 moved the auth token out of .git/config into a $RUNNER_TEMP credential file, which is invisible to us because ad-m/github-push-action builds its own authenticated remote from its github_token input and all six submodules are public https URLs; the v6 container-action runner requirement applies to `uses:`-style container actions, while our build shells out to `docker run` from an ordinary `run:` step; and the v7 fork-PR guard only fires on pull_request_target / workflow_run with an explicit repository/ref input, none of which we use. The floating `v7` tag is used deliberately rather than v7.0.0, which shipped a fork-PR guard that could fire on default checkouts and was fixed in v7.0.1. Also bumps the commented-out docker/setup-buildx-action reference in build_docker.yml so it is not stale if anyone uncomments it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Completes the node24 migration. NyaMisty/upload-artifact-as-is@master declares
`using: node20` and is third-party, so it cannot be fixed in place -- it runs in
every build leg and would keep the deprecation annotation alive no matter what
else was bumped. Replacing it is the only way to clear the warning.
The upload and download halves MUST land together. The uploader stamps its
blobs with Content-Type "zip", which download-artifact@v8 does not recognise as
a zip: v8 decides whether to decompress from the Content-Type or a .zip URL
suffix, and otherwise writes the body to disk verbatim. Bumping only the
downloader would therefore have dropped raw .zip files into build_release/ and
shipped them as release assets. Bumping only the uploader would leave a 6.x
uploader paired with a v4 downloader. Neither half is independently correct.
NyaMisty/upload-artifact-as-is@master -> actions/upload-artifact@v7
actions/download-artifact v4 -> v8
Both sides now run @actions/artifact 6.2.x. v8 rather than v7 because
download-artifact@v7 still ships @actions/artifact ^5.0.0.
The matrix is restructured to an `include:` list because upload-artifact
validates artifact names and rejects / \ : < > | * ? " -- so ${{ matrix.builder }}
("ghcr.io/nyamisty/...") cannot be used as a name. Still exactly 4 legs, and
${{ matrix.builder }} in the Build step is untouched.
Deliberately NOT set: `overwrite` (delete-then-upload races across 4 concurrent
legs), `archive: false` (new raw-upload path; hard-fails on multi-file globs),
`if-no-files-found` (left at the default `warn`, matching previous behaviour --
tighten separately once all four legs are observed emitting a binary).
Artifact names on the run page change from the gcc triple to the matrix label:
AltServer-x86_64 -> AltServer-amd64, AltServer-i586 -> AltServer-i386. RELEASE
ASSET FILENAMES ARE UNCHANGED, as they come from the file inside the artifact
rather than the artifact name. Job display names gain the arch label, which will
stale out any branch-protection required-check names.
The release job's `mv build_output/*/* build_release` still works: with no
`name:` input and merge-multiple off, v8 writes one subdirectory per artifact.
v5 added an `artifacts.length === 1` flattening case, unreachable here because
`needs: [build]` means all four legs must succeed and each uploads one artifact.
UNVERIFIED: the release job is tag-gated, so these two action bumps cannot be
exercised by a branch push. Testing them requires pushing a tag, which publishes
a real GitHub Release -- and action-gh-release@v3's make_latest has no default,
so it may displace the current "latest".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…yaMisty#111) The buildenv image has not been rebuildable for some time. Three independent problems stack up; this fixes the first two. The image still does not build, so NyaMisty#111 stays open -- but each fix was needed to reveal the next, and both are confirmed against a real build rather than reasoned about. 1. Apple versioned the archive's top-level directory. corecrypto.zip now extracts to corecrypto-2024/, not corecrypto/. The download and unzip were always fine; the failure was purely a path mismatch, made invisible by Docker's WORKDIR silently CREATING the missing /buildenv/corecrypto, so the error surfaced one line later as the confusing "source directory does not appear to contain CMakeLists.txt". Renamed version-agnostically rather than hardcoding -2024, and a `test -f corecrypto/CMakeLists.txt` now fails loudly at the real cause if Apple renames it again. 2. Apple's CMakeLists.txt include()s scripts/code-coverage.cmake, which the distribution does not ship -- scripts/ contains only the testvector converters. This is exactly what upstream PR NyaMisty#85 diagnosed in December 2022 ("isn't really relevant for our use") and it was never merged. CODE_COVERAGE is off by default, so the include at line 63 is the only reference that actually breaks configure; the uses at lines 106 and 359 are already guarded. STILL BROKEN, not addressed here: with both fixes applied, cmake now configures far enough to fail at CMakeLists.txt:266 with "No SOURCES given to target: corecrypto_static". CORECRYPTO_SRCS is populated at CoreCryptoSources.cmake:189, and the Linux branch subtracts CORECRYPTO_EXCLUDE_SRCS at line 262; something in that interaction empties the list on the 2024 distribution. That is Apple's CMake, not ours, and needs its own investigation. None of this blocks the main build, which pulls the prebuilt ghcr.io/nyamisty/altserver_builder_alpine_* images -- all four verified still publicly pullable, and all four legs currently build green against them. What is broken is only the ability to rebuild those images from source, which is a bus-factor risk rather than an outage. Verified by building the Dockerfile through the cmake step for arm64v8/alpine:3.15 under Colima; error 1 and error 2 each confirmed present before the fix and absent after. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
FetchAnisetteData() called response.extract_json() with no status or content-type
check, so any failure surfaced as one line naming neither the server nor the
status: "Incorrect Content-Type: must be textual to extract_string, JSON to
extract_json." That string is the most-reported failure in this project. The
hardcoded default anisette server it was talking to (armconverter.com) has been
returning HTTP 502 text/plain since ~2026-09, so every user hit it.
- GetAnisetteURL(): treat an empty value as unset, and remove the dead default
entirely. There is now no default: the old one is down, and pointing every
user at one shared anisette identity can get Apple IDs locked (issue NyaMisty#88).
Unset now yields a message naming the variable to set.
- FetchAnisetteData(): rewritten straight-line. It was ALREADY synchronous --
the old code chained pplx continuations then immediately called task.wait()
-- so this costs no concurrency and makes it possible to attach the URL and
status code to every failure. Transport errors, non-200 responses, non-JSON
bodies and missing/mistyped fields are each reported specifically, as a
ServerError(InvalidAnisetteData) rather than a raw cpprest exception, which
matters because ClientConnection::ErrorResponse dynamic_casts to ServerError
and forwards userInfo -- so the message now reaches AltStore on the device,
not just the server log.
- The http_client CONSTRUCTOR validates the URI and throws before any request:
"localhost:6969" (no scheme) raises std::invalid_argument, which is not an
Error subclass and would have reached the device as errorCode 0 (Unknown).
Since this commit makes the variable mandatory, a hand-typed value is the
likeliest failure it creates, so that path is handled explicitly.
- strptime()'s return value was ignored entirely; an unparseable timestamp
silently became whatever the zero-initialised struct produced. Now checked.
The format deliberately stops at the seconds rather than matching a literal
"Z": the value comes from a third-party anisette server, not from AltStore,
and tails like ".123456Z" or a bare timestamp denote the same instant.
An explicit numeric offset IS rejected, since timegm() ignores the tail and
would otherwise produce an instant silently wrong by that offset.
- mktime() -> timegm(). The timestamp is UTC; mktime interprets its input as
LOCAL time, so the instant sent to Apple was skewed by the host's UTC offset
on every machine not running in UTC.
- ResetProvisioning(): was iterating "C:\ProgramData\Apple Computer\iTunes\adi"
with fs::directory_iterator, throwing filesystem_error on every Linux run.
Both callers (AltServerApp.cpp:471, :516) invoke it from inside
catch (APIError&) on the InvalidAnisetteData path, so it escaped the handler
and replaced Apple's real error with a Windows path -- and at :471 it also
aborted the Sleep(12000) retry that follows. Now a no-op. Note the retry it
was suppressing will now actually run.
- README and --help both advertised the removed default, so both are updated.
The alt_anisette_server image this project used to recommend was last
published in April 2022 and is no longer presented as a recommendation.
Verified by linking the real object into a standalone harness and running it
against a local fake anisette server. Unset, no-scheme, malformed URL, refused
connection, HTTP 502, HTTP 404 and non-JSON each produce a specific message; a
well-formed response succeeds. Timestamp tails "Z", ".789012Z" and bare all
parse, "+02:00" and garbage are rejected. The timegm fix was confirmed by
running the success path under TZ=UTC, EST5EDT, JST-9 and IST-5:30 and getting
tv_sec=1789389296 in all four, matching 2026-09-14T12:34:56Z exactly.
Issue accounting, deliberately conservative:
NyaMisty#104 CLOSED. Symptom and trigger both removed.
NyaMisty#130 The valid-JSON-with-wrong-Content-Type shape is genuinely FIXED, since
extract_utf8string() ignores Content-Type where extract_json() refused.
Other shapes are now diagnosable, not fixed.
NyaMisty#99, NyaMisty#100, NyaMisty#128 Resolved by making the required configuration explicit and
self-describing, NOT by the error handling -- those servers are down and
no client-side change reaches them.
NyaMisty#88 Rationale only, not fixed. A user who picks a public shared anisette
server still shares an identity; this only stops shipping one by default.
Not addressed here: ServerError's recovery suggestion for InvalidAnisetteData is
Windows-only advice ("download the latest versions of iTunes and iCloud"), shown
on the CLI path. Fixing it means adding a substitution to
makefiles/rewrite_altserver_source.py, which belongs in its own commit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…d REVIVAL.md ALTSERVER_ANISETTE_SERVER is read per-request, deep inside FetchAnisetteData(). A daemon started without it therefore comes up cleanly, advertises itself over Bonjour and is discovered by the phone -- then fails only when someone first tries to refresh. That is a long way from the actual mistake, and it matters most in exactly the deployment this project exists for: an unattended headless Linux box where nobody is watching a console. A systemd unit missing Environment=, or `sudo` without -E dropping the variable, both produce a service that looks healthy and cannot sign in. The check WARNS rather than exiting, deliberately. Of the six request types the daemon serves, only AnisetteDataRequest needs an anisette server -- PrepareApp, InstallProvisioningProfiles, RemoveProvisioningProfiles, RemoveApp and EnableUnsignedCodeExecution (AltJIT) all work without one, and refusing to start would break those. It also catches a value with no http:// or https:// scheme, which is the likeliest way to get this wrong now that the variable is mandatory, and otherwise echoes the configured URL so the operator can confirm it was seen. Verified in all three states against a real build: unset and no-scheme each print their warning and the daemon still proceeds to advertise; a valid URL prints "Using anisette server: ...". Also adds REVIVAL.md, a working log for this branch. It records the project goal, the build procedure (macOS cannot build this; the tree lives inside a Colima VM), every commit so far, the verified facts behind them, and a ranked TODO. Several of those facts cost real effort to establish and are not recoverable from the code -- that the CI failure was an unresolvable action rather than the node12 versions; that the old artifact uploader served blobs with Content-Type "zip" which download-artifact@v8 does not treat as a zip; that Apple now ships corecrypto as corecrypto-2024/ and Docker's WORKDIR silently creates the missing directory; that only AnisetteDataRequest needs anisette. REVIVAL.md also records ~/Local Work/AltStore (rileytestut/AltStore, v2.3.3, actively maintained) as the reference to check FIRST for how something is currently done, and ranks the remaining work against the real goal: running unattended on a Linux home server, which makes Wi-Fi discovery and a working anisette server the critical path rather than incidental issues. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two answers that change the plan, both from the operator rather than the code: - The target device runs iOS 26.x, so NyaMisty#131 is confirmed in scope rather than hypothetical. Apps will install successfully and then crash at launch. That makes the upstream_repo bump required, not optional, and it is the largest remaining code task. Worth noting the failure is deceptive: installation reports success, so nothing looks wrong until the app is opened. - The host is a Dell OptiPlex 5060 (x86_64) running Proxmox -> Ubuntu VM -> Docker via Portainer, already publishing to ghcr.io/ben-diehlci/. So the x86_64 leg is the one that matters, sideloading is entirely LAN-local and must not go behind the existing Cloudflare Tunnel / NPM setup, and re-namespacing build_docker.yml stops being hypothetical because the registry namespace already exists. Adds item 8: a purpose-built container for the Portainer stack, which is the natural end state given that platform. Also records the question that gates everything and is not answered by the host notes: whether the Ubuntu VM is bridged onto the same L2 segment as the phone's Wi-Fi, or NAT'd behind Proxmox. mDNS is link-local and does not cross subnets or VLANs, so if the VM is NAT'd or the phone sits on a guest/IoT VLAN, the device can never discover _altserver._tcp regardless of how correct everything else is. Cheap to check, and it would invalidate a lot of downstream work. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…as the phone The operator confirmed the Ubuntu VM is bridged onto the same network as the phone, reaching server services by IP from the phone while at home. So there is no NAT or VLAN boundary and the mDNS prerequisite is satisfied. This was the question gating everything else, since mDNS is link-local. Keeps one narrower check rather than closing the item outright: reaching a host by IP proves L3 routability, while mDNS needs multicast on the same broadcast domain. A Wi-Fi AP doing client isolation or aggressive IGMP snooping can pass ordinary TCP while dropping multicast between wireless and wired hosts, which would look exactly like "the network is fine" right up until the phone cannot discover the server. Records the avahi-browse command that settles it by looking for the phone's own Bonjour advertisements from the server side. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Topology settled: avahi-browse on the VM sees _companion-link._tcp from Apple
devices over IPv4 and IPv6, so multicast crosses from Wi-Fi to the wired VM and
_altserver._tcp will reach the phone.
More importantly, records a landmine found while checking it. dnssd_loader.cpp
does not link Bonjour -- DNSServiceRegister builds a Python one-liner, forks and
execlps python3 with CDLL('libdns_sd.so'). The parent branch of that fork is
`else { ; }`: `status` is declared and never used, there is no waitpid, and the
function returns 0 unconditionally. Advertisement failure is indistinguishable
from success inside AltServer, so a server that cannot advertise runs normally,
logs nothing wrong, and is permanently undiscoverable by the phone. Already
reproduced in the alpine build container.
Two practical consequences for this deployment:
- The usual advice to install libavahi-compat-libdnssd1 is NOT sufficient. That
package ships libdns_sd.so.1, while the code dlopens the UNVERSIONED soname,
whose symlink comes from libavahi-compat-libdnssd-dev. Records the exact
python3 ctypes call to verify it, since it is the same call the program makes.
- The planned container (item 7) needs python3 and the compat dev package, or it
will silently fail to advertise.
Adds TODO 8: make that failure loud. Small patch, and it removes the worst
silent failure mode for an unattended box.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ly succeeding
DNSServiceRegister does not link Bonjour -- it builds a python3 one-liner, forks,
and execlps it. The parent branch of that fork was `else { ; }`: `status` was
declared and never used, there was no waitpid, and the function returned 0
unconditionally. Advertisement failure was therefore indistinguishable from
success inside AltServer, so a server that could not advertise ran normally,
logged nothing wrong, and stayed permanently undiscoverable by the device.
For the deployment this project exists for -- an unattended headless Linux box
-- that is the worst available failure mode. Nobody finds out until a sideloaded
app expires a week later, and the only evidence is an unlabelled Python
traceback interleaved into the parent's stderr.
Two changes, because either alone leaves a hole:
- The parent now polls waitpid(WNOHANG) for ~1s. The helper is meant to run
forever (its Python ends in Event().wait()), so a prompt exit means failure.
Polling rather than blocking because a healthy child never exits.
- The helper now exits non-zero when DNSServiceRegister itself returns an
error. Previously it blocked on Event().wait() regardless of the result, so a
loadable libdns_sd.so with no avahi-daemon behind it looked identical to
success no matter what the parent checked.
On failure it returns kDNSServiceErr_Unknown, which ConnectionManager.cpp:123
already handles by logging and returning without aborting, and prints remediation
naming the non-obvious part: libavahi-compat-libdnssd1 is NOT sufficient. That
package ships libdns_sd.so.1, while the code dlopens the UNVERSIONED soname whose
symlink comes from libavahi-compat-libdnssd-dev. Confirmed empirically on the
target host: CDLL('libdns_sd.so') raised OSError with libdnssd1's usual advice
applied, and succeeded only after installing the -dev package.
VERIFICATION IS INCOMPLETE, deliberately recorded rather than glossed. Both
failure paths were confirmed against a real build: missing libdns_sd.so, and
library-present-but-no-daemon. The SUCCESS path was NOT verified -- avahi will
not run in the alpine build container -- so the no-false-positive case still has
to be confirmed on the real host. A spurious error on a working server would be
worse than the bug this fixes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Finder writes .DS_Store into any directory it browses, so working on this repo from a Mac leaves untracked noise that shows up as pending changes in clients like GitHub Desktop and is easy to commit by accident. Three had already appeared: ./, libraries/ and makefiles/. A bare `.DS_Store` pattern matches at any depth, so one line covers all of them; verified with git check-ignore. Also ignores ._* (AppleDouble resource forks), which macOS creates when writing to non-HFS volumes. None of these were tracked, so ignoring them is sufficient -- no git rm --cached needed. Also restores the trailing newline the file was missing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…eb UI Both are the operator's stated end-goals, explicitly NOT scheduled. Written down with enough grounding to be picked up cold rather than re-derived. F1, self-contained deploy: point Portainer at the repo and have everything come up with no manual steps beyond credentials. Notes the shape this implies (compose file in-repo, image under ghcr.io/ben-diehlci/, network_mode: host, persistent AltServerData given it is a relative path, anisette as a second service) and that it depends on the existing registry-namespace and container items. Records the trap that the image must carry python3 and the libdnssd -dev package or advertisement fails silently. F2, web interface: flags that this is probably NOT optional. While recording it I checked how the two-factor code is obtained, and rewrite_altserver_source.py:96 reads it with `std::cin >> _verificationCode` -- from stdin. Under systemd stdin is /dev/null, and in Docker without -i likewise, so 2FA sign-in cannot currently be completed in the intended deployment at all. ShowAlert has the mirror-image problem: it calls getchar() and blocks on an interactive TTY. That reframes the research question from "build a web UI" to the narrower and cheaper one that has to be answered first: is the Apple session persisted under AltServerData and reused, making 2FA a one-time step that a single `docker run -it` could satisfy, or is a code needed on every refresh? The answer decides whether a web UI is a convenience or a hard dependency of the whole unattended premise. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…strap-only, and 2 lines Deployment research landed and corrects an earlier entry in this file that I had wrong, plus adds two findings that change what "unattended" costs. CORRECTION. A 7-day refresh does NOT re-sign anything and does not transfer an IPA. AltStore/Operations/RefreshAppOperation.swift:68 sends exactly one request, InstallProvisioningProfilesRequest, landing at ClientConnection.cpp:238 -> DeviceManager::InstallProvisioningProfiles -> misagent. Signer has exactly ONE call site in the whole server, AltServerApp.cpp:1453-1454, inside InstallApp, reachable only from the CLI install path. Verified both by grep here rather than taken on trust. So the 2022-stale signer and NyaMisty#131 block the FIRST install on iOS 26.x, not the recurring refresh that is the actual goal. This file previously called it "the largest remaining code task" on the critical path; it is not on the critical path at all. Also: NyaMisty#131 is two lines, not a submodule bump. ldid.cpp:2215 runs hash.resize(20) before :2217-2220 captures alternateCDSHA256 = hash, so the SHA-256 hash-agility attribute carries a 20-byte-truncated hash. Confirmed in source. The competing diagnosis in the issue thread does not hold against this tree -- DER entitlements are emitted, CodeDirectory version is 0x00020400, and a SHA-256 alternate CD is present. NEW: pairing needs one physical USB connection. config.h:99 is #undef HAVE_WIRELESS_PAIRING and idevicepair.c:180-182 says wireless pairing is Apple-TV-only. Steady-state needs no Mac or PC, but the premise does not budget for that one cable trip. NEW, and the reason to build a watchdog: the phone fails silently too. BackgroundRefreshAppsOperation.swift:60 sets ignoresServerNotFoundError = true, consumed at :221-223 to suppress the alert, so a 3am refresh that finds no server notifies nobody. RefreshAllAppsIntent.swift:187 sets it to false, so a manual refresh DOES surface the error -- which makes manual refresh the diagnostic tool and background refresh the thing that goes quiet. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… failures Scope narrowed by the operator: ONE iPhone, iOS 26.x, no iPad and no other devices. Multi-device concerns (activeProfiles juggling, device-slot exhaustion, NyaMisty#113/NyaMisty#86) are therefore out of scope. Records two things worth far more than the code written so far. HOST PREREQUISITES, as a concrete checklist with file:line justification rather than folklore. Highlights that are not obvious and are not in any README: netmuxd must OWN /var/run/usbmuxd with usbmuxd stopped, because stock usbmuxd never emits ConnectionType "Network" and the two collide; the widely-copied USBMUXD_SOCKET_ADRESS instruction in NyaMisty#49 is misspelled (one D) and silently ignored; NTP matters on the ANISETTE host, not the AltServer host, because Linux forwards the server's timestamp verbatim where macOS stamps Date() locally; /var/lib/lockdown needs BOTH <UDID>.plist and SystemConfiguration.plist backed up as a unit; the firewall needs the whole ephemeral range because sin_port = 0 means the port changes every start; and a container needs init: true because the binary installs no SIGTERM handler, so as PID 1 every restart costs the full grace period then SIGKILL. SILENT FAILURE MODES, ranked -- the actual enemy for an unattended box. Fourteen of them, each traced to source. Two deserve calling out here: - Our own dnssd fix (04e928a) does NOT close the advertisement hole completely. It catches a child that exits, and the sys.exit addition catches a non-zero DNSServiceRegister result, but a user in closed_issue_0051 got result 0 with avahi-daemon stopped, consistent with avahi-compat deferring via AVAHI_CLIENT_NO_FAIL. avahi can lie about success and nothing in-process can detect it. Recorded as an explicit limitation rather than left implied. - Real device faults are DISPLAYED as "AltServer could not be found", because AltStore remaps deviceNotFound/lostConnection to serverNotFound for any wireless server that is not isPreferred, and this port hardcodes serverID "1234567" where Mac/Windows use a UUID. So isPreferred is permanently false here and every netmuxd or pairing fault wears the wrong error message. That will send a debugger at mDNS when mDNS is fine. Conclusion recorded: build an external watchdog before trusting any of this. Nothing inside AltServer can be trusted to report its own health -- it has no liveness signal, journalctl -p err is empty no matter what breaks, and adding -d makes the two most diagnostic libusbmuxd lines disappear. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AltStore is not yet installed on the target device -- installing it is the goal -- so the CLI bootstrap path is required, and that is the one path that calls Signer::SignApp. NyaMisty#131 is therefore the FIRST thing that will bite, not a later concern. ldid computed the CodeDirectory hash and truncated it to 20 bytes BEFORE capturing the SHA-256 value that becomes the hash-agility attribute (OID 1.2.840.113635.100.9.2). The attribute carried a SHA-256 hash chopped to 20 bytes, CoreTrust rejected the signature, and the app died at launch with no crash report -- while the INSTALL reported success, which is what makes it so deceptive. Two statements swapped: capture the full hash, then truncate for the cdhashes array, which genuinely does want 20 bytes. Applied in makefiles/AltSign-build/rewrite_ldid_source.py rather than in upstream_repo/ldid/ldid.cpp, because that is a submodule and this rewriter is the project's existing mechanism for patching vendored sources at build time. The patch is guarded: if the pattern does not match exactly once it writes a diagnostic to stderr and exits non-zero, failing the build rather than silently shipping broken signatures again. Guard verified by feeding it already-patched source (exit 1, clear message), and the file check means lookup2.c passes through untouched. Verified: full rebuild exits 0, build/ldid_patched/ldid.cpp has the capture before the resize, binary links. NOT VERIFIED: that this actually makes an app launch on a real iOS 26 device. The diagnosis is confirmed in source and matches jaakkopalvaila's report in issue NyaMisty#131, and the competing diagnosis in that thread was checked and does not hold against this tree -- DER entitlements are emitted, CodeDirectory version is 0x00020400, and a SHA-256 alternate CD is present. But the only real test is installing AltStore on the phone and opening it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The previous entry recorded a pre-amend hash, which never existed on this branch. A commit cannot contain its own SHA, so the reference has to land in a follow-up. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both are on the path of the very first thing that will be run -- a hand-typed CLI install of AltStore.ipa -- so they are worth clearing before that attempt rather than debugging mid-bootstrap. 1. PR NyaMisty#135, X-MMe-Client-Info sanitization. Since ~2026-09 Apple's GSA edge rejects with an immediate 503 any request whose X-MMe-Client-Info contains "com.apple.dt.Xcode", and anisette servers commonly return exactly that -- upstream AltStore still builds one containing com.apple.dt.Xcode/3594.4.19 as of v2.3.3, confirmed in the local clone. Rewritten to com.apple.akd at the single point where anisette data enters the program. This is an unverified third-party claim that cannot be tested without a real Apple ID, and it alters a header Apple sees, so it is defeatable at runtime: ALTSERVER_NO_CLIENTINFO_SANITIZE=1 disables it. If sign-in fails with the rewrite in place, toggle that before blaming the anisette server. It also logs when it actually rewrites something, so the logs say which mode was in effect. 2. argv parsing in AltServerMain.cpp. `case 'a'` had no break and fell through into `case 'p'`, so `-a ID` set the password to ID as well. With the conventional `-u -a -p` ordering it works by accident because -p overwrites afterwards; with `-p PASS -a ID` the password silently becomes the Apple ID and sign-in fails with an unrelated-looking error. All five argv pointers were also uninitialised while udid/appleID/password are read unconditionally at the install call, making a missing flag undefined behaviour. And 'h' was handled in the switch but absent from the optstring, so -h printed "?? getopt returned character code 077 ??" before the usage text. Added a guard that names exactly which required flags are missing instead. Verified against a real build: -h prints usage cleanly; `-u X -a Y` with no -p now reports "Missing: --password", which is the direct proof that -a no longer sets it; all three flags together pass the guard. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Layer 3 is solved, and my earlier local diagnosis of it was wrong. The CI logs
for the buildenv run show the error I had missed locally, because I only looked
at the trailing message:
CMake Error at CMakeLists.txt:266 (add_library):
Cannot find source file:
corecrypto_static/ccrng_static.c
"No SOURCES given to target: corecrypto_static" is a FOLLOW-ON, and it is the
one people notice -- which is why this reads as "the source list is empty" when
the list is fine and exactly one entry is stale. Apple's 2024 distribution moved
ccrng_static.c to the tree root but left CoreCryptoSources.cmake:251 pointing at
the corecrypto_static/ subdirectory, which no longer exists.
Fixed with a guarded sed: it asserts ccrng_static.c is at the root, that
corecrypto_static/ is genuinely absent, and that no stale reference survives, so
it fails the build loudly if Apple moves things again rather than silently
not applying.
cmake now reports Configuring done / Generating done / Build files have been
written. The full corecrypto make is still building at time of commit, so this
is not yet claimed to close NyaMisty#111 -- configure passing is necessary, not
sufficient.
Also settled from the same logs: this is NOT architecture-specific. The CI run
is amd64 and fails identically to local aarch64, which closes the open question
recorded earlier about probing the amd64 leg.
Adds BOOTSTRAP.md: the first-time install runbook, phased so the cabled install
is proven before wireless refresh is attempted. Includes the exact anisette JSON
contract our client requires, the restart-persistence check for anisette
identity, an error-to-meaning table for the install output, and the explicit
warning that "Finished!" prints even on failure.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The full corecrypto make and make install now exit 0 in the alpine builder, so this is no longer "configure passes, rest unknown". All three layers are fixed: the corecrypto-2024 directory rename, the missing scripts/code-coverage.cmake include (upstream PR NyaMisty#85's diagnosis, unmerged since December 2022), and the stale corecrypto_static/ccrng_static.c path in Apple's own source list. The buildenv image can be rebuilt from source again, which it could not for roughly four years. That matters beyond the issue: the whole build depends on four prebuilt ghcr.io images that nobody could reproduce, so this removes a real bus-factor risk rather than just closing a ticket. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
systemctl enable usbmuxd prints 'The unit files have no installation config' because the Ubuntu unit has no [Install] section by design: usbmuxd is started by udev when an iOS device is plugged in. The instruction produced a confusing message for no benefit. The real check is whether idevice_id -l sees the phone in Phase 3. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…Portainer Two gaps the operator hit. First, 'copy it to the VM' was hand-waved: the artifact downloads to the Mac's browser and GitHub artifact URLs require an authenticated API call even for a public repo, so curl on the VM does not work. Records the scp line and the unzip dependency. Second, and more important given this host is managed entirely through Portainer: states explicitly which pieces are stacks and which are not. The anisette server is a long-lived service with persistent state and belongs in a stack. AltServer's BOOTSTRAP run does not -- the 2FA code is read from stdin, so that one run needs a real interactive terminal. It becomes a stack afterwards, once sign-in no longer needs stdin, which is future item F1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… binary Two corrections from a real run-through. The instructions assumed a .zip, but Safari's 'Open safe files after downloading' expands it, so the operator ends up with a bare AltServer-x86_64. Covers both cases, and notes chmod +x is needed regardless -- scp does not reliably preserve the bit either, on top of artifact upload stripping it. More useful: adds a string check to distinguish the new binary from one built before the fixes, which are otherwise indistinguishable. Presence of 'No anisette server is configured' and ABSENCE of the dead armconverter default are sufficient. This matters because installing from a pre-fix artifact on iOS 26 produces an app that installs cleanly and crashes at launch, which reads as a failed deployment rather than a stale binary. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The usage text documents "-h --help" and the switch has handled 'h' all along,
but "help" was never listed in long_options. So --help fell through to the
default branch, which printed
unrecognized option: help
?? getopt returned character code 077 ??
above the usage text and exited 1. It looked like it worked, because the error
branch prints the usage too -- which is why it went unnoticed, including by me
when I added 'h' to the optstring in 04dd7ee. That change fixed -h and left
--help still broken.
Both forms now print identical output and exit 0.
Found by running the shipped binary rather than by reading the source, which is
the second time in this session that a documented-but-unreachable flag only
surfaced under actual execution.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
USB pairing succeeded, which was the step with the most hardware unknowns: Proxmox passthrough worked, idevicepair validate returns SUCCESS, and /var/lib/lockdown is backed up containing both the per-device plist and SystemConfiguration.plist -- the pair that must be restored together. Records one detail worth not rediscovering: idevicepair validate fails with 'Could not validate ... because a passcode is set' unless the device is unlocked at that moment. It succeeds on retry once unlocked. Device UDID deliberately not recorded here; it identifies the hardware. Anisette (Phase 2) is now the only gate before the install itself. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…no data The check as written compared jq output from two files without confirming either contained anything. jq -r on an empty file prints an empty string and exits 0, so when the server returned nothing the loop compared "" to "" four times and reported every field "stable" -- a confident pass on a server that was not answering at all. Hit for real during Phase 2. Now guards first: both files must be non-empty and valid JSON, and each field lookup uses jq -e so a missing key is reported as missing rather than silently becoming an empty string that matches the other empty string. Also adds an explicit "does it answer at all" step, because `curl -s … | jq` on an empty response prints nothing, which reads as a pass at a glance rather than as a failure. Fitting, given this repo: a verification that cannot fail is worse than no verification, and that is the same class of bug as the mDNS advertisement returning success unconditionally. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… get wrong
The operator manages this host entirely through Portainer, so Phase 2 is now a
compose stack (deploy/anisette-stack.yml) rather than the docker run commands it
described before.
THE FINDING THAT MATTERS. dadoum/anisette-v3-server's own README -- and every
forum post copying it -- tells you to mount
/home/Alcoholic/.config/anisette-v3/lib/
That subdirectory holds ONLY the two Apple .so files fetched at first run. The
machine identity is one level up. Verified in the source commit the published
image was actually built from:
59: configurationPath = expandTilde("~/.config/anisette-v3");
95: libraryPath = configurationPath.buildPath("lib");
134: v1Device = new Device(configurationPath.buildPath("device.json"));
136: v1Adi.provisioningPath = configurationPath;
Mount lib/ and the identity sits on the container's writable layer, so every
Portainer "Update the stack" destroys it: a new machine is minted, Apple demands
2FA, and unattended refresh dies silently with nothing logged. That is issue NyaMisty#86,
and it comes straight from the official documentation.
Two other load-bearing settings, both non-obvious:
- TZ must be UTC. The server stamps X-Apple-I-Client-Time from LOCAL wall-clock
time and then appends a literal "Z", so under any other TZ it sends Apple a
timestamp wrong by the UTC offset while claiming to be UTC -- well-formed,
contract-passing and silently wrong. The existing stacks on this host use a
local TZ, so this is an easy and invisible mistake to make.
- The healthcheck probes /v3/client_info, never /. The v1 route at / performs
real provisioning against Apple when unprovisioned, so polling it would hammer
Apple's endpoint at exactly the moment the identity volume went missing.
Image is digest-pinned deliberately: :latest is the only published tag and is
~17 months behind the git source because upstream's publish workflow keeps
failing. Digest verified to resolve, and :latest is a multi-arch index including
linux/amd64.
Confidence is recorded honestly rather than flattened: the JSON contract and the
volume path are verified in source; that this yields a successful Apple sign-in
is NOT -- no completed AltServer-Linux sign-in has been reported in 2026, and
every success report predates both the GSA block and iOS 26.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e inputs A submodule bump changes only its gitlink, so the push reports the bare path. Verified against this repo's history -- `git show --name-only fa3abe2` prints exactly `upstream_repo`, never `upstream_repo/<anything>`. GitHub's `upstream_repo/**` requires that `upstream_repo/` prefix, so it would not match, and the image would keep the old binary with CI green. The bare entry currently in build_image.yml is correct, but check_workflow_paths.py could not tell the difference: covered() stripped `/**` and compared base names, so rewriting that entry as `upstream_repo/**` -- which reads as a tidy-up, since every other entry is a glob -- still printed ok and exited 0. The guard now reads gitlinks from `git ls-files -s` (mode 160000) and requires an exact match when a compile input is itself a submodule. Globs still legitimately cover submodules nested inside a covered directory: bumping libraries/libimobiledevice reports that full path, which 'libraries/**' matches, so the previous commit's entry is right as written. Mutation-tested both directions: 'upstream_repo' -> 'upstream_repo/**' now fails naming the submodule and the offending glob, and dropping 'libraries/**' still fails as before. All four guards pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A refresh triggered from AltStore completed over Wi-Fi with no cable attached: the 44900-byte InstallProvisioningProfilesRequest removed both old profiles, installed two with fresh UUIDs, and logged "Finished handling request!" with no "Failed to handle request:" anywhere in the run. Before the fix a request of almost exactly that shape (44887 bytes) died at "There was an error connecting to the device." Those profile lines are conclusive rather than merely encouraging. They come from DeviceManager.cpp:985/:1029, inside the function at :757 that first calls idevice_new_with_options with IDEVICE_LOOKUP_NETWORK -- reaching idevice_from_mux_device, patch site 2 -- then lockdownd_client_new_with_handshake, which needs a real TCP connection through idevice_connect, patch site 3, then misagent over that connection. None of it can print unless the network device connection was established. netmuxd meanwhile still reports the device with byte-identical connection data, 02 00 00 00 C0 A8 04 2D, the same blob that used to fail. It was not upgraded or reconfigured, so the same input now parses purely because the parser was fixed -- the controlled comparison the diagnosis predicted. The lockdown handshake succeeding over the network also rules out the pairing record being unusable for a network device, which had been flagged as the most likely next failure. Not proven by this run, and recorded as such: unattended refresh, since this was triggered by hand and iOS waking AltStore is a separate ceiling; and the 124932582-byte full-install path, which uses the same connect path but was not re-exercised. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…very page The status page read FAIL "No device on either transport" while wireless refresh was demonstrably working. Not a timing artefact: status_checks.py probed the netmuxd socket with `idevice_id -l`, and -l is not a verbosity switch, it selects the transport. tools/idevice_id.c sets include_usb for -l, include_network for -n, and both only when neither is given. netmuxd presents the phone solely as ConnectionType: Network, so -l against its socket returns an empty list unconditionally -- the check was structurally incapable of reporting OK for a wireless-only deployment, which is the only deployment this project targets. idevicepair validate on the success path had the same defect: idevicepair.c:372 picks IDEVICE_LOOKUP_USBMUX unless -n is passed, so it validated a USB pairing that cannot exist on a cable-free server. Both had to be right for the check to go green and neither was. The transport flag is now a required argument of _devices_via rather than a literal inside it, so the two probes cannot drift onto the same transport again. This also explains an earlier loose end: `idevice_id -l` inside the altserver container printing nothing, previously recorded as unexplained, was the same USB-only flag against the same network-only mux. It was never evidence. Worth noting the direction of the error. During the sockaddr bug this check said FAIL and was read as confirmation; it was right by accident and went on saying FAIL after the fault was fixed. A check that cannot pass is worse than no check, because it is trusted. Separately, the three pages now carry a shared tab bar (Status / Pairing / Install AltStore) instead of scattered inline links. Built by one _nav() helper and injected after all three page strings exist, because PAIRING_PAGE and INSTALL_PAGE are derived from PAGE's <head> and would not inherit a nav placed in PAGE's <body>; three hand-written copies would drift. The active tab is marked with aria-current and the styling hangs off that attribute, so the stylesheet and a screen reader cannot disagree. Verified rendered at desktop and 375px, all three tabs on one row with the right one active and no horizontal scroll. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The cron watched the upstream_repo submodule, which is rileytestut/AltServer-Windows -- not this repo's upstream, NyaMisty/AltServer-Linux. That submodule is pinned at 071b1dd from 2022-04-25 and has not moved since, so the job ran six times a day for years and found nothing. The noise was the lesser issue. On a hit the build job ran `git submodule update --remote -- upstream_repo` and built all four architectures from that newer source without committing it, so a published artifact could come from code no commit in this repo describes. The build rewrites those sources textually and rewrite_altserver_source.py has no match guards at all, so an upstream move could change what the binary does with nothing failing. Unattended is the worst place for that. The check still runs on demand via the existing sync_upstream workflow_dispatch input. The `github.event_name == 'schedule'` conditions in the check and matrix_setup jobs are left in place, so restoring the cron line is the only edit needed to get the old behaviour back. This removes the unattended trigger, not the underlying hazard; guarding rewrite_altserver_source.py stays on the TODO list. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Revive the fork: unattended wireless refresh on a Linux home server
…list 46 agents across four lenses -- stale files, fresh-clone usability, leaked personal data, repo hygiene -- run after bd/revival merged into new. Every one of the 17 findings recorded survived an adversarial attempt to refute it; roughly twice as many were refuted and dropped. Recorded as an unticked checklist rather than prose so the work cannot quietly go missing, and so each item is ticked off in the same commit that fixes it -- the same discipline the rest of this log uses. Also records what was verified GOOD, so it is not re-litigated later: the GHCR image is anonymously pullable, all six submodule URLs resolve, netmuxd's release assets and the digest-pinned anisette image both return 200, nothing secret or generated is tracked, and every shim and both CI workflows are genuinely reached. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rub personal data A11, plus a repo-wide sweep for the user's own data. web/installer.py's docstring has always claimed "Every line is filtered before it leaves this module." It was not true. The filter was a blocklist built around Apple's token names -- GsIdmsToken, adsid, DsPrsId -- and missed the anisette machine identifiers a successful sign-in prints one per line: MachineID, One-Time Password, Local User ID and Device UDID. All four rendered in full into a page served over plain HTTP on the LAN, which is exactly the screenshot risk the redaction exists to prevent. Verified against output captured from a real install, not from reading the regex. The filter now masks the VALUE and keeps the LABEL, so the page still shows how far an install got -- a filter that redacts everything is safe and useless, and would push the operator back to `docker logs`, which is not filtered at all. A catch-all also masks any unlabelled 40+ character base64/hex run, so an identifier nobody enumerated fails closed. tests/check_redaction.py enforces this in CI, in both directions: no secret value may survive, and listed progress lines must come through byte-identical. Asserting the filter "looks right" is what failed here, so it asserts on output instead. Separately, scrubbed the repo of personal data. No credentials, keys, tokens, emails, device serial or Apple team ID were present -- verified by scan. What was present: a LAN IP pair, an SSH username and a hostname, across BOOTSTRAP.md, REVIVAL.md, deploy/anisette-stack.yml and a test fixture. The addresses were shifted rather than flattened so REVIVAL.md's same-/22 subnet discussion still holds and the captured netmuxd bytes still decode to the address beside them; check_conn_data_layout.py caught one place where that consistency slipped. NOTE: this cleans the working tree only. The values remain in git history. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
History was rewritten with git-filter-repo to remove a LAN IP pair, an SSH username and a hostname from all 128 commits. Nothing else of the operator's was ever committed: no Apple ID, password, token, session key, p12, private key, certificate, team ID or device serial appears anywhere in history, verified by pattern scan across every commit. The GsIdmsToken / adsid / DsPrsId / X-Apple-I-MD-M hits are field NAMES in presence-checking scripts and in installer.py's own redaction regex, never values. Rewriting changed every commit SHA, and this log cites them heavily. 39 references across REVIVAL.md and BOOTSTRAP.md were remapped through filter-repo's commit-map. Five were deliberately left alone because they are not commits of this repo: c6f89dea and the 806ab8d37cf / a172604e5af fixes in libimobiledevice, 071b1dd in AltServer-Windows, and 56854e66. Every remaining cited SHA now resolves here. The rewrite was a no-op on file contents: each replacement targeted a string the working tree no longer contained, so the tip tree hash is unchanged at 7b6eaaf before and after. Only ancestry changed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… in the repo The repo sweep is done and history is rewritten. This records the other half: the Apple ID password, signing p12, anisette machine identity and pairing record all live on the deployment, and four of those locations were verified directly against the source rather than assumed. Distinguishes working state (deleting it breaks unattended refresh) from stray copies and logs (safe to clear), because conflating the two is how someone follows a security checklist and ends up re-pairing over USB for no reason. Notes one real exposure: altserver-web binds 0.0.0.0 and the status page shows the device UDID and anisette Device-Id to anyone on the LAN. The sweep was cut short by a session limit, so items beyond the four marked (verified) are recorded as found-but-unverified rather than presented as fact. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ntity A git-credential-manager diagnose run dropped gcm-diagnose.log into the repo root during this session. It was never committed, but it captured the whole process environment including an unrelated session token, and nothing would have stopped it being staged. The additions are the classes that actually matter for this project rather than a generic template: signing material (*.p12 holds the PRIVATE KEY for the Apple developer certificate, and AltServer caches one named after the team id under AltServerData/Certificates/), .env files carrying an Apple ID password, the anisette and lockdown backup tarballs BOOTSTRAP describes taking, device pairing records named after the phone's UDID, and *.log. /build_*/ is written with the underscore deliberately: /build*/ would also match the tracked buildenv/ directory and silently stop new files there from being committed -- the same class of bug as the paths: filter omissions already in this log. Verified both directions: no tracked file becomes ignored, and each new pattern actually matches what it claims. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Closes audit items A1, A2, A3, A4, A6, A9 and A10. README advertised BOOTSTRAP.md as "first-time setup, start to finish", but it was an operator log for one machine. It opened with `git push origin bd/revival` -- a stranger has no push access, and that branch no longer exists -- then scp'd a binary from a named account at a hardcoded LAN address, and skipped the host prerequisites entirely with "Already done on this host", omitting the single package README calls the most common way to end up invisible to your phone. It also contradicted the rest of the repo on whether the project works. Its confidence table still read "Low -- unproven. No one has reported a completed AltServer-Linux sign-in in 2026"; it framed NyaMisty#131 as an open unknown; and it made wireless refresh a speculative Phase 7. All three are confirmed working and README says so. Worst, its Phase 7 told the reader to stop usbmuxd and let netmuxd take /var/run/usbmuxd -- the opposite of what this stack deliberately does. netmuxd gets its own --socket-path so the host usbmuxd keeps the cable and nothing contends. web/pairing.py repeated the same wrong advice inside the UI; corrected there too. Rewritten around what the repo actually ships: pair over USB, deploy the stack, install through the web UI (which exists because the 2FA code is read from stdin and a detached container has no terminal), rather than a one-shot binary with the password on the command line. Every trap that is still true was kept -- the anisette lib/ mount, usbmuxd not being enableable on Ubuntu, "Finished!" not meaning success, the misleading "AltServer could not be found", -d making diagnosis worse -- and two were added: the checks that cannot fail, and that changing the Apple ID password permanently kills unattended refresh until someone re-enters it on the phone. README's "No host preparation is needed" was also wrong, and is now accurate: the stack bind-mounts the host's avahi, D-Bus and usbmuxd sockets plus /var/lib/lockdown, so Phase 1 is a real prerequisite. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Audit A5, A7, A8, A12, A13, A14, A15, A16, A17 -- all 17 items now closed.
Real defects:
A12 shims/old-linux-polyfill.c leaked a file descriptor on every getrandom()
call: /dev/urandom was never closed on the success path, the short-read
path, or the error path. AltServer is a long-running daemon whose signing
work calls this repeatedly, so it accumulated for the life of the process
and would surface far from here as an unrelated open() failing with EMFILE.
It also returned short reads verbatim, so a caller assuming buflen bytes of
entropy could use an uninitialised tail. Now loops, handles EINTR, opens
O_CLOEXEC, and preserves errno across close().
A16 ad-m/github-push-action@master was an unpinned floating branch from another
account, running in a job holding contents: write with GITHUB_TOKEN.
Replaced with plain `git push` -- actions/checkout already persists
credentials, so the third party bought nothing. softprops/action-gh-release
was also moved from the mutable v3 tag to its commit.
A8 the commented-out build fallback in the stack used `context: .`, which
resolves relative to the compose file and so pointed at deploy/, which has
no Dockerfile. Now ../ and docker/Dockerfile.
A5 libraries/ideviceinstaller was a submodule nothing compiled -- a standalone
CLI with its own main(), unlinkable into AltServer even deliberately.
Removed, along with the two places that asserted it was compiled in.
Stale claims corrected against the code: A14 web/server.py said it "deliberately
does NOT sign in or handle 2FA" while serving /install, which does exactly that;
A15 the Dockerfile said the web UI is not started automatically, but the stack
runs it as a service; A17 --help called ALTSERVER_NO_SUBSCRIBE "(*unused*)" while
rewrite_altserver_source.py splices in a getenv for it; A13 a closed TODO carried
guidance ("do not keep usbmuxd running") that this stack has since inverted; A7
the published image is linux/amd64 only and nothing said so, which a Raspberry Pi
user would discover at pull time.
Layout: BOOTSTRAP.md folded into README.md. The two documents overlapped in five
places -- prerequisites, quick start, the web UI, Wi-Fi refresh and running the
binary directly -- and that duplication is what produced the contradictions the
audit found, with each copy drifting separately. One document cannot disagree
with itself. REVIVAL.md moved to docs/, Dockerfile and docker-entrypoint.sh to
docker/, leaving the root to README.md, LICENSE, Makefile and dotfiles.
Verified rather than assumed: the image builds end to end from docker/Dockerfile
with COPY docker/docker-entrypoint.sh landing correctly, the static binary builds
clean and contains the new help text, all five guards pass, and no anchor or
cross-reference is left dangling.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…days ago The item asked to deploy and prove the sockaddr fix against a real phone. That happened and is recorded in the Done section above -- profiles refreshed over Wi-Fi with no cable and netmuxd's connection data was byte-identical before and after -- but the TODO entry was never ticked, so the list still read as though the central piece of work were outstanding. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…did not TODO 0b. Every source in upstream_repo/AltServer is passed through this script on the way to the compiler, and the substitutions are matched against upstream text. If upstream renames a symbol, reflows a signature, or changes an include, a pattern silently stops matching and the build still succeeds -- emitting a binary quietly missing a transformation. This is the rewriter that strips the Win32 GUI and splices in the console implementations of Authenticate, ShowAlert and Start, so "quietly missing" here means the parts that talk to Apple. It had no raise, assert or sys.exit anywhere. Two kinds of check, because the two kinds of substitution fail differently. The AltServerApp.cpp block runs for exactly one file and every substitution in it is mandatory, so each asserts a match count. Those counts were MEASURED against the current submodule rather than assumed -- strsafe.h is included twice, and an assertion of ==1 would have broken the build on correct input. The global substitutions run over all 35 files in that directory, including binaries like MenuBarIcon.ico and Resource.aps, and legitimately match zero times in most of them, so a per-file count would be meaningless. They are checked as post-conditions on the output instead: no L"..." literal, no boost::filesystem, no bare std::wstring may survive (std::wstring_convert is fine, and is what line 15 deliberately reconstructs). Post-conditions are stronger than counting here because they also catch an occurrence arriving in a NEW form the pattern was never written to handle -- and weaker in one way, so both are used: they only fire on a file that actually contains the construct. Behaviour is unchanged: output is byte-identical to the unguarded version across all 35 files. Nine mutations were introduced one at a time and all nine failed the rewrite -- five upstream moves (an include renamed, strsafe.h included once instead of twice, a removePart anchor renamed, an insertion marker reflowed, the Authenticate signature reflowed) and four broken substitutions caught by their post-conditions. Full static binary still builds clean. README's rewriter table said this one does not fail loudly. That is no longer true, and the table now explains why this one needs both mechanisms. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ntial-hygiene tool TODO 0c, TODO 1, and the server-side half of the credential sweep. 0c. Makefile:24 was a multi-target rule naming both archives, which GNU make expands into two independent targets each carrying the same recursive recipe. Both are .PHONY so both always ran, and libplist.a is reachable through lib_AltSign while libimobiledevice.a is reachable through lib_libimobiledevice -- so under -j, which every shipping build uses, the sub-make ran twice concurrently. Every object compiled twice to the same path and `ar rcs` ran twice on the same archive: wasted time, and two cc processes writing one .o is a corruption risk that would surface as an unrelated link error. libplist.a now depends on libimobiledevice.a and carries no recipe of its own, because the sub-make's `all` target already builds both. Measured before and after: invocations 2 -> 1, idevice.c.o compiled once, ar rcs libplist.a run once, both archives and the binary still produced. 1. InvalidAnisetteData's recovery suggestion told the operator to install iTunes and iCloud from Apple rather than the Microsoft Store. There is no iTunes to install on Linux, and the real causes are an unreachable or unhealthy anisette server and clock skew on the anisette host, whose timestamp is forwarded to Apple verbatim. It had to be rewritten in the rewriter rather than supplied at the throw site, because ServerError::localizedRecoverySuggestion() returns from that case directly and never reaches the default branch a userInfo key would populate. The substitution is guarded, so an upstream reword fails the build rather than silently leaving the Windows text in place. Verified in the linked binary. deploy/credential-hygiene.sh audits the places this deployment stores Apple credentials and device identity, and can clear the safe ones. Report-only by default. It counts credential markers in container logs rather than printing them, and never emits a secret value. Its most important property is what it refuses to do. Four things are working state, not leaks -- the Apple ID password in the container environment, the signing p12, the anisette machine identity, and the pairing record in /var/lib/lockdown -- and no flag removes them, because a security checklist that quietly forces a USB re-pair is worse than no checklist. It flags the 0.0.0.0 web UI binding, which publishes the device UDID to the LAN, and gives the exact one-line change rather than making it silently. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ed sections
Smoke-testing on a host with none of the containers present surfaced a real bug:
`grep -c` prints 0 AND exits 1 when nothing matches, so `|| echo 0` appended a
second zero and the arithmetic test failed with
[: 0
0: integer expression expected
for both the container-log scan and the shell-history scan. set -e is not on, so
the non-zero exit needed no handling in the first place.
The interrupted-install section also printed nothing at all when neither
container existed, which reads like a crash rather than a skip. It now says so.
Verified in both directions against a scratch HOME: report-only flags four items
and deletes nothing, --clean-strays removes the three stray copies, and
.bash_history is flagged but never auto-deleted.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
build_docker.sh hardcoded ghcr.io/nyamisty as the push target, so in this fork every docker push was denied -- a fork's GITHUB_TOKEN cannot write to another account's packages. The job built four architectures under QEMU first, so it spent several minutes doing that before failing at the first push, every time, for anyone who ran it. The namespace now comes from GITHUB_REPOSITORY_OWNER, which Actions supplies, so it works in any fork without editing. GHCR_NAMESPACE overrides it, and the value is lowercased because GHCR requires that (Ben-Diehlci -> ben-diehlci). It refuses to run rather than guessing when neither is set. Also added set -euo pipefail. Without it a failed build fell through to its push, and a failed push fell through to the next architecture, so the job's exit status reflected only the last command. This changes where images are PUBLISHED and nothing else. All six consumers -- both workflows, docker/Dockerfile, the stack's commented build block and README -- still pull ghcr.io/nyamisty/altserver_builder_alpine_*, which are public, alive and what every build currently uses. Repointing them before this script has successfully published a replacement set would break the build outright, so that stays a separate deliberate step; the script prints the list on success. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e CI needs The script built armv7 first, and the set -e added in the previous commit means the first failure stops the run. armv7, aarch64 and i386 all build under QEMU emulation on a GitHub runner: slow, and by far the likeliest to fail. amd64 is native, and is the only one anything consumes -- build_image.yml passes it as the BUILDER arg. So the previous commit made one case worse than the unguarded script it replaced: there, a failed armv7 fell through and amd64 still got built and pushed. Now it would abort before reaching it. Ordering native-first keeps the loud failure and removes that regression: whatever breaks under emulation, the image the build actually depends on is already published. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every build path pulled ghcr.io/nyamisty/altserver_builder_alpine_* -- the four workflow matrix entries, build_image.yml's BUILDER arg, docker/Dockerfile's ARG default, the stack's commented build block, and README's docker run example. For a project whose premise is that upstream went quiet, depending on that account's packages surviving was the wrong foundation: if they were deleted, every build here would stop, and until today nobody could rebuild them (issue NyaMisty#111). Both halves of that are now resolved. NyaMisty#111 is closed, and the buildenv workflow published a full set under ghcr.io/ben-diehlci -- all four public and anonymously pullable. They are built from THIS repo's buildenv/Dockerfile, so they carry the corecrypto fixes NyaMisty's 2022-era images predate. Order mattered and was deliberate: the images were published and verified BEFORE anything was repointed. Repointing first would have left every consumer pulling images that did not exist. Verified after the switch: the static binary builds clean against the new aarch64 image and contains the sockaddr patch, and the full container image builds all 19 steps, ships a working AltServer --help, and carries python3, netmuxd and the entrypoint. Binary is ~68KB smaller than the one NyaMisty's toolchain produces, which is expected from a toolchain rebuilt with the corecrypto fixes. REVIVAL.md keeps its historical mentions of the old images but two claims there were corrected, having just been disproved: that nobody could rebuild them, and this morning's note that every consumer still pointed at them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Asked whether the hygiene script should run on a timer. It should not: most of what it checks is static, so a daily run would re-report the same thing until nobody reads it, and --clean-strays does rm -rf inside the containers, which on a schedule could delete an in-progress install's working directory. It is an audit tool. The thing that actually needed automating was the log, and the right fix is to stop the credentials arriving rather than to sweep them up afterwards. docker/redact-log.py filters AltServer's stdout and stderr. It imports _redact from installer.py rather than reimplementing the rules: two copies of a security filter drift, and the unwatched one rots. tests/check_redaction.py now exercises the shipped script as a subprocess and fails if a secret survives it, if a progress line is dropped, or if it stops importing from installer.py -- all three mutation-tested. It fails open at every level. Missing installer.py, a line that makes _redact raise, or an unexpected error at the top level each degrade to passing output through unchanged, because losing the logs would cost more than the credential already sitting in them. The entrypoint uses process substitution rather than a pipeline. `AltServer | filter` would make AltServer a child of a subshell, so it would no longer be tini's direct child, would not receive SIGTERM on docker stop, and its exit status would not be the container's. Redirecting first and then exec'ing keeps all three. Verified with the same construction: exit status 42 propagates intact, the exec'd process keeps its own pid, stderr is merged and filtered. Log rotation added to all four services as a floor -- Docker's json-file default is unbounded, and these grow with every refresh. 10MB x 3 caps it without a cron job or anything racing a running install. Rotation alone would still leave 30MB of credentials on disk, which is why it is the floor and not the fix. Also fixed a real false negative in credential-hygiene.sh, found by running it: /var/lib/docker/containers is mode 0710 root:root, so the unprivileged `[ -e ]` returned false and it reported "no json-file log on disk" for every container on a host whose logs were full of credentials. The check now runs under sudo. Verified in the built image: the installed filter masks MachineID and the device UDID while leaving progress lines byte-identical, and the container starts and logs normally through the new entrypoint. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
docker/redact-log.py masks the VALUE and keeps the LABEL, so a successfully filtered line still reads 'MachineID : [withheld]'. The marker grep matched the word MachineID and counted it, reporting '1 line carrying credentials' for a log that was doing exactly what it should. Hits now exclude lines containing [withheld], and a log with redacted lines and no unredacted markers is reported as clean, naming how many the filter caught -- which is the more useful signal anyway: it confirms the filter is running, rather than leaving 'no credential markers found' ambiguous between working and idle. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…the file
Switching to the narrower altserver-mdns profile meant hand-editing two
security_opt lines, which a git-backed Portainer stack overwrites on the next
pull. It is now ${ALTSERVER_APPARMOR:-unconfined}, set once in Portainer's stack
environment and preserved across redeploys.
The default stays unconfined deliberately. A container that asks for a profile
the host kernel has not loaded FAILS TO START, so hardcoding altserver-mdns would
break deploying this repo unchanged on any host that has not run
deploy/apparmor/install.sh first -- a hostile default for a project whose quick
start promises no host preparation.
Verified both branches resolve as intended and the file still parses with the
placeholder in place.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… count
install.sh checked its own work with `aa-status --profiled | grep -qx altserver-mdns`.
That flag prints the NUMBER of loaded profiles, not their names, so the grep was
matching a profile name against an integer and could never succeed. It reported
WARNING: altserver-mdns did not appear in aa-status
for a profile that had loaded correctly -- and because the script exits non-zero
there, it told the operator to stop at exactly the point where continuing was
safe. A verification step that cannot pass is the same class of bug as the
reachability check that used a USB-only flag.
It now reads /sys/kernel/security/apparmor/profiles, which is the kernel's own
list, and falls back to parsing plain aa-status output. The failure message also
now says what not to do next: setting ALTSERVER_APPARMOR before the profile is
loaded gives a container that will not start.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…entity check_redaction.py's constants were copied verbatim from a real install log: the live X-Apple-I-MD-M machine identity, X-Apple-I-MD-LU local user id, and X-Mme-Device-Id. The file's own docstring said 'Shapes are real; digits are scrambled'. They were never scrambled. A test written to prove credentials do not leak was itself publishing them. Every fixture now contains the literal string EXAMPLE, so no future reader can mistake one for a captured value, and the comment says plainly not to paste a real value here again -- not even a partial one, since the docstring also quoted a truncated MachineID. The one-time password rotates per request and the committed one is long dead. The machine identity and local user id do not rotate: they are the stable identity this server presents to Apple, and REVIVAL.md records shared anisette identity as the mechanism behind the issue NyaMisty#88 Apple ID lockouts. Removing them here does not remove them from history -- 17 commits still carry them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The install page reported 'no UDID found' for a phone that was paired, online and refreshing over Wi-Fi. /install prefills the UDID from /api/pairing, and pairing.py looked the device up with `idevice_id -l` -- USB only, since tools/idevice_id.c's case 'l' sets include_usb and nothing else. On a cable-free server that returns an empty list, so the page demanded a cable that is not needed and cannot help. This is the third instance of the same flag trap, after the reachability check and idevicepair validate. The pattern: -l and -n are not verbosity switches, they select which transports are enumerated, and a wireless-only deployment is invisible to -l by construction. The lookup now queries USB and network separately and merges them. A device found only over the network is reported as 'iPhone detected over Wi-Fi' with a note that no cable is needed, rather than falling through to the plug-it-in step -- the pairing wizard should not demand a cable from someone whose phone is already paired. Validation follows the same transport, since idevicepair.c:372 selects IDEVICE_LOOKUP_USBMUX unless -n is passed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Confirming a refresh worked has meant SSHing in and running `docker logs -f altserver`, because that is the only thing that can actually tell you: both `idevice_id` and the wireless status row use Debian's libimobiledevice rather than the vendored copy AltServer links, and were green throughout a bug that broke every wireless refresh. The panel reads a file on the altserver-data volume, which both containers already mount at /data. docker/redact-log.py gained --tee and writes there as well as to stdout, so the UI needs neither the Docker socket -- root-on-host, for a service that takes an Apple ID password over plain HTTP -- nor a bind mount of /var/lib/docker. Everything in that file has been through _redact, so the view is safe by construction rather than by a second filter that could drift. The tee is capped at 1MB, keeping the most recent 500KB. Nothing rotates a file written for the life of a deployment, and unbounded growth is what the docker logs were doing before. Polling runs only while watching is on, and stops itself after five minutes: a refresh takes seconds, so a longer session means the tab was left open, and an abandoned tab polling every two seconds is a self-inflicted load on a box whose job is to sit quietly. check_page_js caught a real bug while writing this. PAGE is a Python string, so a '\n' inside the embedded JS is interpreted by Python into a REAL newline, which ends the string literal and kills the entire script block -- the same failure that guard was written for. Fixed the way the rest of this file already does it, with String.fromCharCode(10). Verified in a browser: the toggle starts and stops, the log renders, the state line reports the line count, and a redacted MachineID shows as [withheld]. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Switching to the narrow profile made both mDNS status rows report 'avahi-browse timed out after 15s' while refresh itself kept working and the server stayed discoverable from another machine. dmesg named it exactly: apparmor="DENIED" operation="dbus_signal" interface="org.freedesktop.Avahi.ServiceBrowser" member="ItemNew" name=":1.3" mask="receive" label="altserver-mdns" A D-Bus signal carries the sender's UNIQUE connection name, not its well-known one. peer=(name=org.freedesktop.Avahi) matches method calls and their replies -- which is why publishing was unaffected -- but never a signal from :1.3. So avahi-browse subscribed, got neither ItemNew nor CacheExhausted, and waited for its timeout. Matched on the interface instead, which does not change with avahi's unique name. The ptrace denials in the same output are left alone on purpose, and the profile now says so. altserver-web runs with pid: host, so pgrep -af AltServer walks every process on the box; AltServer is under this profile and readable, and the denials are for unrelated host and docker-default processes. Widening to peer=unconfined would let this container read other services' /proc/PID/environ. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The dashboard was as slow as the SUM of its checks. Two of them shell out to avahi-browse with a 15s timeout, so once the AppArmor profile started denying avahi's D-Bus signals the page took over half a minute to show anything: the checks were correctly reporting a problem, and the page was unusable while they did it. A status page that becomes unreadable exactly when something is wrong is the wrong shape for the one tool you reach for when something is wrong. They are subprocess and HTTP calls, so a thread pool is the right tool. The page is now as slow as its SLOWEST check. check_clock is the only genuine dependency, needing the timestamp check_anisette collects, so anisette runs first and the remaining five run together. Output order is preserved by index rather than by completion, so the page does not reshuffle between polls. Each result is also collected defensively: a check that raises now becomes an UNKNOWN row naming the exception, rather than blanking the dashboard. Measured with two slow checks stubbed in: 3.0s against the 6.0s the serial version would have taken, six checks returned, order unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Found by scanning for SHAPES rather than for values already known to be a problem. Every previous sweep grepped for specific strings -- the IPs, the username, the anisette identity -- so it could only find what had already been identified. That is why the test fixtures survived several 'clean' reports: a scan keyed to known values cannot find an unknown one. Scanning for classes instead (UUIDs, long hex and base64 runs, emails, private IPs, home paths, Apple serial and team-id shapes) surfaced two more: tests/check_redaction.py had a real provisioning profile UUID, lifted from the same log paste as the anisette fixtures. Stale -- profiles are replaced on every refresh, and that one was superseded the same day -- but real. Now synthetic, and deliberately distinct from DEVICE_ID, which is in SECRETS: the first replacement collided with it and made a must-be-unchanged line contain a must-never-appear value, which the guard caught. docs/REVIVAL.md recorded the phone's mDNS name. Generic for a device named 'iPhone', but genericised anyway. Everything else the scan surfaced is upstream: a GUID constant in AltServerApp.cpp, an example IP in PhoneHelper.c, two Arch PKGBUILD maintainer addresses, four PKGBUILD source checksums, and the digest-pinned anisette image. The scan method itself needed fixing first. git grep's ERE handles \b inconsistently, so the first pass reported 'clean' for UUIDs while git grep -lF found the same value in 22 commits. A validation step against a string known to be present is what caught it -- the same lesson as every other check in this log. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reviewed for two audiences. The technical half was fine -- build, rewriters, env vars and reference are all there. The other half was not. A newcomer's first two screens were 'AltServer for AltStore, but on-device' followed immediately by a table whose second row is 'Apple's 2026 GSA client-info block'. That is maintainer-facing: it says what this fork FIXED, which matters if you arrived from an issue thread and is noise if you just want it running. It now sits behind a 'Fork status' heading that says so, and says it is safe to skip. In its place: what the thing actually does, for someone who does not already know what AltStore or a 7-day certificate is; a What you need table (Linux host with Docker, an iPhone, a cable once, an Apple ID, ~30 minutes); an explicit What it will not do; and a jump table so each audience can leave the intro early. Also restored the prerequisites summary lost when BOOTSTRAP.md was folded in -- there was nothing telling a reader what hardware or accounts to have ready -- fixed four 'Phase N' references left from BOOTSTRAP's numbering, and promoted The web UI out of the numbered steps, where as an h3 after step 7 it read as step 8. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This is a fork that got AltServer-Linux working again end to end, offered back in
case it's useful. It's large — 95 commits — so it's probably not a
merge-as-is. Everything below is independent, and I'm happy to split any piece
into its own small PR; say which and I'll open them.
Verified against live Apple infrastructure and a real iPhone on iOS 26, not just
compiled.
The one that matters most: wireless refresh (#77)
libraries/libimobiledeviceis pinned atc6f89dea, which reads a usbmux networkaddress as a BSD sockaddr — byte 0 as
sa_len, byte 1 as the family. netmuxdemits the Linux layout on Linux (
devices.rsgates the BSD form ontarget_os = "macos"), so byte 1 is0x00:Byte 1 matches neither
AF_INET(0x02) norAF_INET6(0x1E), soidevice_connectreturnsIDEVICE_E_UNKNOWN_ERRORand AltStore reports "Therewas an error connecting to the device." Byte 0 is also read as a length, giving
malloc(2), a truncated address, and then a 14–26 byte read out of a 2-byte heapbuffer.
Three sites are affected, and the one on AltServer's actual path is
idevice_from_mux_device— reached viaidevice_new_with_options— not thedevice-list site. A fix covering only the obvious two leaves
malloc(2)where itmatters.
Both layouts put the port at offset 2 and the address at offset 4, so only the
family test and the copy length need to change, and the two forms are
distinguishable without ambiguity. Implemented as a build-time rewriter
(
makefiles/libimobiledevice-build/rewrite_idevice_source.py) rather than asubmodule edit, matching the existing AltSign/ldid convention.
Other fixes
com.apple.dt.XcodeinX-MMe-Client-Infonow 503s; rewritten at the single point anisette data enters429docs/gsa-connection-probe.sh)DNSServiceRegisterreturning 0 is not proof of publication; failures are now detected and reportedps-pis world-readable;ALTSERVER_*env vars added as the preferred pathUSBMUXD_SOCKET_ADRESS(#49)Rewriter guards
rewrite_altserver_source.pyhad noraise,assertorsys.exit, so asubstitution whose pattern stopped matching after an
upstream_repobump wouldproduce a quietly wrong binary — it's the rewriter that strips the Win32 GUI and
splices in the console
Authenticate/ShowAlert/Start. All four rewritersnow fail the build loudly. Output verified byte-identical across all 35 files,
with nine deliberate regressions each caught.
Also fixed: a concurrency bug where
libimobiledevice.makran twice under-jbecause
Makefile:24was a multi-target.PHONYrule, compiling every objecttwice to the same path.
Please drop these if you merge
They're specific to running this as a deployment and aren't upstream's business:
deploy/— compose stacks and an AppArmor profiledocs/REVIVAL.md— a working log, deliberately longweb/— a setup UI (2FA entry in the browser, since the code is read fromstd::cin)ghcr.io/ben-diehlci— that's this fork's namespacebuildenv/build_docker.shis worth taking though: it hardcodedghcr.io/nyamisty, so everydocker pushwas denied in any fork. It now derivesthe namespace from
GITHUB_REPOSITORY_OWNER, which works unchanged for you.Thanks
This only exists because the Linux port did. Fork:
https://github.com/Ben-Diehlci/altserver-linux
🤖 Generated with Claude Code