Skip to content

Commit fa3a802

Browse files
committed
merge ralph/kernel-hardening into main
2 parents d43010a + 1ea2f16 commit fa3a802

123 files changed

Lines changed: 11242 additions & 1157 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/nodejs-compatibility.mdx

Lines changed: 102 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
---
22
title: Node.js Compatibility
33
description: Target Node.js version and standard-library compatibility matrix for secure-exec.
4+
icon: "list-check"
45
---
56

67
## Target Node Version
@@ -9,116 +10,109 @@ description: Target Node.js version and standard-library compatibility matrix fo
910

1011
## Support Tiers
1112

12-
| Icon | Meaning |
13-
| --- | --- |
14-
| 🟢 | **Supported**: native or full implementation. |
15-
| 🔵 | **Planned**: not yet implemented; on the roadmap. |
16-
| 🟡 | **Partial**: functional with behavioral gaps or missing APIs. |
17-
|| **TBD**: under consideration; not yet committed. |
18-
| 🔴 | **Stub**: requireable but most APIs throw on call. |
19-
|| **Unsupported**: not available; `require()` throws immediately. |
20-
21-
---
22-
23-
## Module Compatibility Matrix
24-
25-
### Core I/O and Networking
26-
27-
| Module | Status | Notes |
28-
| --- | --- | --- |
29-
| **`fs`** | 🟡 | Core I/O implemented: `readFile`, `writeFile`, `appendFile`, `open`, `read`, `write`, `close`, `readdir`, `mkdir`, `rmdir`, `rm`, `unlink`, `stat`, `lstat`, `rename`, `copyFile`, `exists`, `createReadStream`, `createWriteStream`, `writev`, `access`, `realpath`. Deferred: `watch`, `watchFile`, `chmod`, `chown`, `link`, `symlink`, `readlink`, `truncate`, `utimes`. |
30-
| **`http`** | 🟡 | `request`, `get`, `createServer` with bridged request/response classes. Fetch-based, fully buffered. No connection pooling, keep-alive tuning, WebSocket upgrade, or trailer headers. `Agent` is stub-only. |
31-
| **`https`** | 🟡 | Same contract and limitations as `http`. |
32-
| **`http2`** | 🔴 | Compatibility classes only (`Http2ServerRequest`, `Http2ServerResponse`); `createServer`/`createSecureServer` throw. |
33-
| **`net`** | 🔵 | Planned. |
34-
| **`tls`** | 🔵 | Planned. |
35-
| **`dns`** | 🟢 | `lookup`, `resolve`, `resolve4`, `resolve6`, plus `dns.promises`. Permission-gated. |
36-
| **`dgram`** || |
37-
| **Fetch globals** | 🟢 | `fetch`, `Headers`, `Request`, `Response`. |
38-
39-
### Process and Runtime
40-
41-
| Module | Status | Notes |
13+
| Tier | Label | Meaning |
4214
| --- | --- | --- |
43-
| **`process`** | 🟢 | `env` (permission-gated), `cwd`/`chdir`, `exit`, timers, stdio, `hrtime`, `platform`, `arch`, `version`, `argv`, `pid`, `ppid`, `uid`, `gid`. |
44-
| **`child_process`** | 🟢 | `spawn`, `spawnSync`, `exec`, `execSync`, `execFile`, `execFileSync`. `fork` unsupported. |
45-
| **`os`** | 🟢 | `platform`, `arch`, `type`, `release`, `version`, `homedir`, `tmpdir`, `hostname`, `userInfo`, `os.constants`. |
46-
| **`timers`** | 🟢 | `setTimeout`, `clearTimeout`, `setInterval`, `clearInterval`, `setImmediate`, `clearImmediate`. |
47-
| **`module`** | 🟢 | `createRequire`, `Module` basics, builtin resolution. |
48-
| **`worker_threads`** | 🔴 | Requireable; APIs throw on call. |
49-
| **`cluster`** || |
50-
| **`vm`** | 🔴 | Polyfill via `Function()`/`eval()`. No real context isolation; shares global scope. |
51-
| **`v8`** | 🔴 | Mock heap stats; `serialize`/`deserialize` use JSON instead of V8 binary format. |
52-
53-
### Crypto and Security
54-
55-
| Module | Status | Notes |
56-
| --- | --- | --- |
57-
| **`crypto`** | 🔵 | `getRandomValues()` and `randomUUID()` use host secure randomness. `subtle.*` throws. Full crypto planned. |
58-
| **Web Crypto** | 🔵 | Planned. |
59-
60-
### Data and Encoding
15+
| 1 | Bridge | Runtime implementation in secure-exec bridge modules. |
16+
| 2 | Polyfill | Browser-compatible polyfill package implementation. |
17+
| 3 | Stub | Minimal compatibility surface for lightweight usage and type/instance checks. |
18+
| 4 | Deferred | `require()` succeeds, but APIs throw deterministic unsupported errors on call. |
19+
| 5 | Unsupported | Not implemented by design; `require()` throws immediately. |
6120

62-
| Module | Status | Notes |
63-
| --- | --- | --- |
64-
| **`buffer`** | 🟢 | |
65-
| **`stream`** | 🟢 | |
66-
| **`string_decoder`** | 🟢 | |
67-
| **`zlib`** | 🟢 | |
68-
| **`querystring`** | 🟢 | |
21+
Unsupported API errors follow this format: `"<module>.<api> is not supported in sandbox"`.
22+
Unsupported modules use: `"<module> is not supported in sandbox"`.
6923

70-
### Utilities and Diagnostics
24+
## Compatibility Matrix
7125

72-
| Module | Status | Notes |
26+
| Module | Tier | Status |
7327
| --- | --- | --- |
74-
| **`path`** | 🟢 | |
75-
| **`url`** | 🟢 | |
76-
| **`util`** | 🟢 | |
77-
| **`assert`** | 🟢 | |
78-
| **`events`** | 🟢 | |
79-
| **`console`** | 🟢 | Circular-safe bounded formatting. Drop-by-default; use `onStdio` hook for streaming. |
80-
| **`constants`** | 🟢 | |
81-
| **`tty`** | 🔴 | `isatty()` returns `false`; `ReadStream`/`WriteStream` are compatibility constructors. |
82-
| **`async_hooks`** || |
83-
| **`perf_hooks`** || |
84-
| **`diagnostics_channel`** || |
85-
| **`readline`** || |
86-
87-
### Unsupported
88-
89-
| Module | Status |
90-
| --- | --- |
91-
| **`dgram`** ||
92-
| **`wasi`** ||
93-
| **`inspector`** ||
94-
| **`repl`** ||
95-
| **`trace_events`** ||
96-
| **`domain`** ||
97-
98-
---
99-
100-
## Error Format
101-
102-
Unsupported API calls follow this format:
103-
104-
```
105-
<module>.<api> is not supported in sandbox
106-
```
107-
108-
Unsupported modules use:
109-
110-
```
111-
<module> is not supported in sandbox
112-
```
113-
114-
---
115-
116-
## Additional Notes
117-
118-
### Node-Modules Overlay
119-
120-
The Node runtime composes a read-only `/app/node_modules` overlay from `<cwd>/node_modules` (default `cwd` is host `process.cwd()`, configurable via `moduleAccess.cwd`). Writes under `/app/node_modules/**` are denied with `EACCES`. Native addons (`.node`) are rejected.
121-
122-
### Permission Model
123-
124-
Runtime permissions are deny-by-default for `fs`, `network`, `childProcess`, and `env`. If a domain checker is not configured, operations fail with `EACCES`. Embedders opt in via explicit permission policies (`allowAll`, `allowAllFs`, `allowAllNetwork`, `allowAllChildProcess`, `allowAllEnv`).
28+
| `fs` | 1 (Bridge) + 4 (Deferred APIs) | Implemented: `readFile`, `writeFile`, `appendFile`, `open`, `read`, `write`, `close`, `readdir`, `mkdir`, `rmdir`, `rm`, `unlink`, `stat`, `lstat`, `rename`, `copyFile`, `exists`, `createReadStream`, `createWriteStream`, `writev`, `access`, `realpath`, `chmod`, `chown`, `link`, `symlink`, `readlink`, `truncate`, `utimes`, `cp`, `mkdtemp`, `opendir`, `glob`, `statfs`, `readv`, `fdatasync`, `fsync`. Metadata-sensitive operations (`stat`, `exists`, `readdir` with `withFileTypes`) use metadata-native driver paths instead of content probing. `rename` delegates to driver semantics (atomic where supported; explicit limitation errors where not). Deferred: `watch`, `watchFile`. |
29+
| `process` | 1 (Bridge) | Env access (permission-gated), cwd/chdir, exit semantics, timers, stdio, eventing, and basic usage/system metadata APIs. |
30+
| `os` | 1 (Bridge) | Platform/arch/version, user/system info, and `os.constants`. |
31+
| `child_process` | 1 (Bridge) + 5 (`fork`) | Implemented: `spawn`, `spawnSync`, `exec`, `execSync`, `execFile`, `execFileSync`; `fork` is intentionally unsupported. |
32+
| `http` | 1 (Bridge) | Implemented: `request`, `get`, `createServer`; bridged request/response/server classes and constants. Includes `Agent` with connection pooling (`maxSockets`, `keepAlive`), HTTP upgrade (101 Switching Protocols) handling, and trailer headers support. |
33+
| `https` | 1 (Bridge) | Implemented: `request`, `get`, `createServer` with the same contract as `http`, including `Agent` pooling, upgrade handling, and trailer headers. |
34+
| `http2` | 3 (Stub) + 5 (Full support) | Provides compatibility classes (`Http2ServerRequest`, `Http2ServerResponse`); `createServer` and `createSecureServer` are unsupported. |
35+
| `dns` | 1 (Bridge) | Implemented: `lookup`, `resolve`, `resolve4`, `resolve6`, plus `dns.promises` variants. |
36+
| `module` | 1 (Bridge) | Implements `createRequire`, `Module` basics, and builtin resolution (`require.resolve("fs")` returns builtin identifiers). |
37+
| `timers` | 1 (Bridge) | `setTimeout`, `clearTimeout`, `setInterval`, `clearInterval`, `setImmediate`, `clearImmediate`. |
38+
| `path` | 2 (Polyfill) | `path-browserify`; supports default and named ESM imports. |
39+
| `buffer` | 2 (Polyfill) | Polyfill via `buffer`. |
40+
| `url` | 2 (Polyfill) | Polyfill via `whatwg-url` and node-stdlib-browser shims. |
41+
| `events` | 2 (Polyfill) | Polyfill via `events`. |
42+
| `stream` | 2 (Polyfill) | Polyfill via `readable-stream`. |
43+
| `util` | 2 (Polyfill) | Polyfill via node-stdlib-browser. |
44+
| `assert` | 2 (Polyfill) | Polyfill via node-stdlib-browser. |
45+
| `querystring` | 2 (Polyfill) | Polyfill via node-stdlib-browser. |
46+
| `string_decoder` | 2 (Polyfill) | Polyfill via node-stdlib-browser. |
47+
| `zlib` | 2 (Polyfill) | Polyfill via node-stdlib-browser. |
48+
| `vm` | 2 (Polyfill) | Polyfill via node-stdlib-browser. |
49+
| `punycode` | 2 (Polyfill) | Polyfill via node-stdlib-browser. |
50+
| `crypto` | 3 (Stub) | Limited bridge/polyfill blend; `getRandomValues()` and `randomUUID()` use host `node:crypto` secure randomness, or throw deterministic unsupported errors if host entropy is unavailable; `subtle.*` methods throw deterministic unsupported errors. |
51+
| `tty` | 2 (Polyfill) | `tty-browserify`; `isatty()` returns `false`; `ReadStream`/`WriteStream` are compatibility constructors. |
52+
| `v8` | 3 (Stub) | Pre-registered stub with mock heap stats and JSON-based `serialize`/`deserialize`. |
53+
| `constants` | 2 (Polyfill) | `constants-browserify`; `os.constants` remains available via `os`. |
54+
| Fetch globals (`fetch`, `Headers`, `Request`, `Response`) | 1 (Bridge) | Bridged via network bridge implementation. |
55+
| `async_hooks` | 3 (Stub) | `AsyncLocalStorage` (with `run`, `enterWith`, `getStore`, `disable`, `exit`), `AsyncResource` (with `runInAsyncScope`, `emitDestroy`), `createHook` (returns enable/disable no-ops), `executionAsyncId`, `triggerAsyncId`. |
56+
| `diagnostics_channel` | 3 (Stub) | No-op `channel()` and `tracingChannel()` stubs; channels always report `hasSubscribers: false`; `publish`, `subscribe`, `unsubscribe` are no-ops. Provides Fastify compatibility. |
57+
| Deferred modules (`net`, `tls`, `readline`, `perf_hooks`, `worker_threads`) | 4 (Deferred) | `require()` returns stubs; APIs throw deterministic unsupported errors when called. |
58+
| Unsupported modules (`dgram`, `cluster`, `wasi`, `inspector`, `repl`, `trace_events`, `domain`) | 5 (Unsupported) | `require()` fails immediately with deterministic unsupported-module errors. |
59+
60+
## Tested Packages
61+
62+
The [project-matrix test suite](https://github.com/rivet-dev/secure-exec/tree/main/packages/secure-exec/tests/projects) validates that real-world npm packages produce identical output in secure-exec and host Node.js. Each fixture is a black-box Node project with no sandbox-specific code.
63+
64+
| Package | Category | What It Tests |
65+
| --- | --- | --- |
66+
| [express](https://npmjs.com/package/express) | Web Framework | HTTP server, middleware, routing |
67+
| [fastify](https://npmjs.com/package/fastify) | Web Framework | Async middleware, schema validation, plugins |
68+
| [next](https://npmjs.com/package/next) | Web Framework | React SSR, module resolution, build tooling |
69+
| [vite](https://npmjs.com/package/vite) | Build Tool | ESM, HMR server, plugin system |
70+
| [astro](https://npmjs.com/package/astro) | Web Framework | Island architecture, SSR, multi-framework |
71+
| [hono](https://npmjs.com/package/hono) | Web Framework | ESM imports, lightweight HTTP |
72+
| [dotenv](https://npmjs.com/package/dotenv) | Configuration | Environment variable loading, fs reads |
73+
| [semver](https://npmjs.com/package/semver) | Utility | Version parsing and comparison |
74+
| [rivetkit](https://npmjs.com/package/rivetkit) | SDK | Local vendor package resolution |
75+
| crypto (builtin) | Crypto | `crypto.randomBytes`, `randomUUID`, `getRandomValues` |
76+
| fs-metadata-rename | Filesystem | `stat` metadata, `rename` semantics |
77+
| module-access | Module Resolution | Node modules overlay access |
78+
| conditional-exports | Module Resolution | Package `exports` field resolution |
79+
| optional-deps | Module Resolution | Optional dependency handling |
80+
| peer-deps | Module Resolution | Peer dependency resolution |
81+
| transitive-deps | Module Resolution | Transitive dependency chains |
82+
| pnpm-layout | Package Manager | pnpm `node_modules` structure |
83+
| npm-layout | Package Manager | npm flat `node_modules` layout |
84+
| yarn-classic-layout | Package Manager | Yarn v1 `node_modules` layout |
85+
| yarn-berry-layout | Package Manager | Yarn Berry PnP/node_modules layout |
86+
| bun-layout | Package Manager | Bun `node_modules` layout |
87+
| workspace-layout | Package Manager | npm workspace `node_modules` layout |
88+
| net-unsupported (fail) | Error Handling | `net.createServer` correctly errors |
89+
90+
To request a new package be added to the test suite, [open an issue](https://github.com/rivet-dev/secure-exec/issues/new?labels=package-request&title=Package+request:+%5Bpackage-name%5D).
91+
92+
## Logging Behavior
93+
94+
- `console.log`/`warn`/`error` are supported and serialize arguments with circular-safe bounded formatting.
95+
- `exec()`/`run()` results do not expose buffered `stdout`/`stderr` fields.
96+
- By default, secure-exec drops console emissions instead of buffering runtime-managed output.
97+
- Consumers that need logs should use the explicit `onStdio` hook to stream `stdout`/`stderr` events in emission order.
98+
99+
## TypeScript Workflows
100+
101+
- Core `secure-exec` runtimes execute JavaScript only.
102+
- Sandboxed TypeScript type checking and compilation belong in the separate `@secure-exec/typescript` package.
103+
104+
## Node-Modules Overlay Behavior
105+
106+
- Node runtime composes a read-only `/app/node_modules` overlay from `<cwd>/node_modules` (default `cwd` is host `process.cwd()`, configurable via `moduleAccess.cwd`).
107+
- Overlay reads are constrained to canonical paths under `<cwd>/node_modules` and fail closed on out-of-scope symlink/canonical escapes.
108+
- Writes and mutations under `/app/node_modules/**` are denied with `EACCES`.
109+
- Native addons (`.node`) are rejected in overlay-backed module loading.
110+
111+
## Permission Model (Runtime/Bridge Scope)
112+
113+
- This section describes the core runtime/bridge contract only.
114+
- Runtime permissions are deny-by-default for `fs`, `network`, `childProcess`, and `env`.
115+
- If a domain checker is not configured, operations fail with `EACCES`.
116+
- `filterEnv` strips environment variables unless `permissions.env` explicitly allows them.
117+
- Embedders can opt in via explicit permission policies such as `allowAll`, `allowAllFs`, `allowAllNetwork`, `allowAllChildProcess`, and `allowAllEnv`.
118+
- Driver-specific convenience defaults (for example, direct `createNodeDriver(...)` usage when adapters are provided without an explicit `permissions` policy) are implementation details and are not the canonical runtime/bridge security contract.

packages/kernel/src/kernel.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,11 @@ import {
3636
FILETYPE_REGULAR_FILE,
3737
FILETYPE_DIRECTORY,
3838
FILETYPE_PIPE,
39+
FILETYPE_CHARACTER_DEVICE,
3940
SEEK_SET,
4041
SEEK_CUR,
4142
SEEK_END,
43+
O_APPEND,
4244
SIGTERM,
4345
SIGWINCH,
4446
KernelError,
@@ -1015,7 +1017,11 @@ class KernelImpl implements Kernel {
10151017
} catch {
10161018
content = new Uint8Array(0);
10171019
}
1018-
const cursor = Number(entry.description.cursor);
1020+
1021+
// O_APPEND: every write seeks to end of file first (POSIX)
1022+
const cursor = (entry.description.flags & O_APPEND)
1023+
? content.length
1024+
: Number(entry.description.cursor);
10191025
const endPos = cursor + data.length;
10201026
const newContent = new Uint8Array(Math.max(content.length, endPos));
10211027
newContent.set(content);

packages/kernel/src/pty.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,7 @@ export class PtyManager {
296296
const state = this.ptys.get(ptyId);
297297
if (!state) throw new KernelError("EBADF", "PTY not found");
298298
return {
299+
icrnl: state.termios.icrnl,
299300
opost: state.termios.opost,
300301
onlcr: state.termios.onlcr,
301302
icanon: state.termios.icanon,
@@ -311,6 +312,7 @@ export class PtyManager {
311312
const state = this.ptys.get(ptyId);
312313
if (!state) throw new KernelError("EBADF", "PTY not found");
313314

315+
if (termios.icrnl !== undefined) state.termios.icrnl = termios.icrnl;
314316
if (termios.opost !== undefined) state.termios.opost = termios.opost;
315317
if (termios.onlcr !== undefined) state.termios.onlcr = termios.onlcr;
316318
if (termios.icanon !== undefined) state.termios.icanon = termios.icanon;
@@ -378,13 +380,15 @@ export class PtyManager {
378380
const { termios } = state;
379381

380382
// Fast path: no discipline processing (raw pass-through)
381-
if (!termios.icanon && !termios.echo && !termios.isig) {
383+
if (!termios.icanon && !termios.echo && !termios.isig && !termios.icrnl) {
382384
this.deliverInput(state, data);
383385
return data.length;
384386
}
385387

386388
// Process byte by byte through discipline
387-
for (const byte of data) {
389+
for (let byte of data) {
390+
// ICRNL: convert CR (0x0d) to NL (0x0a) before all other processing
391+
if (termios.icrnl && byte === 0x0d) byte = 0x0a;
388392
// Signal character handling (requires isig)
389393
if (termios.isig) {
390394
const signal = this.signalForByte(state, byte);

packages/kernel/src/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,8 @@ export class KernelError extends Error {
428428

429429
/** Terminal attributes — controls line discipline behavior on a PTY. */
430430
export interface Termios {
431+
/** Map CR (0x0d) to NL (0x0a) on input (POSIX ICRNL). */
432+
icrnl: boolean;
431433
/** Post-process output (master for ONLCR, etc.). */
432434
opost: boolean;
433435
/** Map NL to CR-NL on output (requires opost). */
@@ -453,6 +455,7 @@ export interface TermiosCC {
453455
/** Returns the POSIX-standard default termios: canonical on, echo on, isig on, opost+onlcr on. */
454456
export function defaultTermios(): Termios {
455457
return {
458+
icrnl: true,
456459
opost: true,
457460
onlcr: true,
458461
icanon: true,

packages/kernel/test/kernel-integration.test.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2832,6 +2832,77 @@ describe("kernel + MockRuntimeDriver integration", () => {
28322832

28332833
proc.kill();
28342834
});
2835+
2836+
it("ICRNL — CR (0x0d) converted to NL (0x0a) in canonical mode, delivered as newline", async () => {
2837+
const driver = new MockRuntimeDriver(["proc"], {
2838+
proc: { neverExit: true },
2839+
});
2840+
({ kernel } = await createTestKernel({ drivers: [driver] }));
2841+
2842+
const ki = driver.kernelInterface!;
2843+
const proc = kernel.spawn("proc", []);
2844+
const { masterFd, slaveFd } = ki.openpty(proc.pid);
2845+
2846+
ki.ptySetDiscipline(proc.pid, masterFd, { canonical: true, echo: false });
2847+
2848+
// Write 'hello' + CR (0x0d) — ICRNL converts CR to LF, flushes line
2849+
const input = new Uint8Array([0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x0d]); // 'hello\r'
2850+
ki.fdWrite(proc.pid, masterFd, input);
2851+
2852+
const data = await ki.fdRead(proc.pid, slaveFd, 1024);
2853+
expect(new TextDecoder().decode(data)).toBe("hello\n");
2854+
2855+
proc.kill();
2856+
});
2857+
2858+
it("ICRNL — CR input echoes as CR+LF", async () => {
2859+
const driver = new MockRuntimeDriver(["proc"], {
2860+
proc: { neverExit: true },
2861+
});
2862+
({ kernel } = await createTestKernel({ drivers: [driver] }));
2863+
2864+
const ki = driver.kernelInterface!;
2865+
const proc = kernel.spawn("proc", []);
2866+
const { masterFd, slaveFd } = ki.openpty(proc.pid);
2867+
2868+
ki.ptySetDiscipline(proc.pid, masterFd, { canonical: true, echo: true });
2869+
2870+
// Write 'A' + CR — ICRNL converts to LF, echo should produce 'A' then CR+LF
2871+
ki.fdWrite(proc.pid, masterFd, new Uint8Array([0x41, 0x0d])); // 'A\r'
2872+
2873+
// Slave reads the flushed line
2874+
const slaveData = await ki.fdRead(proc.pid, slaveFd, 1024);
2875+
expect(new TextDecoder().decode(slaveData)).toBe("A\n");
2876+
2877+
// Master reads echoed: 'A' + CR+LF (newline echo)
2878+
const echoData = await ki.fdRead(proc.pid, masterFd, 1024);
2879+
expect(new TextDecoder().decode(echoData)).toBe("A\r\n");
2880+
2881+
proc.kill();
2882+
});
2883+
2884+
it("ICRNL disabled — CR passes through as-is", async () => {
2885+
const driver = new MockRuntimeDriver(["proc"], {
2886+
proc: { neverExit: true },
2887+
});
2888+
({ kernel } = await createTestKernel({ drivers: [driver] }));
2889+
2890+
const ki = driver.kernelInterface!;
2891+
const proc = kernel.spawn("proc", []);
2892+
const { masterFd, slaveFd } = ki.openpty(proc.pid);
2893+
2894+
// Disable ICRNL via tcsetattr
2895+
ki.tcsetattr(proc.pid, masterFd, { icrnl: false });
2896+
ki.ptySetDiscipline(proc.pid, masterFd, { canonical: false, echo: false, isig: false });
2897+
2898+
// Write CR — should pass through as 0x0d, not converted to 0x0a
2899+
ki.fdWrite(proc.pid, masterFd, new Uint8Array([0x0d]));
2900+
2901+
const data = await ki.fdRead(proc.pid, slaveFd, 1024);
2902+
expect(data[0]).toBe(0x0d);
2903+
2904+
proc.kill();
2905+
});
28352906
});
28362907

28372908
// -------------------------------------------------------------------

0 commit comments

Comments
 (0)