FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

fix(core): getRandomValues fills the caller's typed array on iOS (#11… · NativeScript/NativeScript@9154f61 · GitHub

Commit 9154f61

Browse files
authored
fix(core): getRandomValues fills the caller's typed array on iOS (#11405)
NSMutableData never aliases bytes it is handed: with freeWhenDone:NO it copies them on creation, with freeWhenDone:YES it copies them and frees the original. Wrapping V8's backing store in one therefore filled a private copy and left the caller's array zeroed, or double-freed the allocation. Hand the typed array to SecRandomCopyBytes directly; the runtime resolves a view to its backing store at the view's byte offset, so the fill lands in the caller's own window with nothing in between.
1 parent a76525a commit 9154f61

4 files changed

Lines changed: 86 additions & 31 deletions

File tree

‎packages/core/references.d.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
/// <reference path="../types-ios/src/lib/ios/objc-x86_64/objc!CFNetwork.d.ts" />
33
/// <reference path="../types-ios/src/lib/ios/objc-x86_64/objc!CoreText.d.ts" />
44
/// <reference path="../types-ios/src/lib/ios/objc-x86_64/objc!Darwin.d.ts" />
5+
/// <reference path="../types-ios/src/lib/ios/objc-x86_64/objc!Security.d.ts" />
56
/// <reference path="../types-ios/src/lib/ios/objc-x86_64/objc!_DarwinFoundation1.d.ts" />
67
/// <reference path="../types-ios/src/lib/ios/objc-x86_64/objc!_DarwinFoundation2.d.ts" />
78
/// <reference path="../types-ios/src/lib/ios/objc-x86_64/objc!_DarwinFoundation3.d.ts" />

‎packages/core/vitest.setup.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,14 @@ global.NSData = {
5353
},
5454
};
5555
global.NSMutableData = { ...global.NSData };
56+
// Security's CSPRNG entry point, which the crypto shim feeds a typed array directly. The stub
57+
// fills it so specs can prove the bytes reach the caller's own view, and records the call.
58+
global.errSecSuccess = 0;
59+
global.kSecRandomDefault = { native: 'kSecRandomDefault' };
60+
global.SecRandomCopyBytes = (rnd: any, count: number, bytes: Uint8Array) => {
61+
bytes.fill(0xab);
62+
return 0;
63+
};
5664
global.NSCCrypto = {
5765
randomUUID() {
5866
return 'native-uuid';

‎packages/core/wgc/crypto/index.spec.ts‎

Lines changed: 69 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -30,39 +30,82 @@ describe('Crypto.getRandomValues', () => {
3030
});
3131

3232
describe('Crypto.getRandomValues (iOS)', () => {
33-
// The bytes belong to V8's BackingStore. freeWhenDone:NO is what keeps Foundation from
34-
// freeing an allocation that V8's ArrayBufferSweeper also frees.
35-
it('wraps the buffer without donating ownership to Foundation', () => {
33+
/** Runs `fn` and returns the arguments SecRandomCopyBytes received. */
34+
function captureSecRandom(fn: () => void) {
35+
const spy = vi.spyOn(globalThis as any, 'SecRandomCopyBytes');
36+
37+
try {
38+
fn();
39+
expect(spy).toHaveBeenCalledTimes(1);
40+
41+
return spy.mock.calls[0] as any[];
42+
} finally {
43+
spy.mockRestore();
44+
}
45+
}
46+
47+
// The view itself goes to Security: the runtime resolves it to V8's backing store at the
48+
// view's byte offset, so the fill lands in the caller's array with nothing in between.
49+
it('hands a byte view straight to SecRandomCopyBytes and the bytes land in it', () => {
3650
const bytes = new Uint8Array(16);
37-
const data = captureNativeCall((globalThis as any).NSCCrypto, () => crypto.getRandomValues(bytes));
51+
const [rnd, count, target] = captureSecRandom(() => crypto.getRandomValues(bytes));
3852

39-
expect(data.selector).toBe('dataWithBytesNoCopyLengthFreeWhenDone');
40-
expect(data.args[0]).toBe(bytes);
41-
expect(data.args[1]).toBe(16);
42-
expect(data.args[2]).toBe(false);
53+
expect(rnd).toBe((globalThis as any).kSecRandomDefault);
54+
expect(count).toBe(16);
55+
expect(target).toBe(bytes);
56+
expect(Array.from(bytes)).toEqual(new Array(16).fill(0xab));
4357
});
4458

45-
it('wraps only the view window of an offset byte view', () => {
46-
const view = new Uint8Array(new ArrayBuffer(32), 4, 10);
47-
const data = captureNativeCall((globalThis as any).NSCCrypto, () => crypto.getRandomValues(view));
48-
49-
expect(data.selector).toBe('dataWithBytesNoCopyLengthFreeWhenDone');
50-
expect(data.args[0]).toBe(view);
51-
expect(data.args[1]).toBe(10);
52-
expect(data.args[2]).toBe(false);
59+
it('fills only the window of an offset byte view', () => {
60+
const buffer = new ArrayBuffer(32);
61+
const view = new Uint8Array(buffer, 4, 10);
62+
const [, count, target] = captureSecRandom(() => crypto.getRandomValues(view));
63+
64+
expect(count).toBe(10);
65+
expect(target).toBe(view);
66+
const all = new Uint8Array(buffer);
67+
expect(Array.from(all.subarray(0, 4))).toEqual([0, 0, 0, 0]);
68+
expect(Array.from(all.subarray(4, 14))).toEqual(new Array(10).fill(0xab));
69+
expect(Array.from(all.subarray(14))).toEqual(new Array(18).fill(0));
5370
});
5471

55-
it('wraps only the view window of a non-byte typed array', () => {
72+
it('reinterprets a non-byte typed array over its window only', () => {
5673
const buffer = new ArrayBuffer(32);
57-
const data = captureNativeCall((globalThis as any).NSCCrypto, () => crypto.getRandomValues(new Uint32Array(buffer, 8, 2)));
58-
const wrapped = data.args[0] as Uint8Array;
59-
60-
expect(data.selector).toBe('dataWithBytesNoCopyLengthFreeWhenDone');
61-
expect(wrapped.buffer).toBe(buffer);
62-
expect(wrapped.byteOffset).toBe(8);
63-
expect(wrapped.byteLength).toBe(8);
64-
expect(data.args[1]).toBe(8);
65-
expect(data.args[2]).toBe(false);
74+
const words = new Uint32Array(buffer, 8, 2);
75+
const [, count, target] = captureSecRandom(() => crypto.getRandomValues(words));
76+
77+
expect(count).toBe(8);
78+
expect(target).toBeInstanceOf(Uint8Array);
79+
expect(target.buffer).toBe(buffer);
80+
expect(target.byteOffset).toBe(8);
81+
expect(target.byteLength).toBe(8);
82+
expect(Array.from(words)).toEqual([0xabababab, 0xabababab]);
83+
});
84+
85+
it('throws when Security reports a failure', () => {
86+
const spy = vi.spyOn(globalThis as any, 'SecRandomCopyBytes').mockReturnValue(-50);
87+
88+
try {
89+
expect(() => crypto.getRandomValues(new Uint8Array(8))).toThrow(/SecRandomCopyBytes failed \(-50\)/);
90+
} finally {
91+
spy.mockRestore();
92+
}
93+
});
94+
95+
// NSMutableData copies bytes it does not own and frees ones it does, so no NSData wrapper
96+
// can ever alias V8's memory: the shim must not route through the framework at all.
97+
it('never wraps the bytes in NSData or calls the framework', () => {
98+
const crypto_ = vi.spyOn((globalThis as any).NSCCrypto, 'getRandomValues');
99+
const mutable = vi.spyOn((globalThis as any).NSMutableData, 'dataWithBytesNoCopyLengthFreeWhenDone');
100+
101+
try {
102+
crypto.getRandomValues(new Uint8Array(8));
103+
expect(crypto_).not.toHaveBeenCalled();
104+
expect(mutable).not.toHaveBeenCalled();
105+
} finally {
106+
crypto_.mockRestore();
107+
mutable.mockRestore();
108+
}
66109
});
67110
});
68111

‎packages/core/wgc/crypto/index.ts‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,14 @@ export class Crypto {
2626
(<any>org).nativescript.winter_tc.Crypto.getRandomValues(bytes);
2727
}
2828
if (__IOS__) {
29-
// The pointer is V8-owned: freeWhenDone must stay NO, or Foundation and V8's
30-
// ArrayBufferSweeper both free the same allocation.
31-
const data = NSMutableData.dataWithBytesNoCopyLengthFreeWhenDone(bytes as never, bytes.byteLength, false);
32-
33-
NSCCrypto.getRandomValues(data);
29+
// The view goes to Security directly: the runtime hands over V8's backing store at
30+
// the view's byte offset, so the bytes land in the caller's array. No NSData may
31+
// sit in between — NSMutableData copies bytes it does not own, so a no-copy wrapper
32+
// fills a private copy, and one that owns them frees V8's allocation.
33+
const status = SecRandomCopyBytes(kSecRandomDefault, bytes.byteLength, bytes);
34+
if (status !== errSecSuccess) {
35+
throw new Error(`getRandomValues: SecRandomCopyBytes failed (${status})`);
36+
}
3437
}
3538

3639
return typedArray;

0 commit comments

Comments
 (0)

Back | FazBrowse Home | New Git URL