View Issue Details
| ID | Project | Category | View Status | Date Submitted | Last Update |
|---|---|---|---|---|---|
| 0011951 | Taler | qtart | public | 2026-11-05 08:59 | 2026-11-05 08:59 |
| Reporter | bot-codex-florian-dold [agent] | Assigned To | Resolution deadline | ||
| Implementer | Reviewer | ||||
| Release signed by | Deployed by | ||||
| Verifier | Communicator | CM_7 | |||
| Data protection authority notification required | No | ||||
| CSIRT notification required | No | ||||
| Estimated effort (hours) | Actual effort (hours) | 0 | |||
| Fast-track | No | ||||
| Work package | none | ||||
| Priority | normal | Severity | major | Reproducibility | have not tried |
| Status | new | Resolution | open | ||
| Summary | 0011951: qtart: zero-delay timer chains starve HTTP response processing | ||||
| Description | A background computation that cooperatively yields with await new Promise(resolve => setTimeout(resolve, 0)) can prevent Qtart from delivering an HTTP response that has already arrived. This can stall an in-progress payment while ordinary host API requests remain responsive. In quickjs/quickjs-libc.c, js_std_loop drains pending promise jobs before calling js_os_poll. js_os_poll checks host messages, then runs an expired timer and returns immediately, before poll() and handle_http_message(). The timer resolves the computation's promise, whose continuation runs another chunk and schedules another already-expired timer. A sustained chain can therefore bypass HTTP and worker I/O indefinitely. The event-source fairness rotation below poll() cannot help when that code is never reached. The early timer return also bypasses the poll_iteration_count increment. The prelude maps setTimeout directly to os.setTimeout, with no positive minimum delay. Native HTTP callbacks enqueue responses on http_pipe, separate from host_pipe. The explicit pre-timer host-message check explains why API calls can remain fast while HTTP completion is starved. Observed while investigating Android wallet-core 1.6.49 (9b08843c54a93d83b2df70cad9d25093b85720e1). Source inspection confirmed the same event-loop ordering in quickjs-tart 3f58047d99674e7376680d18f4475be0f2687543, which bumps the Android package to 1.6.49. The local runtime reproducer below demonstrates the scheduling defect independently of the wallet database; it was not run on the affected Android device. In the wallet log, native request 28 starts at 12:24:25.439 and its response is ready at 25.564. Wallet-core records the temporally corresponding /pay completion at 27.429, about 1.87 seconds later, amid background balance calculations. Native and wallet-core request IDs are not explicitly mapped in the capture, so this correspondence is inferred from timing. | ||||
| Steps To Reproduce | Run this as an ES module with qtart. It makes a pipe readable before starting 100 two-millisecond computation chunks: import * as os from "os"; const [rd, wr] = os.pipe(); const buf = new Uint8Array([1]); os.write(wr, buf.buffer, 0, 1); const start = Date.now(); let count = 0; os.setReadHandler(rd, () => { os.read(rd, buf.buffer, 0, 1); console.log("I/O serviced after", Date.now() - start, "ms; chunks", count); os.setReadHandler(rd, null); os.close(rd); os.close(wr); }); (async () => { for (let i = 0; i < 100; i++) { const end = Date.now() + 2; while (Date.now() < end) {} count++; await new Promise(resolve => os.setTimeout(resolve, 0)); } })(); Observed locally: the readable pipe is serviced after all 100 chunks, at 200 ms. Changing the timer delay to 1 ms services it after the first chunk, at 2 ms. The positive-delay variant is a diagnostic control, not a proposed fairness guarantee. | ||||
| Additional Information | Expected fix and acceptance criteria: - Include ready timers, host messages, HTTP responses, worker messages, and other ready I/O in a bounded fair dispatch policy. Preserve prompt host-message and shutdown handling without merely moving starvation to another event source. - Add a regression test in which HTTP or worker completion must be delivered while a sustained zero-delay timer chain is still running, not only after the chain ends. Use deterministic synchronization where possible rather than fragile timing thresholds. - Check both timer-heavy and message-heavy workloads for starvation. - Do not rely only on changing wallet yields from 0 to 1 ms: multiple ready timers or other work can eliminate that accidental polling opportunity. Relevant code: quickjs/quickjs-libc.c js_os_poll (around lines 3355-3470), js_std_loop (around 5361), handle_http_resp (around 2373); prelude.js setTimeout mapping. This issue addresses runtime fairness. Separate wallet-core issues cover excessive computation and interactive-work prioritization. | ||||
| Tags | No tags attached. | ||||
| Date Modified | Username | Field | Change |
|---|---|---|---|
| 2026-11-05 08:59 | bot-codex-florian-dold | New Issue |