invalidate_on_error, avb_version, hash_alg, and size are sibling
props of vbmeta.device_state. When device_state is present but
its complements are missing, the partial set is itself a
detection signal. Fill with safe defaults if absent; never
overwrite when present.
Creating a vendor-specific boot prop on a device that never had
one is itself a detection signal. Existence-guard the four
boot-lock targets so absent props stay absent.
Live install on the user's Pixel surfaced a fourth bootloader-lock
prop the source plan never enumerated. The Chunqiu-style detector
card listed three indicators on its bootloader-unlock row and
flagged red because ro.boot.vbmeta.device_state remained unlocked
even after ro.boot.verifiedbootstate, ro.boot.flash.locked, and
ro.boot.veritymode were all spoofed.
Add ro.boot.vbmeta.device_state=locked to BootStateManager.targets.
Verified post-reboot on device: all four props now report the
spoofed values.
The source plan's bulletin-history schema declared a four-value
status enum: success, network_error, parse_error,
validation_rejected. The poller only ever wrote the first three;
when PatchLevelManager.updateTo silently rejected a date for bad
format, floor violation, past/future bounds, or atomicWrite IO
error, the history still recorded status=success and applied=true
because applied was set from isNewer before updateTo ran.
Make updateTo return Boolean. Wire the result through fetchAndParse
so a rejected apply lands as status=validation_rejected with
applied=false and an error string identifying the date that failed.
Closes the only spec gap from fancy-humming-firefly.md uncovered
during the source-plan cross-audit.
currentPatch returned the raw system= value unchanged. A
malformed value such as system=tomorrow flowed into the
lexicographic comparison `date > current`, where any well-formed
YYYY-MM-DD from the bulletin sorts before lowercase letters, so
the poller permanently judged its date "not newer" and never
applied an update. Validate the read value against the date
pattern; on mismatch, log a warning and treat as passive.
The 60-day window covers Pixel monthly bulletin cadence plus
pre-announcement slip but rejects far-future hostile inputs. The
prior bare constant left readers wondering whether the value was
arbitrary; the KDoc closes that loop.
applyToProps reaches Runtime.exec("resetprop", name, value) twice
per call, once for system and once for vendor. Boot-time
initialize, BulletinPoller's handler thread, and the PifObserver
inotify thread can all reach applyToProps independently. Two
concurrent invocations for different dates could interleave such
that system and vendor end up with mismatched values. Synchronize
on the singleton so each apply runs to completion before the
next begins.
A read failure (partial-write race during user edit, SELinux
denial, or any other IOException) used to fall through the
runCatching and rewrite the file with only the global block,
silently destroying every existing [pkg] override. Drop the
runCatching so the failure bubbles to atomicWrite, where the M3
guard in updateTo logs and returns without applyToProps, leaving
both file and props untouched.
isGlobalKeyAssignment treated any bare line whose first token
matched system/boot/vendor/all as a global assignment and
stripped it. A user line of literally "all" or "system" written
without a value (malformed config but reachable) thus got eaten
on the next atomicWrite. Require '=' in the trimmed line before
treating it as a key=value assignment.
parsePatchLevelValue silently synthesized day=01 when input was
6 chars (YYYY-MM) and caller wanted 8-digit YYYYMMDD. If
ro.vendor.build.security_patch ever returns YYYY-MM on a target
device (older Samsung firmware does), the synthesized day
disagrees with the real bulletin day -- a detection fingerprint.
Return null so getRealDevicePatchLevelInt falls through to
Build.VERSION.SECURITY_PATCH, which is always YYYY-MM-DD.
Revert 29b2a85. AOSP 15 KeyMint reference TA at
system/keymint/common/src/tag/info.rs:61-89 lists both Tag::EcCurve
and Tag::KeySize in KEYMINT_ENFORCED_CHARACTERISTICS, and
check_ec_params at common/src/tag.rs:632 says "Key size is not
needed, but if present should match the curve" -- the TA passes
through whatever the caller supplies and keystore2 supplies both
for EC keys per KeyMintBenchmark.cpp:234,259. Omitting KEY_SIZE
made the simulator's characteristics list shorter than real
hardware, a detection fingerprint.
PatchLevelManager.initialize ran once at boot; PIF edits required
a reboot to take effect. Add a FileObserver on
/data/adb/modules/playintegrityfix for CLOSE_WRITE, MOVED_TO, and
DELETE on the four known PIF filenames, re-resolving and applying
the new date when any of them changes. Skip the watch when the
PIF dir is absent so the daemon does not start a stale inotify
node before the module is even installed.
resolvePifPatch selected the last existing file regardless of
size. A 0-byte file picked up by lastOrNull caused JSONObject("")
to throw, the catch silently fell back to SystemProperties, and a
preceding non-empty PIF was ignored. Filter out zero-length files
so the lookup walks past them to the next valid candidate.
writeText and Files.move can throw IOException, SecurityException,
or AtomicMoveNotSupportedException. The exception previously
propagated through updateTo into BulletinPoller.fetchAndParse's
broad catch, which mislabelled it as "network_error" in the
history. Wrap atomicWrite, log the real failure, and return
before resetprop so the on-disk file and live props stay
consistent on failure.
atomicWrite previously overwrote the entire security_patch.txt
with only the three global lines, destroying the per-package
[pkg] overrides supported by ConfigurationManager. Read the
existing file, strip only global system/boot/vendor/all key
assignments, prepend the refreshed global block, and append
everything else (comments, blanks, all [pkg] sections) verbatim.
PatchLevelManager.initialize previously called updateTo, which
overwrote security_patch.txt with explicit dates and destroyed
the Phase 1 default of system=prop. Split prop application into
a new private applyToProps so initialize only resetprops; never
writes the file. BulletinPoller now treats currentPatch() == null
(the signal for system=prop or missing/blank) as passive and
skips updateTo. The file becomes user-owned config; props track
PIF or the device default.
BootStateManager.apply and PatchLevelManager.initialize ran after
initializeInterceptors, so keystore2 cached ro.boot.* and
ro.build.version.security_patch from the un-spoofed values during
hook init. Move both before the interceptor so the hook sees the
spoofed snapshot. ConfigurationManager stays between them since
it only loads files and is independent of prop state.
BulletinPoller.start ran inside App.main's outer try{...} catch
that rethrows, so any HandlerThread or Looper init failure killed
the daemon including keystore interception. Wrap the start call in
its own try so a poller failure logs and falls through, leaving
the rest of the pipeline alive.
fetchAndParse and appendHistory each catch their own exceptions,
but scheduleNext can throw IllegalStateException if the Looper is
torn down or any helper raises an unanticipated error. Without an
outer catch, the reschedule chain broke and the poller stayed dead
until reboot. Wrap the entire body so a thrown exception still
attempts to schedule the next poll.
HttpsURLConnection resolves bulletin.source via getaddrinfo, which
uses UDP/53 first. Without UDP socket rules the resolver fails
before TCP even attempts, killing BulletinPoller silently on
enforcing SELinux kernels. Mirror the existing TCP rules onto UDP
for ksu and magisk.
PatchLevelManager only rejected dates more than ~1 year in the
past. A MITM serving <td>2099-12-31</td> from a spoofed bulletin
response slipped through validation and got written to
security_patch.txt plus resetprop'd. Add a 60-day upper bound past
today using LocalDate.plusDays so month boundaries are handled
correctly. The existing past bound stays.
Gradle's buildRustCertgen task uses commandLine("cargo", ...), which
ProcessBuilder resolves against the daemon's inherited PATH rather
than the env injected via Exec.environment(). Non-login shells (CI,
IDE-spawned terminals, fresh tmux panes) don't source the profile.d
hook that prepends ~/.cargo/bin, so the daemon dies with
"A problem occurred starting process 'command 'cargo''" even when
rustup is installed. Prepending ~/.cargo/bin at script entry makes
the script self-contained regardless of how the shell was launched.
BulletinPoller fetches the Pixel security bulletin index page on
its own HandlerThread with 5s/30s/2m/10m/30m bootstrap backoff,
then 24h steady cadence. The first <td>YYYY-MM-DD</td> match is
the latest published patch; newer-than-current dates flow through
PatchLevelManager.updateTo for validation + atomic write + resetprop.
Persists the last 10 attempts to last_bulletin_fetch.json (atomic
rename) with status, http_code, parsed_date, applied, and error
fields so operators can audit history without logcat.
Sepolicy rule appends TCP-socket allow rules for both ksu and
magisk source domains so HttpsURLConnection survives SELinux
enforcement on either root provider. Uninstall.sh cleans the
three new artifacts.
PatchLevelManager resolves the active security patch from
PlayIntegrityFix via the same six-path override chain as
Tricky-Addon's get_extra.sh (pif.json/pif.prop/custom.pif.*,
later entries override earlier ones). Falls back to live
ro.build.version.security_patch when no PIF source is present.
updateTo() validates YYYY-MM-DD format, rejects dates below
2020-01-01 or more than one year older than today, then atomically
stages security_patch.txt with explicit system/boot/vendor dates
and resetprops ro.build.version.security_patch plus
ro.vendor.build.security_patch. Cert tags 706/718/719 then encode
consistent dates via AndroidDeviceUtils.parsePatchLevelValue
(YYYYMM for OS, YYYYMMDD for VENDOR/BOOT per AOSP Tag.aidl).
Wired from App.main after BootStateManager.apply().
BootStateManager.apply() runs from App.main after ConfigurationManager
init and sets ro.boot.verifiedbootstate=green, ro.boot.flash.locked=1,
ro.boot.veritymode=enforcing via resetprop so the attestation
extension's hardcoded verifiedBootState=Verified agrees with what
detectors observe via getprop.
Adds an internal AndroidDeviceUtils.setProperty(name, value: String)
overload so the existing private ByteArray variant stays exclusive
to vbmeta digest persistence while config-package callers can set
plain string props without hex encoding.
Closes documented vulnerability D44 (countermeasure-matrix.md).
Out-of-box install seeds /data/adb/tricky_store/security_patch.txt
with system=prop so TEESimulator passively mirrors live device props.
ConfigurationManager auto-forces boot=prop+vendor=prop when system=prop
(ConfigurationManager.kt:253-256), giving full coverage with one line.
Eliminates Chunqiu code 26 (Tampered Attestation Key) on out-of-box
installs without requiring the Tricky-Addon-Update-Target-List
companion module.
Previously module/update.json had to be hand-bumped to keep
versionCode and zipUrl in lockstep with module.prop's expanded
$gitCommitCount. Wire a refreshUpdateJson task to the
prepareModuleFiles${variant} pipeline so every zipDebug/zipRelease
regenerates the file from current verName and gitCommitCount.
Gradle's exec environment does not inherit the user's interactive
shell PATH, so cargo-ndk could not find cargo even when it lived in
~/.cargo/bin. Prepend ~/.cargo/bin to PATH for buildRustCertgen so
the Rust toolchain resolves reliably from any shell.
Java sourceCompatibility/targetCompatibility were already 21, but
the Kotlin compiler defaulted to JVM 17 bytecode, producing a
toolchain skew warning on every build. Align the Kotlin target to
match the Java target.
AOSP keystore2 attestation lists KEY_SIZE only when there is no
authoritative key-shape tag. For EC keys the curve already pins the
key size, so emitting both KEY_SIZE and EC_CURVE is a forgery
fingerprint. Guard the createAuth call accordingly.
Regenerated keys were being filtered as deleted because the
deletion marker in Keystore2Interceptor.deletedSoftwareKeys
survived past the regen call. Clear the marker at both software
and TEE generation paths so the next getKeyEntry returns the
fresh key instead of NOT_FOUND.
Instrumentation-only. Logs entry (primitive class, callingUid, input size)
and throwable propagation (class, SSE error code, message, stack top) in
both SoftwareOperation.updateAad and SoftwareOperationBinder.updateAad.
Intended to distinguish F1 hypotheses H1 (binder swallows SSE) vs H3
(AIDL signature drift) once duck's probe key is routed through our
simulator instead of the real TEE.
Duck Detector's 'TEE Simulator generate-mode fingerprint' probe scans the
generateKey reply parcel for a 16-byte marker where the securityLevel byte
is 0x00 (SOFTWARE). Real KeyMint HAL uses 0x64 (KEYSTORE=100) for
keystore-enforced metadata (creation time, user ID, etc.).
This single-line change aligns with real hardware behavior and defeats
the probe. Tested on OnePlus 13 (Android 16, KSU 3.2.4):
- Before: 'TEE Simulator generate-mode fingerprint: Matched' (score 50)
- After: 'No TEE Simulator generate-mode fingerprint observed' (score 4)
Reference: https://github.com/eltavine/Duck-Detector-Refactoring/commit/e368038
The release job now uploads assets with stable names
(TEESimulator-RS-Release.zip) alongside versioned ones, so the
/latest/download/ URL in update.json always resolves. versionCode
in update.json is bumped to the commit count automatically after
each release, committed with [skip ci] to prevent loops.
v6.0 changed bare target.txt entries from AUTO to GENERATE, breaking
apps like BHIM that need TEE-backed attestation keys. Restore AUTO as
default and resolve it at config level (PATCH if TEE works, GENERATE
if not) to bypass the non-deterministic raceTeePatch path.
AOSP ta/src/keys.rs:451-478 requires self-signed leaf (depth 1) when
no attestation challenge is provided. Both Kotlin and Rust paths now
return subject==issuer, signed by generated key, no attestation
extension. Adds cert chain trace logging in debug builds.
createOperation was building effectiveParams from key-generation params
but dropping operation-time fields (nonce, blockMode, padding,
minMacLength). This caused GCM decrypt to fail with
"IV must be specified in GCM mode" since the nonce from the begin call
never reached CipherPrimitive.
Also adds nonce field to KeyMintAttestation and handles GCM/CBC/CTR IV
initialization in CipherPrimitive.
AOSP KeyMint only includes the attestation extension (OID
1.3.6.1.4.1.11129.2.1.17) when ATTESTATION_CHALLENGE is present.
Without a challenge, generateKey produces a plain self-signed cert.
Our code unconditionally added the extension, which behavioral
probes detect by generating a key without a challenge and checking
for the OID.
Fixes both the Rust native-certgen and BouncyCastle paths.
Also skips AAID computation when no challenge is provided,
matching keystore2 security_level.rs:457 behavior.
The retained cert chain was applied to response metadata but the
authorizations array was left unpatched, allowing a detector to compare
metadata patch levels against cert attestation values and spot the
divergence. Refs upstream JingMatrix #164.
PATCH and AUTO modes inherit the real TEE's attestation quirks (epoch 0
cert dates, version mismatch, missing USAGE_COUNT_LIMIT) which can't be
fixed in post-patch. GENERATE mode builds attestation from scratch with
full control over every field. Users who want real TEE key generation
can still use the ? suffix for explicit PATCH mode.
Under binder stress, debug builds hammered logd with 6-7 syscalls per
keygen, causing thread contention that spiked ping latency past G10b's
threshold. Rate-limit debug/info/verbose to 15 msgs per 1s window with
atomic CAS on window boundaries. Warnings and errors always pass.
Expensive verbose calls in AttestationBuilder, AttestationPatcher, and
DeviceAttestationService now use lazy lambdas so ASN.1 formatting only
runs when the message will actually be emitted.
Strip LOGV from the buffer parse loop and add a fast pre-check that
peeks at the first binder command before entering the full parser.
Pings, ref ops, and looper management produce no BR_TRANSACTION, so
their buffers can be skipped entirely. Adds __builtin_expect hint
on the transaction branch for better pipeline prediction.
Drops G2 binder ping ratio from 3.95x to 1.17x in debug builds.