mirror of
https://github.com/stablyai/orca.git
synced 2026-10-02 08:02:02 +00:00
fix(mobile): validate activation metadata types
This commit is contained in:
@@ -1534,8 +1534,10 @@ copy.
|
||||
JSON scalar types. The shared chunk envelope now caps encoded data at the
|
||||
exact 65,536-character representation of 48 KiB and rejects oversized,
|
||||
type-confused, noncanonical, length-mismatched, and extra-field responses.
|
||||
Broader generated mutation, path/MIME/CSP, and persisted-cache metadata
|
||||
fuzzing remains open.
|
||||
Native activation records also require exact string-typed active/previous
|
||||
hashes; numeric hash-shaped values fail with the same stable error on iOS and
|
||||
Android. Broader generated mutation, path/MIME/CSP, and persisted-cache
|
||||
metadata fuzzing remains open.
|
||||
- [ ] Fuzz bridge envelopes, schemas, sizes, IDs, ordering, cancellation, and
|
||||
subscription lifecycle.
|
||||
- [ ] Attempt cross-host, cross-build, cross-workspace, and cross-session races.
|
||||
@@ -2553,12 +2555,14 @@ copy.
|
||||
| 2026-07-28 | Complete | Photos-revocation validation passes 568 mobile files / 3,366 tests with 2 expected skips, both typechecks and lints, changed-file formatting, 55 reliability gates, max-lines, package verification, and diff hygiene. RNW remains `9ed8c7f7d9be87c85b2431ece4eac3365a73e62bebf409846dea0ce72c9d1dde`: 49 assets, 9,280,463 raw bytes, and 2,684,481 gzip bytes. |
|
||||
| 2026-07-28 | Complete | The exact iPhone 17 Pro / iOS 26.5 Photos picker remains presented across Home/foreground. Explicit Cancel returns to the same hosted Session while private origin and opaque workspace authority remain unchanged; the journey then completes post-grant revocation, denial, terminal/page isolation, and network/navigation isolation. |
|
||||
| 2026-07-28 | Complete | Picker-interruption validation passes 568 mobile files / 3,371 tests with 2 expected skips, both typechecks and lints, changed-file formatting, 55 reliability gates, max-lines, package verification, and diff hygiene. RNW remains `9ed8c7f7d9be87c85b2431ece4eac3365a73e62bebf409846dea0ce72c9d1dde`: 49 assets, 9,280,463 raw bytes, and 2,684,481 gzip bytes. |
|
||||
| 2026-07-28 | Complete | Fresh exact-app iPhone 17 Pro / iOS 26.5 and Android API 36 arm64 Debug emulator runs loaded the active manifest-declared RNW script, rejected a mutated undeclared same-origin script path, and retained the hosted document. Both runs also passed network/navigation isolation; Android recorded zero sentinel observations and a clean bridge log. |
|
||||
| 2026-07-28 | Complete | Executable-isolation validation passes 568 mobile files / 3,373 tests with 2 expected skips and 1 focused file / 23 tests. Mobile and RNW typechecks/lints, full mobile and changed-file formatting, 55 reliability gates, max-lines, native Swift faults, 76-task Android module tests, package verification, and diff hygiene pass. RNW remains `9ed8c7f7…`: 49 assets, 9,280,463 raw bytes, and 2,684,481 gzip bytes. |
|
||||
| 2026-07-28 | Finding | Android native manifest parsing accepted quoted numeric schema, bridge, total-byte, and asset-byte fields through coercive `JSONObject.optInt`, unlike the strict shared TypeScript schema and iOS parser. The Android store now requires exact scalar types, and the same five-case deterministic corpus passes in TypeScript, Swift, and Kotlin before any stage is created. |
|
||||
| 2026-07-28 | Complete | Manifest scalar hardening passes 1 shared contract file / 20 tests, the native Swift fault executable, and the refreshed Android module suite across 76 Gradle tasks. Node typecheck, changed TypeScript lint/formatting, max-lines, and diff hygiene pass. No RNW package content changed. |
|
||||
| 2026-07-28 | Finding | Foundation bridges JSON `true` through `NSNumber`, and Swift `as? Int` accepted it as `1`; Boolean schema, bridge, total-byte, and asset-byte fields could therefore pass iOS native parsing. The store now excludes `CFBoolean` and accepts only integral JSON numbers. The mirrored five-case Boolean corpus passes in TypeScript, Swift, and Kotlin before staging. |
|
||||
| 2026-07-28 | Complete | The combined ten-case manifest scalar corpus passes 1 shared contract file / 25 tests, the native Swift fault executable, and the refreshed Android module suite across 76 Gradle tasks. Node typecheck, changed TypeScript lint/formatting, max-lines, and diff hygiene pass. No RNW package content changed. |
|
||||
| 2026-07-28 | Finding | The shared package chunk schema bounded decoded bytes but not the encoded `dataBase64` string before canonical decoding. It now enforces the exact 65,536-character ceiling for 48 KiB, and a focused corpus covers the ceiling plus quoted/Boolean numeric fields, non-Boolean EOF, unknown fields, noncanonical base64, and decoded-length mismatch. |
|
||||
| 2026-07-28 | Complete | Package-request/chunk validation passes 1 shared file / 14 tests and the unchanged downloader passes 1 mobile file / 14 tests. Existing request-path/base64/length coverage is retained alongside the new chunk corpus. Node/mobile typechecks, changed-file lint/formatting, max-lines, and diff hygiene pass. |
|
||||
| 2026-07-28 | Complete | Fresh exact-app iPhone 17 Pro / iOS 26.5 and Android API 36 arm64 Debug emulator runs loaded the active manifest-declared RNW script, rejected a mutated undeclared same-origin script path, and retained the hosted document. Both runs also passed network/navigation isolation; Android recorded zero sentinel observations and a clean bridge log. |
|
||||
| 2026-07-28 | Complete | Executable-isolation validation passes 568 mobile files / 3,373 tests with 2 expected skips and 1 focused file / 23 tests. Mobile and RNW typechecks/lints, full mobile and changed-file formatting, 55 reliability gates, max-lines, native Swift faults, 76-task Android module tests, package verification, and diff hygiene pass. RNW remains `9ed8c7f7…`: 49 assets, 9,280,463 raw bytes, and 2,684,481 gzip bytes. |
|
||||
| 2026-07-28 | Finding | Android native manifest parsing accepted quoted numeric schema, bridge, total-byte, and asset-byte fields through coercive `JSONObject.optInt`, unlike the strict shared TypeScript schema and iOS parser. The Android store now requires exact scalar types, and the same five-case deterministic corpus passes in TypeScript, Swift, and Kotlin before any stage is created. |
|
||||
| 2026-07-28 | Complete | Manifest scalar hardening passes 1 shared contract file / 20 tests, the native Swift fault executable, and the refreshed Android module suite across 76 Gradle tasks. Node typecheck, changed TypeScript lint/formatting, max-lines, and diff hygiene pass. No RNW package content changed. |
|
||||
| 2026-07-28 | Finding | Foundation bridges JSON `true` through `NSNumber`, and Swift `as? Int` accepted it as `1`; Boolean schema, bridge, total-byte, and asset-byte fields could therefore pass iOS native parsing. The store now excludes `CFBoolean` and accepts only integral JSON numbers. The mirrored five-case Boolean corpus passes in TypeScript, Swift, and Kotlin before staging. |
|
||||
| 2026-07-28 | Complete | The combined ten-case manifest scalar corpus passes 1 shared contract file / 25 tests, the native Swift fault executable, and the refreshed Android module suite across 76 Gradle tasks. Node typecheck, changed TypeScript lint/formatting, max-lines, and diff hygiene pass. No RNW package content changed. |
|
||||
| 2026-07-28 | Finding | The shared package chunk schema bounded decoded bytes but not the encoded `dataBase64` string before canonical decoding. It now enforces the exact 65,536-character ceiling for 48 KiB, and a focused corpus covers the ceiling plus quoted/Boolean numeric fields, non-Boolean EOF, unknown fields, noncanonical base64, and decoded-length mismatch. |
|
||||
| 2026-07-28 | Complete | Package-request/chunk validation passes 1 shared file / 14 tests and the unchanged downloader passes 1 mobile file / 14 tests. Existing request-path/base64/length coverage is retained alongside the new chunk corpus. Node/mobile typechecks, changed-file lint/formatting, max-lines, and diff hygiene pass. |
|
||||
| 2026-07-28 | Finding | Android activation metadata used coercive `JSONObject.optString`, so a 64-digit JSON number could become a hash-shaped active or previous build ID. Both native stores now require string-typed hashes and normalize malformed activation metadata to `mobile_web_activation_invalid`. |
|
||||
| 2026-07-28 | Complete | Mirrored numeric active/previous regressions pass the native Swift fault executable and the refreshed Android module suite across 76 Gradle tasks. Mobile formatting, max-lines, and diff hygiene pass. No RNW package content changed. |
|
||||
| 2026-07-28 | Next | Complete the remaining parity inventory and cutover cleanup, then execute the physical-device, topology, security, performance, packaged-release, and App Store gates. |
|
||||
|
||||
@@ -488,7 +488,7 @@ IDs, and unrelated provider state never enter the page result.
|
||||
| Contract | Status | Source |
|
||||
| -------------------------------- | ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Multi-asset manifest v1 | Implemented and focused-test passing | `src/shared/mobile-web/manifest-contract.ts`; TypeScript, Swift, and Kotlin reject quoted/Boolean numeric scalar confusion before staging |
|
||||
| Manifest resource bounds | Implemented, measured, and focused-test passing | 48 KiB chunks / 65,536 base64 chars, 10 MiB/asset, 32 MiB/package, 256 assets; shared, Desktop, Swift, and Kotlin limits pass; RNW has a 10 MiB CI budget |
|
||||
| Manifest resource bounds | Implemented, measured, and focused-test passing | 48 KiB chunks / 65,536 base64 chars, 10 MiB/asset, 32 MiB/package, 256 assets; shared, Desktop, Swift, and Kotlin limits pass; RNW has a 10 MiB CI budget |
|
||||
| Bridge envelope and capabilities | Implemented and focused-test passing | `src/shared/mobile-web/bridge-contract.ts`; bounded native-chat schemas and remaining operation payload schemas |
|
||||
| Terminal stream | Implemented and focused-test passing | `src/shared/mobile-web/terminal-stream-contract.ts`; broker adapter and real-stream validation remain |
|
||||
| Shell navigation events | Implemented and focused-test passing | Strict opaque routes with monotonic sequence, shell-session/build context, and one-shot restore |
|
||||
@@ -497,7 +497,7 @@ IDs, and unrelated provider state never enter the page result.
|
||||
| Bridge request/subscription caps | Implemented and focused-test passing | 640 KiB envelope, 64 pending requests, 32 subscriptions, per-operation byte/concurrency/rate grants |
|
||||
| Package read concurrency | Implemented and focused-test passing | Four concurrent 48 KiB reads / 192 KiB in flight per connection |
|
||||
| Terminal stream memory | Implemented and focused-test passing | 16 KiB input, 64 KiB output batch, 256 KiB outstanding, 2 MiB snapshot |
|
||||
| Native verified cache | Implemented and native-policy-test passing | 128 MiB per host, 512 MiB global, 16 MiB minimum free; active/session generations are protected |
|
||||
| Native verified cache | Implemented and native-policy-test passing | 128 MiB per host, 512 MiB global, 16 MiB minimum free; active/session generations are protected; activation hashes are exact-type validated |
|
||||
|
||||
## Next Inventory Action
|
||||
|
||||
|
||||
@@ -2714,8 +2714,11 @@ staging. The corpus found Android `JSONObject.optInt` string coercion and iOS
|
||||
`NSNumber`/`CFBoolean` integer bridging; both native parsers now require exact
|
||||
JSON scalar types. The shared chunk envelope also caps base64 at the exact
|
||||
65,536-character encoding of 48 KiB and rejects type confusion, noncanonical
|
||||
encoding, decoded-length mismatch, and extra fields. Generated mutation and the
|
||||
remaining path/MIME/CSP/cache corpus are still required.
|
||||
encoding, decoded-length mismatch, and extra fields. Native activation
|
||||
metadata now likewise requires string-typed active and previous hashes on both
|
||||
platforms; hash-shaped JSON numbers fail with
|
||||
`mobile_web_activation_invalid`. Generated mutation and the remaining
|
||||
path/MIME/CSP/cache corpus are still required.
|
||||
|
||||
## App Store Gate
|
||||
|
||||
|
||||
@@ -205,8 +205,10 @@ cross-scope races, privacy/authorization audit, and independent review.
|
||||
ten-case TypeScript/Swift/Kotlin quoted/Boolean numeric manifest corpus
|
||||
passes after removing Android `JSONObject.optInt` string coercion and iOS
|
||||
`NSNumber`/`CFBoolean` integer bridging. Chunk base64 is capped at 65,536
|
||||
characters and its bounded request/chunk mutation corpus passes; generated
|
||||
mutation and the other listed boundaries remain.
|
||||
characters and its bounded request/chunk mutation corpus passes. Native
|
||||
activation metadata rejects numeric active/previous hashes with the same
|
||||
stable error on both platforms; generated mutation and the other listed
|
||||
boundaries remain.
|
||||
- [ ] Attempt cross-host, cross-build, cross-workspace, cross-session, replay,
|
||||
reconnect, process-loss, and host-removal races.
|
||||
- [ ] Verify no credential or privileged host identity reaches URLs, DOM state,
|
||||
|
||||
+9
-10
@@ -419,21 +419,20 @@ internal class MobileWebPackageStore internal constructor(
|
||||
throw IllegalArgumentException("mobile_web_generation_invalid")
|
||||
}
|
||||
|
||||
private fun readActivation(hostRoot: File): Pair<String, String?> {
|
||||
private fun readActivation(hostRoot: File): Pair<String, String?> = try {
|
||||
val value = parseJsonObject(File(hostRoot, "activation.json").readText(Charsets.UTF_8))
|
||||
require(value.keys().asSequence().toSet().let { it == setOf("active") || it == setOf("active", "previous") }) {
|
||||
"mobile_web_activation_invalid"
|
||||
}
|
||||
val active = value.optString("active")
|
||||
require(value.keys().asSequence().toSet().let { it == setOf("active") || it == setOf("active", "previous") })
|
||||
val active = strictJsonString(value, "active") ?: ""
|
||||
val previous = if (value.has("previous") && !value.isNull("previous")) {
|
||||
value.optString("previous")
|
||||
strictJsonString(value, "previous")
|
||||
?: throw IllegalArgumentException("mobile_web_activation_invalid")
|
||||
} else {
|
||||
null
|
||||
}
|
||||
require(SHA256_PATTERN.matches(active) && (previous == null || SHA256_PATTERN.matches(previous))) {
|
||||
"mobile_web_activation_invalid"
|
||||
}
|
||||
return active to previous
|
||||
require(SHA256_PATTERN.matches(active) && (previous == null || SHA256_PATTERN.matches(previous)))
|
||||
active to previous
|
||||
} catch (_: Exception) {
|
||||
throw IllegalArgumentException("mobile_web_activation_invalid")
|
||||
}
|
||||
|
||||
private fun writeActivation(hostRoot: File, active: String, previous: String?) {
|
||||
|
||||
+21
@@ -212,6 +212,27 @@ class MobileWebPackageStoreTest {
|
||||
assertFalse(currentDocument.exists())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rejectsNumericActivationFields() {
|
||||
val root = temporary.newFolder()
|
||||
val store = MobileWebPackageStore(root)
|
||||
val hostRoot = File(root, sha256Hex("paired-host".toByteArray()))
|
||||
require(hostRoot.mkdirs())
|
||||
val numericHash = "1".repeat(64)
|
||||
val validHash = "a".repeat(64)
|
||||
|
||||
listOf(
|
||||
"""{"active":$numericHash}""",
|
||||
"""{"active":"$validHash","previous":$numericHash}"""
|
||||
).forEach { activation ->
|
||||
File(hostRoot, "activation.json").writeText(activation)
|
||||
val error = assertThrows(IllegalArgumentException::class.java) {
|
||||
store.openSession("paired-host", null, 1)
|
||||
}
|
||||
assertEquals("mobile_web_activation_invalid", error.message)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rejectsLowStorageBeforeCreatingAStage() {
|
||||
val root = temporary.newFolder()
|
||||
|
||||
@@ -19,6 +19,7 @@ enum MobileWebPackageStoreTests {
|
||||
try rejectsIncompleteAndCorruptGeneration(root: root.appendingPathComponent("corrupt"))
|
||||
try activatesAndRecoversPreviousGeneration(root: root.appendingPathComponent("rollback"))
|
||||
try fallsBackFromCorruptActiveGeneration(root: root.appendingPathComponent("corrupt-active"))
|
||||
try rejectsNumericActivationFields(root: root.appendingPathComponent("activation-types"))
|
||||
try rejectsLowStorage(root: root.appendingPathComponent("low-storage"))
|
||||
try evictsUnprotectedGeneration(root: root.appendingPathComponent("eviction"))
|
||||
try evictsAnotherHostForGlobalQuota(root: root.appendingPathComponent("global-eviction"))
|
||||
@@ -297,6 +298,31 @@ enum MobileWebPackageStoreTests {
|
||||
precondition(!FileManager.default.fileExists(atPath: currentDocument.path))
|
||||
}
|
||||
|
||||
private static func rejectsNumericActivationFields(root: URL) throws {
|
||||
let store = MobileWebPackageStore(cacheRoot: root)
|
||||
let hostRoot = root.appendingPathComponent(sha256Hex(Data("paired-host".utf8)))
|
||||
try FileManager.default.createDirectory(at: hostRoot, withIntermediateDirectories: true)
|
||||
let numericHash = String(repeating: "1", count: 64)
|
||||
let validHash = String(repeating: "a", count: 64)
|
||||
let invalid = [
|
||||
#"{"active":\#(numericHash)}"#,
|
||||
#"{"active":"\#(validHash)","previous":\#(numericHash)}"#,
|
||||
]
|
||||
|
||||
for activation in invalid {
|
||||
try Data(activation.utf8).write(to: hostRoot.appendingPathComponent("activation.json"))
|
||||
precondition(
|
||||
throwsCode("mobile_web_activation_invalid") {
|
||||
_ = try store.openSession(
|
||||
hostIdentity: "paired-host",
|
||||
buildId: nil,
|
||||
bridgeVersion: 1
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private static func rejectsLowStorage(root: URL) throws {
|
||||
let store = MobileWebPackageStore(
|
||||
cacheRoot: root,
|
||||
|
||||
@@ -603,13 +603,17 @@ final class MobileWebPackageStore {
|
||||
}
|
||||
|
||||
private func readActivation(hostRoot: URL) throws -> MobileWebActivationRecord {
|
||||
let data = try Data(contentsOf: hostRoot.appendingPathComponent("activation.json"))
|
||||
let activation = try JSONDecoder().decode(MobileWebActivationRecord.self, from: data)
|
||||
guard isSha256(activation.active), activation.previous == nil || isSha256(activation.previous!)
|
||||
else {
|
||||
do {
|
||||
let data = try Data(contentsOf: hostRoot.appendingPathComponent("activation.json"))
|
||||
let activation = try JSONDecoder().decode(MobileWebActivationRecord.self, from: data)
|
||||
guard isSha256(activation.active), activation.previous == nil || isSha256(activation.previous!)
|
||||
else {
|
||||
throw MobileWebStoreError("mobile_web_activation_invalid")
|
||||
}
|
||||
return activation
|
||||
} catch {
|
||||
throw MobileWebStoreError("mobile_web_activation_invalid")
|
||||
}
|
||||
return activation
|
||||
}
|
||||
|
||||
private func writeActivation(_ activation: MobileWebActivationRecord, hostRoot: URL) throws {
|
||||
|
||||
Reference in New Issue
Block a user