From d8b31739e7902915310142d5bb30f472d990f75b Mon Sep 17 00:00:00 2001 From: SatoshiQaziMuhammed Date: Mon, 3 Aug 2026 07:25:46 -0700 Subject: [PATCH] ci: fail the build when a 64-bit native library is not 16 KB aligned (#20) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ci: fail the build when a 64-bit native library is not 16 KB aligned Play rejects updates whose 64-bit native libraries are not aligned for 16 KB memory pages, but nothing in the build says so. A misaligned .so surfaces only as a policy issue in Play Console, days after a release has gone out โ€” which is how one reached production here unnoticed. The check reads what the build actually produced rather than trusting the dependency list, because a single transitive dependency shipping an unaligned library is enough to fail the whole bundle. It walks every .aab and .apk under app/build/outputs, pulls each arm64-v8a and x86_64 .so, and compares its largest LOAD segment alignment against 16384. 32-bit ABIs are skipped: those devices use 4 KB pages. Verified against the v1.2.0 release bundle before it was cleaned up: 14 libraries scanned, and it caught libpl_droidsonroids_gif.so at 0x1000 โ€” the one library Play flagged. A check that cannot fail on known-bad input is not a check. * test: make the tamper-detection test actually tamper checkTamperedDataFails wrote to encoded[lastIndex - 1] but computed the new value from encoded.last(). Whenever those two bytes already differed by exactly 0x01 the write was a no-op, the ciphertext stayed valid, decryption succeeded, and the expected AEADBadTagException never arrived โ€” so the test failed for reasons unrelated to what it checks. One run in 256. Flip a bit in the byte being written instead, which changes the ciphertext every time. --- .github/workflows/android_build.yml | 45 +++++++++++++++++++ .../ExchangeSecretsTest.kt | 8 +++- 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/.github/workflows/android_build.yml b/.github/workflows/android_build.yml index fcd1cc25..8d80983c 100644 --- a/.github/workflows/android_build.yml +++ b/.github/workflows/android_build.yml @@ -236,6 +236,51 @@ jobs: if: ${{ inputs.build-debug-tests }} run: ./gradlew assembleDebugAndroidTest + # Play rejects updates whose 64-bit native libraries are not aligned for 16 KB + # memory pages. Nothing in the build says so โ€” a misaligned .so only surfaces as a + # policy issue in Play Console, days later, after a release has already gone out. + # One dependency shipping an unaligned library is enough, so check what was + # actually produced rather than trusting the dependency list. + # + # 32-bit ABIs are exempt: those devices use 4 KB pages. + - name: ๐Ÿ” Verify 16 KB native alignment + if: ${{ !startsWith(inputs.gradlew-command, 'false') }} + run: | + set -euo pipefail + work=$(mktemp -d) + found=0 + bad=0 + + for archive in $(find app/build/outputs -name '*.aab' -o -name '*.apk' | sort); do + for entry in $(unzip -Z1 "$archive" 2>/dev/null | grep -E 'lib/(arm64-v8a|x86_64)/.*\.so$' || true); do + found=$((found + 1)) + unzip -o -q -j "$archive" "$entry" -d "$work" + lib="$work/$(basename "$entry")" + + # Largest LOAD segment alignment; 16384 (0x4000) is the requirement. + align=$(readelf -lW "$lib" 2>/dev/null | awk '/LOAD/ {print $NF}' | sort -u | tail -1) + align_dec=$((align)) + + if [ "$align_dec" -lt 16384 ]; then + echo "::error::$(basename "$archive") $entry aligned at $align, needs 0x4000 (16384)" + bad=$((bad + 1)) + else + echo " ok $entry $align" + fi + rm -f "$lib" + done + done + + rm -rf "$work" + + if [ "$found" -eq 0 ]; then + echo "No 64-bit native libraries in the build output โ€” nothing to check." + exit 0 + fi + + echo "Checked $found 64-bit native libraries, $bad misaligned." + [ "$bad" -eq 0 ] + - name: ๐Ÿงน Delete key after building if: ${{ !startsWith(inputs.keystore-file-name, 'false') }} run: rm ./app/${{ inputs.keystore-file-name }} diff --git a/feature-account-migration/src/test/java/io/novafoundation/nova/feature_account_migration/ExchangeSecretsTest.kt b/feature-account-migration/src/test/java/io/novafoundation/nova/feature_account_migration/ExchangeSecretsTest.kt index d5e8f507..c4f19258 100644 --- a/feature-account-migration/src/test/java/io/novafoundation/nova/feature_account_migration/ExchangeSecretsTest.kt +++ b/feature-account-migration/src/test/java/io/novafoundation/nova/feature_account_migration/ExchangeSecretsTest.kt @@ -68,8 +68,12 @@ class ExchangeSecretsTest { val encoded = keyExchangeUtils.encrypt("Hello".toByteArray(), peerA, peerB.public) - // Change one byte - encoded[encoded.lastIndex - 1] = (encoded.last() xor 0x01) + // Flip a bit in the byte being written, not in a different one: reading last() + // while writing lastIndex - 1 leaves the array untouched whenever those two + // bytes already differ by exactly 0x01, which is 1 run in 256. Decryption then + // succeeds, no AEADBadTagException is thrown, and a tamper-detection test + // reports a failure that has nothing to do with tamper detection. + encoded[encoded.lastIndex - 1] = (encoded[encoded.lastIndex - 1] xor 0x01) keyExchangeUtils.decrypt(encoded, peerB, peerA.public) }