Skip to content
Merged
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ Node, which is how the tests run.
- `src/natives/` JS implementations of JDK native methods, by package;
`java2d*.ts` port libawt's Java2D loops and must match HotSpot's pixels;
`font*.ts` are the font natives (scaler, fontconfig, layout) over one TrueType font;
`crypto.ts` the SHA and BigInteger methods HotSpot replaces with intrinsics
`crypto.ts` the SHA, AES and BigInteger methods HotSpot replaces with intrinsics
- `src/sockets.ts` sockets (`Sock`, POSIX readiness) and the in-VM loopback backend;
`src/net-node.ts` host sockets, DNS and interfaces for Node
- `src/node.ts` Node entry, over `build/jdk` or, built as `dist/node.js`, the
Expand Down
36 changes: 35 additions & 1 deletion src/jit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,11 @@ const STRING_BUILDER = "java/lang/StringBuilder";

const LONG_MIN = -(1n << 63n), LONG_MAX = (1n << 63n) - 1n;

const VH = "java/lang/invoke/VarHandle";
const VH_AD = "java/lang/invoke/VarHandle$AccessDescriptor";
const VF = "java/lang/invoke/VarForm";
const VH_CHECK = "checkAccessModeThenIsDirect(Ljava/lang/invoke/VarHandle$AccessDescriptor;)Z";

const runtime = {
JObject, JArray, Frame, NativeFrame, JavaThrow, PendingThrow, newArrayData,
hooks,
Expand All @@ -149,6 +154,27 @@ const runtime = {
return memberTarget(t, t.vm.getField(form, "java/lang/invoke/LambdaForm", "vmentry"));
},
memberTarget,
// Fills a VarHandle call site's cache [handle, implementation] when the
// guard would link straight to the implementation: it decides from the
// handle's final and stable fields and the site's AccessDescriptor, so the
// same handle always takes the same path. Indirect handles, and exact ones
// whose type check has not passed yet, are left uncached.
vhTarget(t: JThread, cell: any[], h: JObject | null, ad: JObject) {
const vm = t.vm;
// IndirectVarHandle overrides the check and is never direct
if (h === null || h.cls.selectMethod(VH_CHECK)?.cls.name !== VH) return;
Comment thread
project516-review-bot[bot] marked this conversation as resolved.
Resolved
if (vm.getField(h, VH, "exact")) {
const exact = (vm.getField(h, VH, "methodTypeTable") as JArray | null)?.a[vm.getField(ad, VH_AD, "type")];
if (!exact || exact !== vm.getField(ad, VH_AD, "symbolicMethodTypeExact")) return;
}
const vform = vm.getField(h, VH, "vform") as JObject;
const types = vm.getField(vform, VF, "methodType_table") as JArray;
if (types.a[vm.getField(ad, VH_AD, "type")] !== vm.getField(ad, VH_AD, "symbolicMethodTypeErased")) return;
const mn = (vm.getField(vform, VF, "memberName_table") as JArray | null)?.a[vm.getField(ad, VH_AD, "mode")] as JObject | null;
if (!mn) return;
cell[1] = memberTarget(t, mn);
cell[0] = h;
},
virtualTarget(t: JThread, mn: JObject, recv: JObject | null, pc: number): JMethod {
const m = memberTarget(t, mn);
if (recv === null) npeAt(pc);
Expand Down Expand Up @@ -1196,7 +1222,15 @@ function genPolymorphic(g: Gen, pc: number, next: number, d: number, target: JMe
let dd = d;
let code = "";
if (link.m.argSlots === n + 1) { code = `s${dd}=${K(link.appendix)};`; dd++; }
return code + genInvoke(g, 0xb8, pc, next, dd, link.m, g.top.m.cls, d, false);
code += genInvoke(g, 0xb8, pc, next, dd, link.m, g.top.m.cls, d, false);
if (link.m.cls.name !== "java/lang/invoke/VarHandleGuards" || !link.appendix) return code;
// the handle seen last here: call its implementation directly
const cell = K([null, null]);
const push = target.ret === "V" ? "" : `s${base}=t.ret;`;
return `if(${args[0]}!==null&&${args[0]}===${cell}[0]){f.pc=${pc};tm=${cell}[1];` +
`if(tm.jit!==null&&tm.cls.state===${INITIALIZED}&&(cf=rt.enter(t,tm,null))!==null){` +
`r=tm.jit(t,cf${args.map((x) => "," + x).join("")});if(r!==0){t.unpool(cf);${exit(pc, base, EXIT_SUSPEND)}}${push}}` +
`else{${call(pc, d)}}}else{rt.vhTarget(t,${cell},${args[0]},${K(link.appendix)});${code}}`;
}
}
const push = target.ret === "V" ? "" : `s${base}=t.ret;`;
Expand Down
58 changes: 54 additions & 4 deletions src/natives/crypto.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// The methods HotSpot replaces with intrinsics for SHA digests and BigInteger
// arithmetic. As bytecode they run on Java longs, which are JS BigInts here,
// so a 64-bit step allocates; these do the same work on numbers, or on one
// BigInt for a whole multiplication.
// The methods HotSpot replaces with intrinsics for SHA digests, AES blocks
// and BigInteger arithmetic. As bytecode they run on Java longs, which are JS
// BigInts here, so a 64-bit step allocates; these do the same work on
// numbers, or on one BigInt for a whole multiplication.

import type { JArray, JObject } from "../runtime.ts";
import type { JThread } from "../thread.ts";
Expand All @@ -12,6 +12,7 @@ const SHA1 = "sun/security/provider/SHA";
const SHA2 = "sun/security/provider/SHA2";
const SHA5 = "sun/security/provider/SHA5";
const BI = "java/math/BigInteger";
const AES = "com/sun/crypto/provider/AESCrypt";

// the `state` field's slot in each digest class
const stateSlots = new WeakMap<JVM, Map<string, number>>();
Expand Down Expand Up @@ -213,7 +214,56 @@ function montgomery(a: bigint, b: bigint, nArr: Int32Array, len: number, inv: bi
return product;
}

// AESCrypt's round tables and the slots of its key schedule fields
interface Aes { T1: Int32Array; T2: Int32Array; T3: Int32Array; T4: Int32Array; S: Int8Array; K: number; limit: number }
const aesTables = new WeakMap<JVM, Aes>();
function aes(t: JThread, self: JObject): Aes {
let a = aesTables.get(t.vm);
if (!a) {
const vm = t.vm, c = self.cls;
const table = (name: string) => (vm.getStatic(c, name) as JArray).a as any;
a = {
T1: table("T1"), T2: table("T2"), T3: table("T3"), T4: table("T4"), S: table("S"),
K: vm.slotOf(AES, "K"), limit: vm.slotOf(AES, "limit"),
};
aesTables.set(vm, a);
}
return a;
}

// the last round's four output bytes for one column
function aesLast(out: Int8Array, o: number, S: Int8Array, a: number, b: number, c: number, d: number, k: number) {
out[o] = S[a >>> 24] ^ (k >>> 24);
out[o + 1] = S[(b >>> 16) & 255] ^ (k >>> 16);
out[o + 2] = S[(c >>> 8) & 255] ^ (k >>> 8);
out[o + 3] = S[d & 255] ^ k;
}

// AESCrypt.implEncryptBlock. Like HotSpot's intrinsic it does no bounds checks:
// its callers (FeedbackCipher.encrypt and the modes) make them.
function aesEncrypt(t: JThread, self: JObject, inp: Int8Array, i: number, out: Int8Array, o: number) {
Comment thread
project516-review-bot[bot] marked this conversation as resolved.
Resolved
const { T1, T2, T3, T4, S, K: kSlot, limit: limitSlot } = aes(t, self);
const K = (self.f[kSlot] as JArray).a as Int32Array, limit = self.f[limitSlot] as number;
let t0 = word(inp, i) ^ K[0], t1 = word(inp, i + 4) ^ K[1], t2 = word(inp, i + 8) ^ K[2], t3 = word(inp, i + 12) ^ K[3];
let k = 4;
while (k < limit) {
const a0 = T1[t0 >>> 24] ^ T2[(t1 >>> 16) & 255] ^ T3[(t2 >>> 8) & 255] ^ T4[t3 & 255] ^ K[k];
const a1 = T1[t1 >>> 24] ^ T2[(t2 >>> 16) & 255] ^ T3[(t3 >>> 8) & 255] ^ T4[t0 & 255] ^ K[k + 1];
const a2 = T1[t2 >>> 24] ^ T2[(t3 >>> 16) & 255] ^ T3[(t0 >>> 8) & 255] ^ T4[t1 & 255] ^ K[k + 2];
t3 = T1[t3 >>> 24] ^ T2[(t0 >>> 16) & 255] ^ T3[(t1 >>> 8) & 255] ^ T4[t2 & 255] ^ K[k + 3];
t0 = a0; t1 = a1; t2 = a2;
k += 4;
}
aesLast(out, o, S, t0, t1, t2, t3, K[k]);
aesLast(out, o + 4, S, t1, t2, t3, t0, K[k + 1]);
aesLast(out, o + 8, S, t2, t3, t0, t1, K[k + 2]);
aesLast(out, o + 12, S, t3, t0, t1, t2, K[k + 3]);
}

export const intrinsics: Natives = {
[`${AES}.implEncryptBlock([BI[BI)V`]: (t, [self, inp, i, out, o]) =>
aesEncrypt(t, self, (inp as JArray).a as Int8Array, i, (out as JArray).a as Int8Array, o),

[`${SHA1}.implCompress0([BI)V`]: (t, [self, buf, ofs]) =>
sha1(state(t, self, SHA1).a as Int32Array, (buf as JArray).a as Int8Array, ofs),
[`${SHA2}.implCompress0([BI)V`]: (t, [self, buf, ofs]) =>
Expand Down
68 changes: 53 additions & 15 deletions src/natives/misc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,33 +20,38 @@ export function indexScale(c: JClass): number {
return p ? WIDTH[p] : REF_SCALE;
}

// A primitive array's DataView, kept in x (unused otherwise for arrays, and
// not copied by clone).
function view(a: JArray): DataView {
const ta = a.a as Int8Array;
return new DataView(ta.buffer, ta.byteOffset, ta.byteLength);
const v = a.x as DataView | undefined;
if (v !== undefined) return v;
const d = a.a as Int8Array;
return (a.x = new DataView(d.buffer, d.byteOffset, d.byteLength));
Comment thread
project516-review-bot[bot] marked this conversation as resolved.
Resolved
}

function dvRead(dv: DataView, p: number, k: string): any {
// native order is little-endian
function dvRead(dv: DataView, p: number, k: string, le = true): any {
switch (k) {
case "Z": return dv.getInt8(p) !== 0 ? 1 : 0;
case "B": return dv.getInt8(p);
case "S": return dv.getInt16(p, true);
case "C": return dv.getUint16(p, true);
case "I": return dv.getInt32(p, true);
case "J": return dv.getBigInt64(p, true);
case "F": return dv.getFloat32(p, true);
case "D": return dv.getFloat64(p, true);
case "S": return dv.getInt16(p, le);
case "C": return dv.getUint16(p, le);
case "I": return dv.getInt32(p, le);
case "J": return dv.getBigInt64(p, le);
case "F": return dv.getFloat32(p, le);
case "D": return dv.getFloat64(p, le);
}
throw new Error(`bad kind ${k}`);
}

function dvWrite(dv: DataView, p: number, k: string, v: any) {
function dvWrite(dv: DataView, p: number, k: string, v: any, le = true) {
switch (k) {
case "Z": case "B": dv.setInt8(p, v); return;
case "S": case "C": dv.setInt16(p, v, true); return;
case "I": dv.setInt32(p, v, true); return;
case "J": dv.setBigInt64(p, v, true); return;
case "F": dv.setFloat32(p, v, true); return;
case "D": dv.setFloat64(p, v, true); return;
case "S": case "C": dv.setInt16(p, v, le); return;
case "I": dv.setInt32(p, v, le); return;
case "J": dv.setBigInt64(p, v, le); return;
case "F": dv.setFloat32(p, v, le); return;
case "D": dv.setFloat64(p, v, le); return;
}
throw new Error(`bad kind ${k}`);
}
Expand Down Expand Up @@ -135,6 +140,39 @@ export const intrinsics: Natives = {
[`${U}compareAndExchangeShort(Ljava/lang/Object;JSS)S`]: (t, [, o, off, e, v]) => cas(t, o, off, "S", e, v),
};

// Unaligned access, intrinsics in HotSpot too: one native-order read or write
// instead of the Java version's byte-by-byte assembly.
// a primitive array access that the element-indexed path cannot do
function unaligned(o: JObject | null, off: number, k: string): o is JArray {
if (!(o instanceof JArray)) return false;
const elem = o.cls.component!.prim;
return !!elem && !(sameKind(elem, k) && (off - ARRAY_BASE_OFFSET) % WIDTH[k] === 0);
Comment thread
project516-review-bot[bot] marked this conversation as resolved.
Resolved
}
// Unsafe.convEndian, through a shared scratch view: a VM runs its Java
// threads on one JS thread and this never yields, so calls cannot interleave.
const swapView = new DataView(new ArrayBuffer(8));
Comment thread
project516-review-bot[bot] marked this conversation as resolved.
Resolved
function convEndian(k: string, v: any, bigEndian: boolean): any {
if (!bigEndian) return v;
dvWrite(swapView, 0, k, v);
return dvRead(swapView, 0, k, false);
}
for (const [name, k] of [["Long", "J"], ["Int", "I"], ["Short", "S"], ["Char", "C"]]) {
const get = (t: JThread, o: JObject | null, offset: bigint, bigEndian: boolean) => {
const off = Number(offset);
if (unaligned(o, off, k)) return dvRead(view(o), off - ARRAY_BASE_OFFSET, k, !bigEndian);
return convEndian(k, unsafeGet(t, o, offset, k), bigEndian);
};
const put = (t: JThread, o: JObject | null, offset: bigint, v: any, bigEndian: boolean) => {
const off = Number(offset);
if (unaligned(o, off, k)) dvWrite(view(o), off - ARRAY_BASE_OFFSET, k, v, !bigEndian);
else unsafePut(t, o, offset, k, convEndian(k, v, bigEndian));
};
intrinsics[`${U}get${name}Unaligned(Ljava/lang/Object;J)${k}`] = (t, [, o, off]) => get(t, o, off, false);
intrinsics[`${U}get${name}Unaligned(Ljava/lang/Object;JZ)${k}`] = (t, [, o, off, be]) => get(t, o, off, be !== 0);
intrinsics[`${U}put${name}Unaligned(Ljava/lang/Object;J${k})V`] = (t, [, o, off, v]) => put(t, o, off, v, false);
intrinsics[`${U}put${name}Unaligned(Ljava/lang/Object;J${k}Z)V`] = (t, [, o, off, v, be]) => put(t, o, off, v, be !== 0);
}

function* park(t: JThread, absolute: boolean, time: bigint): NativeGen {
if (t.parkPermit) {
t.parkPermit = false;
Expand Down
24 changes: 23 additions & 1 deletion test/java/CryptoMath.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@
import java.security.SecureRandom;
import java.security.interfaces.RSAPrivateCrtKey;
import java.util.HexFormat;
import java.util.Arrays;
import javax.crypto.Cipher;
import javax.crypto.Mac;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

// SHA digests and BigInteger arithmetic, at sizes that reach their intrinsics.
// SHA digests, AES and BigInteger arithmetic, at sizes that reach their intrinsics.
public class CryptoMath {
static final HexFormat HEX = HexFormat.of();

Expand All @@ -28,6 +31,25 @@ public static void main(String[] args) throws Exception {
mac.init(new SecretKeySpec(data, 0, 40, "HmacSHA512"));
System.out.println("hmac " + HEX.formatHex(mac.doFinal(data)));

for (int keyLen : new int[] {16, 24, 32}) {
SecretKeySpec key = new SecretKeySpec(data, keyLen, keyLen, "AES");
IvParameterSpec iv = new IvParameterSpec(data, 100, 16);
for (String mode : new String[] {"AES/ECB/NoPadding", "AES/CBC/PKCS5Padding", "AES/CTR/NoPadding"}) {
Cipher c = Cipher.getInstance(mode);
Comment thread
Project516 marked this conversation as resolved.
Dismissed
c.init(Cipher.ENCRYPT_MODE, key, mode.contains("ECB") ? null : iv);
byte[] enc = c.doFinal(data, 5, 160);
c.init(Cipher.DECRYPT_MODE, key, mode.contains("ECB") ? null : iv);
boolean back = Arrays.equals(c.doFinal(enc), Arrays.copyOfRange(data, 5, 165));
System.out.println(mode + " " + keyLen + " " + HEX.formatHex(MessageDigest.getInstance("SHA-256").digest(enc), 0, 8) + " " + back);
}
// in place, in and out overlapping
Cipher c = Cipher.getInstance("AES/ECB/NoPadding");
Comment thread
Project516 marked this conversation as resolved.
Dismissed
c.init(Cipher.ENCRYPT_MODE, key);
byte[] buf = Arrays.copyOf(data, 64);
c.doFinal(buf, 0, 48, buf, 16);
System.out.println("in place " + keyLen + " " + HEX.formatHex(buf, 16, 40));
}

SecureRandom rnd = SecureRandom.getInstance("SHA1PRNG");
rnd.setSeed(42);
for (int bits : new int[] {31, 64, 100, 512, 1024, 2048, 3000, 4100}) {
Expand Down
70 changes: 70 additions & 0 deletions test/java/Unaligned.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.HexFormat;

// Multi-byte reads and writes at every alignment, in both byte orders, through
// byte array views, heap and direct buffers, and views of other element types,
// and native-order reads across the elements of other primitive arrays.
public class Unaligned {
public static void main(String[] args) {
HexFormat hex = HexFormat.of();
for (ByteOrder order : new ByteOrder[] {ByteOrder.BIG_ENDIAN, ByteOrder.LITTLE_ENDIAN}) {
VarHandle ints = MethodHandles.byteArrayViewVarHandle(int[].class, order);
VarHandle longs = MethodHandles.byteArrayViewVarHandle(long[].class, order);
VarHandle shorts = MethodHandles.byteArrayViewVarHandle(short[].class, order);
VarHandle chars = MethodHandles.byteArrayViewVarHandle(char[].class, order);
for (int at = 0; at < 8; at++) {
byte[] b = new byte[24];
ints.set(b, at, 0x81828384);
longs.set(b, at + 4, 0x0102030405060708L);
shorts.set(b, at + 12, (short) -2);
chars.set(b, at + 14, 'ꯍ');
System.out.println(order + " " + at + " " + hex.formatHex(b) + " " + Integer.toHexString((int) ints.get(b, at))
+ " " + Long.toHexString((long) longs.get(b, at + 4)) + " " + (short) shorts.get(b, at + 12)
+ " " + Integer.toHexString((char) chars.get(b, at + 14)) + " " + Long.toHexString((long) longs.get(b, at + 1)));
}
for (ByteBuffer buf : new ByteBuffer[] {ByteBuffer.allocate(32), ByteBuffer.allocateDirect(32)}) {
buf.order(order);
for (int at = 0; at < 5; at++) {
buf.putLong(at, -0x123456789abcdefL).putInt(at + 9, 0x7f00ff01).putShort(at + 14, (short) 0x8001).putChar(at + 17, 'Z')
.putDouble(at + 20, Math.PI).putFloat(at + 28 - at, 1.5f);
System.out.println((buf.isDirect() ? "direct " : "heap ") + order + " " + at + " " + Long.toHexString(buf.getLong(at))
+ " " + buf.getInt(at + 9) + " " + buf.getShort(at + 14) + " " + buf.getChar(at + 17) + " " + buf.getDouble(at + 20)
+ " " + buf.getFloat(28) + " " + buf.getInt(at + 1));
}
}
// a clone written through a view after the original was
byte[] orig = new byte[8];
ints.set(orig, 1, 0x11223344);
byte[] copy = orig.clone();
ints.set(copy, 1, 0x55667788);
System.out.println(order + " clone " + hex.formatHex(orig) + " " + hex.formatHex(copy));
// an int buffer over a byte buffer, read through a long view
ByteBuffer bb = ByteBuffer.allocate(40).order(order);
bb.asIntBuffer().put(new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10});
System.out.println(order + " view " + Long.toHexString(bb.asLongBuffer().get(2)) + " " + bb.position(3).slice().order(order).getInt(0));
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}
// Arrays.mismatch reads longs across the elements of int, char and short
// arrays, from any starting element
int[] ia = new int[40], ib = new int[40];
char[] ca = new char[70], cb = new char[70];
short[] sa = new short[70], sb = new short[70];
StringBuilder out = new StringBuilder("mismatch");
for (int diff = 0; diff < 40; diff += 3) {
ib[diff] = ia[diff] + 1;
cb[diff + 25] = 'x';
sb[diff + 25] = -1;
for (int from = 0; from < 5; from++) {
out.append(' ').append(java.util.Arrays.mismatch(ia, from, 40, ib, from, 40))
.append(',').append(java.util.Arrays.mismatch(ca, from, 70, cb, from + 1, 70))
.append(',').append(java.util.Arrays.mismatch(sa, from, 70, sb, from, 70));
}
ib[diff] = ia[diff];
cb[diff + 25] = 0;
sb[diff + 25] = 0;
}
System.out.println(out);
}
}
Loading
Loading