Skip to content

Commit 58cc119

Browse files
committed
feat: US-073 - Fix call_id overflow and accept error handling
1 parent 91d1f86 commit 58cc119

8 files changed

Lines changed: 104 additions & 57 deletions

File tree

crates/v8-runtime/src/bridge.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ pub struct AsyncBridgeFnStore {
148148
/// Stores pending promise resolvers keyed by call_id.
149149
/// Single-threaded: only accessed from the session thread.
150150
pub struct PendingPromises {
151-
map: RefCell<HashMap<u32, v8::Global<v8::PromiseResolver>>>,
151+
map: RefCell<HashMap<u64, v8::Global<v8::PromiseResolver>>>,
152152
}
153153

154154
impl PendingPromises {
@@ -159,12 +159,12 @@ impl PendingPromises {
159159
}
160160

161161
/// Store a resolver for a given call_id.
162-
pub fn insert(&self, call_id: u32, resolver: v8::Global<v8::PromiseResolver>) {
162+
pub fn insert(&self, call_id: u64, resolver: v8::Global<v8::PromiseResolver>) {
163163
self.map.borrow_mut().insert(call_id, resolver);
164164
}
165165

166166
/// Remove and return the resolver for a given call_id.
167-
pub fn remove(&self, call_id: u32) -> Option<v8::Global<v8::PromiseResolver>> {
167+
pub fn remove(&self, call_id: u64) -> Option<v8::Global<v8::PromiseResolver>> {
168168
self.map.borrow_mut().remove(&call_id)
169169
}
170170

@@ -428,7 +428,7 @@ fn serialize_v8_args_into(scope: &mut v8::HandleScope, args: &v8::FunctionCallba
428428
pub fn resolve_pending_promise(
429429
scope: &mut v8::HandleScope,
430430
pending: &PendingPromises,
431-
call_id: u32,
431+
call_id: u64,
432432
result: Option<Vec<u8>>,
433433
error: Option<String>,
434434
) -> Result<(), String> {

crates/v8-runtime/src/host_call.rs

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
use std::cell::RefCell;
44
use std::collections::{HashMap, HashSet};
55
use std::io::{Read, Write};
6-
use std::sync::atomic::{AtomicU32, Ordering};
6+
use std::sync::atomic::{AtomicU64, Ordering};
77
use std::sync::{Arc, Mutex};
88

99
use crate::ipc_binary::{self, BinaryFrame};
@@ -87,7 +87,7 @@ impl ResponseReceiver for ReaderResponseReceiver {
8787
/// Shared routing table: maps call_id → session_id for BridgeResponse routing.
8888
/// The connection handler uses this to determine which session a BridgeResponse
8989
/// belongs to (since BridgeResponse has call_id but no session_id).
90-
pub type CallIdRouter = Arc<Mutex<HashMap<u32, String>>>;
90+
pub type CallIdRouter = Arc<Mutex<HashMap<u64, String>>>;
9191

9292
/// Context for sync-blocking bridge calls from a V8 session.
9393
///
@@ -102,9 +102,9 @@ pub struct BridgeCallContext {
102102
/// Session ID included in every BridgeCall
103103
pub session_id: String,
104104
/// Monotonically increasing call_id counter
105-
next_call_id: AtomicU32,
105+
next_call_id: AtomicU64,
106106
/// Set of in-flight call_ids (for duplicate rejection)
107-
pending_calls: Mutex<HashSet<u32>>,
107+
pending_calls: Mutex<HashSet<u64>>,
108108
/// Optional routing table for call_id → session_id mapping.
109109
/// When set, call_ids are registered here so the connection handler
110110
/// can route BridgeResponse messages to the correct session.
@@ -125,7 +125,7 @@ impl BridgeCallContext {
125125
}),
126126
response_rx: Mutex::new(Box::new(ReaderResponseReceiver::new(reader))),
127127
session_id,
128-
next_call_id: AtomicU32::new(1),
128+
next_call_id: AtomicU64::new(1),
129129
pending_calls: Mutex::new(HashSet::new()),
130130
call_id_router: None,
131131
}
@@ -143,7 +143,7 @@ impl BridgeCallContext {
143143
sender,
144144
response_rx: Mutex::new(response_rx),
145145
session_id,
146-
next_call_id: AtomicU32::new(1),
146+
next_call_id: AtomicU64::new(1),
147147
pending_calls: Mutex::new(HashSet::new()),
148148
call_id_router: Some(router),
149149
}
@@ -232,7 +232,7 @@ impl BridgeCallContext {
232232
/// Send a BridgeCall without blocking for a response.
233233
/// Returns the call_id for later matching with BridgeResponse.
234234
/// Used by async promise-returning bridge functions.
235-
pub fn async_send(&self, method: &str, args: Vec<u8>) -> Result<u32, String> {
235+
pub fn async_send(&self, method: &str, args: Vec<u8>) -> Result<u64, String> {
236236
let call_id = self.next_call_id.fetch_add(1, Ordering::Relaxed);
237237

238238
// Register call_id → session_id for BridgeResponse routing
@@ -258,7 +258,7 @@ impl BridgeCallContext {
258258
}
259259

260260
/// Check if a call_id is currently pending.
261-
pub fn is_call_pending(&self, call_id: u32) -> bool {
261+
pub fn is_call_pending(&self, call_id: u64) -> bool {
262262
self.pending_calls.lock().unwrap().contains(&call_id)
263263
}
264264

@@ -288,7 +288,7 @@ mod tests {
288288

289289
/// Serialize a BridgeResponse into length-prefixed binary frame bytes
290290
fn make_response_bytes(
291-
call_id: u32,
291+
call_id: u64,
292292
result: Option<Vec<u8>>,
293293
error: Option<String>,
294294
) -> Vec<u8> {
@@ -543,7 +543,7 @@ mod tests {
543543
for j in 0..10 {
544544
let frame = BinaryFrame::BridgeCall {
545545
session_id: format!("sess-{}", i),
546-
call_id: (i * 100 + j) as u32,
546+
call_id: (i * 100 + j) as u64,
547547
method: "_fn".into(),
548548
payload: vec![],
549549
};
@@ -614,7 +614,7 @@ mod tests {
614614
}
615615

616616
// Verify all frames arrive and decode correctly
617-
for i in 0..5u32 {
617+
for i in 0..5u64 {
618618
let bytes = rx.recv().expect("recv");
619619
let decoded = ipc_binary::read_frame(&mut Cursor::new(&bytes)).expect("decode");
620620
match decoded {

crates/v8-runtime/src/ipc_binary.rs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ pub enum BinaryFrame {
6464
},
6565
BridgeResponse {
6666
session_id: String,
67-
call_id: u32,
67+
call_id: u64,
6868
status: u8, // 0 = success, 1 = error
6969
payload: Vec<u8>, // V8-serialized result OR UTF-8 error message
7070
},
@@ -83,7 +83,7 @@ pub enum BinaryFrame {
8383
// Rust → Host
8484
BridgeCall {
8585
session_id: String,
86-
call_id: u32,
86+
call_id: u64,
8787
method: String,
8888
payload: Vec<u8>, // V8-serialized args
8989
},
@@ -388,7 +388,7 @@ fn decode_body(buf: &[u8]) -> io::Result<BinaryFrame> {
388388
})
389389
}
390390
MSG_BRIDGE_RESPONSE => {
391-
let call_id = read_u32(buf, &mut pos)?;
391+
let call_id = read_u64(buf, &mut pos)?;
392392
let status = read_u8(buf, &mut pos)?;
393393
let payload = buf[pos..].to_vec();
394394
Ok(BinaryFrame::BridgeResponse {
@@ -415,7 +415,7 @@ fn decode_body(buf: &[u8]) -> io::Result<BinaryFrame> {
415415
Ok(BinaryFrame::WarmSnapshot { bridge_code })
416416
}
417417
MSG_BRIDGE_CALL => {
418-
let call_id = read_u32(buf, &mut pos)?;
418+
let call_id = read_u64(buf, &mut pos)?;
419419
let m_len = read_u16(buf, &mut pos)? as usize;
420420
let method = read_utf8(buf, &mut pos, m_len)?;
421421
let payload = buf[pos..].to_vec();
@@ -539,6 +539,18 @@ fn read_u32(buf: &[u8], pos: &mut usize) -> io::Result<u32> {
539539
Ok(v)
540540
}
541541

542+
fn read_u64(buf: &[u8], pos: &mut usize) -> io::Result<u64> {
543+
if *pos + 8 > buf.len() {
544+
return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "unexpected end of frame"));
545+
}
546+
let v = u64::from_be_bytes([
547+
buf[*pos], buf[*pos + 1], buf[*pos + 2], buf[*pos + 3],
548+
buf[*pos + 4], buf[*pos + 5], buf[*pos + 6], buf[*pos + 7],
549+
]);
550+
*pos += 8;
551+
Ok(v)
552+
}
553+
542554
fn read_i32(buf: &[u8], pos: &mut usize) -> io::Result<i32> {
543555
if *pos + 4 > buf.len() {
544556
return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "unexpected end of frame"));

crates/v8-runtime/src/main.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -421,7 +421,20 @@ fn main() {
421421
.expect("failed to spawn connection handler");
422422
}
423423
Err(e) => {
424-
eprintln!("accept error: {}", e);
424+
// Transient errors: log and continue accepting
425+
let is_transient = matches!(
426+
e.raw_os_error(),
427+
Some(libc::EMFILE)
428+
| Some(libc::ENFILE)
429+
| Some(libc::ECONNABORTED)
430+
| Some(libc::EINTR)
431+
| Some(libc::EAGAIN)
432+
);
433+
if is_transient {
434+
eprintln!("transient accept error (continuing): {}", e);
435+
continue;
436+
}
437+
eprintln!("fatal accept error: {}", e);
425438
break;
426439
}
427440
}

packages/secure-exec-v8/src/ipc-binary.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -177,8 +177,8 @@ export function decodeFrame(buf: Buffer): BinaryFrame {
177177
};
178178
}
179179
case MSG_BRIDGE_RESPONSE: {
180-
const callId = buf.readUInt32BE(pos);
181-
pos += 4;
180+
const callId = Number(buf.readBigUInt64BE(pos));
181+
pos += 8;
182182
const status = buf[pos++];
183183
const payload = Buffer.from(buf.subarray(pos));
184184
return { type: "BridgeResponse", sessionId, callId, status, payload };
@@ -200,8 +200,8 @@ export function decodeFrame(buf: Buffer): BinaryFrame {
200200
return { type: "WarmSnapshot", bridgeCode };
201201
}
202202
case MSG_BRIDGE_CALL: {
203-
const callId = buf.readUInt32BE(pos);
204-
pos += 4;
203+
const callId = Number(buf.readBigUInt64BE(pos));
204+
pos += 8;
205205
const mLen = buf.readUInt16BE(pos);
206206
pos += 2;
207207
const method = buf.toString("utf8", pos, pos + mLen);
@@ -332,9 +332,9 @@ function encodeBody(frame: BinaryFrame): Buffer {
332332
case "BridgeResponse": {
333333
parts.push(Buffer.from([MSG_BRIDGE_RESPONSE]));
334334
parts.push(encodeSessionId(frame.sessionId));
335-
const fixed = Buffer.alloc(5);
336-
fixed.writeUInt32BE(frame.callId, 0);
337-
fixed[4] = frame.status;
335+
const fixed = Buffer.alloc(9);
336+
fixed.writeBigUInt64BE(BigInt(frame.callId), 0);
337+
fixed[8] = frame.status;
338338
parts.push(fixed);
339339
parts.push(frame.payload);
340340
break;
@@ -367,8 +367,8 @@ function encodeBody(frame: BinaryFrame): Buffer {
367367
case "BridgeCall": {
368368
parts.push(Buffer.from([MSG_BRIDGE_CALL]));
369369
parts.push(encodeSessionId(frame.sessionId));
370-
const callIdBuf = Buffer.alloc(4);
371-
callIdBuf.writeUInt32BE(frame.callId, 0);
370+
const callIdBuf = Buffer.alloc(8);
371+
callIdBuf.writeBigUInt64BE(BigInt(frame.callId), 0);
372372
parts.push(callIdBuf);
373373
const mBuf = Buffer.from(frame.method, "utf8");
374374
const mLen = Buffer.alloc(2);

packages/secure-exec-v8/test/ipc-binary.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -578,10 +578,10 @@ describe("wire format interop", () => {
578578
expect(body[0]).toBe(0x81); // msg_type
579579
expect(body[1]).toBe(1); // sid_len
580580
expect(body.toString("utf8", 2, 3)).toBe("X"); // sid
581-
expect(body.readUInt32BE(3)).toBe(42); // call_id
582-
expect(body.readUInt16BE(7)).toBe(2); // method_len
583-
expect(body.toString("utf8", 9, 11)).toBe("fn"); // method
584-
expect(Buffer.compare(body.subarray(11), Buffer.from([0xaa, 0xbb]))).toBe(
581+
expect(Number(body.readBigUInt64BE(3))).toBe(42); // call_id (u64)
582+
expect(body.readUInt16BE(11)).toBe(2); // method_len
583+
expect(body.toString("utf8", 13, 15)).toBe("fn"); // method
584+
expect(Buffer.compare(body.subarray(15), Buffer.from([0xaa, 0xbb]))).toBe(
585585
0,
586586
); // payload
587587
});

progress.txt

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,8 @@ PRD: ralph/kernel-hardening (46 stories)
143143
- Source policy tests (isolate-runtime-injection-policy, bridge-registry-policy) read specific source files by path — update them when moving code between files
144144
- esmModuleCache has a sibling esmModuleReverseCache (Map<ivm.Module, string>) for O(1) module→path lookup — both must be updated together and cleared together in execution.ts
145145
- V8 runtime event loop uses crossbeam Receiver<SessionCommand> — run_event_loop(scope, rx, pending) polls channel for BridgeResponse/StreamEvent/TerminateExecution
146-
- CallIdRouter (Arc<Mutex<HashMap<u32,String>>>) maps call_id→session_id for BridgeResponse routing; populated by BridgeCallContext, consumed by connection handler
146+
- CallIdRouter (Arc<Mutex<HashMap<u64,String>>>) maps call_id→session_id for BridgeResponse routing; populated by BridgeCallContext, consumed by connection handler
147+
- call_id is u64 throughout IPC stack (Rust AtomicU64 + 8-byte BE wire format + TS BigInt↔Number conversion); prevents wrap-around after ~4B calls
147148
- ChannelResponseReceiver implements ResponseReceiver trait — passes BinaryFrame directly from session channel to sync_call without re-serialization; BridgeCallContext::new() wraps reader in ReaderResponseReceiver (for tests), with_receiver() takes ResponseReceiver directly (production)
148149
- Per-connection SessionManager: each UDS connection gets its own SharedWriter and CallIdRouter (not global)
149150
- After iso.terminate_execution() in tests, call iso.cancel_terminate_execution() to allow continued isolate use
@@ -2810,3 +2811,24 @@ PRD: ralph/kernel-hardening (46 stories)
28102811
- V8 isolate teardown SIGSEGV is pre-existing and test-order-dependent — the process crashes during exit cleanup when both execution and snapshot tests run in the same process, but all test assertions pass before the crash
28112812
- `PermissionsExt` import moved to test module only since production code no longer needs it
28122813
---
2814+
2815+
## 2026-03-19 - US-073
2816+
- What was implemented:
2817+
- Changed call_id from u32 to u64 across the entire IPC stack (Rust + TypeScript)
2818+
- Rust: AtomicU32→AtomicU64 in host_call.rs, HashMap<u32>→HashMap<u64> in CallIdRouter and PendingPromises, updated BinaryFrame types
2819+
- Rust IPC: added read_u64 helper, BridgeCall/BridgeResponse encode/decode now use 8-byte BE for call_id
2820+
- TypeScript IPC: readBigUInt64BE/writeBigUInt64BE with Number() conversion for callId in BridgeCall/BridgeResponse
2821+
- Fixed accept error handling: transient errors (EMFILE, ENFILE, ECONNABORTED, EINTR, EAGAIN) log and continue; fatal errors break
2822+
- Files changed:
2823+
- crates/v8-runtime/src/host_call.rs — AtomicU32→AtomicU64, HashSet<u32>→HashSet<u64>, CallIdRouter HashMap<u32>→<u64>, async_send return u64
2824+
- crates/v8-runtime/src/ipc_binary.rs — BinaryFrame call_id: u32→u64, added read_u64, updated encode/decode
2825+
- crates/v8-runtime/src/bridge.rs — PendingPromises HashMap<u32>→<u64>, resolve_pending_promise call_id: u64
2826+
- crates/v8-runtime/src/main.rs — transient vs fatal accept error handling
2827+
- packages/secure-exec-v8/src/ipc-binary.ts — BigInt for 8-byte call_id read/write
2828+
- packages/secure-exec-v8/test/ipc-binary.test.ts — updated wire format byte offset test for 8-byte call_id
2829+
- **Learnings for future iterations:**
2830+
- On Linux, EAGAIN == EWOULDBLOCK (same errno value) — don't include both in match arms or you get unreachable pattern warning
2831+
- Wire format changes (u32→u64) require rebuilding the Rust binary (cargo build --release) before integration tests pass
2832+
- BinaryFrame::BridgeCall/BridgeResponse call_id field type change propagates automatically through destructuring patterns in session.rs and main.rs
2833+
- TS BigInt↔Number conversion via Number(buf.readBigUInt64BE()) and BigInt(frame.callId) works cleanly for counters within Number.MAX_SAFE_INTEGER
2834+
---

0 commit comments

Comments
 (0)