From 959d0ee01c2fa1e60c2d1493e6b93e10a752412a Mon Sep 17 00:00:00 2001 From: Sam Clegg Date: Sun, 23 Aug 2026 22:41:34 -0700 Subject: [PATCH] [noderawfs] Add `getpass` to libc build Include `getpass.c` in libc and ensure `getpass()` works correctly at least under Node.js. This includes two bugfixes needed to make `getpass()` work: 1. In `libtty.js`: In canonical mode (`ICANON`), make `read` return as soon as a newline character (`\n`) is encountered. 2. In `libnoderawfs.js`: Properly mark TTY streams as non-seekable when opened via `FS.open`, and remove hardcoded `seekable: false` from standard stream creation. See: #27583 --- src/lib/libnoderawfs.js | 5 +- src/lib/libtty.js | 3 ++ test/codesize/test_codesize_cxx_ctors1.json | 8 +-- test/codesize/test_codesize_cxx_ctors2.json | 8 +-- test/codesize/test_codesize_cxx_except.json | 8 +-- .../test_codesize_cxx_except_wasm.json | 8 +-- .../test_codesize_cxx_except_wasm_legacy.json | 8 +-- test/codesize/test_codesize_cxx_lto.json | 8 +-- test/codesize/test_codesize_cxx_mangle.json | 8 +-- test/codesize/test_codesize_cxx_noexcept.json | 8 +-- .../test_codesize_file_preload.expected.js | 3 ++ test/codesize/test_codesize_file_preload.json | 8 +-- test/codesize/test_codesize_files_js_fs.json | 8 +-- test/codesize/test_codesize_hello_dylink.json | 8 +-- .../test_codesize_hello_dylink_all.json | 8 +-- test/decorators.py | 2 + test/test_other.py | 49 +++++++++++++++---- test/unistd/getpass.c | 19 +++++++ tools/native_sigs.py | 1 + tools/system_libs.py | 2 +- 20 files changed, 121 insertions(+), 59 deletions(-) create mode 100644 test/unistd/getpass.c diff --git a/src/lib/libnoderawfs.js b/src/lib/libnoderawfs.js index 509f5898d0165..d9a4eeda70d16 100644 --- a/src/lib/libnoderawfs.js +++ b/src/lib/libnoderawfs.js @@ -65,10 +65,10 @@ addToLibrary({ return { path, node: { id: st.ino, mode, node_ops: NODERAWFS, path }}; }, createStandardStreams() { - FS.createStream({ nfd: 0, position: 0, path: '/dev/stdin', flags: 0, seekable: false }, 0); + FS.createStream({ nfd: 0, position: 0, path: '/dev/stdin', flags: 0 }, 0); var paths = [,'/dev/stdout', '/dev/stderr']; for (var i = 1; i < 3; i++) { - FS.createStream({ nfd: i, position: 0, path: paths[i], flags: {{{ cDefs.O_TRUNC | cDefs.O_CREAT | cDefs.O_WRONLY }}}, seekable: false }, i); + FS.createStream({ nfd: i, position: 0, path: paths[i], flags: {{{ cDefs.O_TRUNC | cDefs.O_CREAT | cDefs.O_WRONLY }}} }, i); } }, // generic function for all node creation @@ -203,6 +203,7 @@ addToLibrary({ rtn.shared.refcnt++; if (nodeTTY.isatty(rtn.nfd)) { rtn.tty = { ops: TTY.default_tty_ops }; + rtn.seekable = false; } } return rtn; diff --git a/src/lib/libtty.js b/src/lib/libtty.js index 010cdad2d9a62..0bbd792cce399 100644 --- a/src/lib/libtty.js +++ b/src/lib/libtty.js @@ -77,6 +77,9 @@ addToLibrary({ if (result === null || result === undefined) break; bytesRead++; buffer[offset+i] = result; + // We currently only support canonical mode (ICANON), where + // read(2) returns as soon as a line delimiter is read. + if (result === {{{ charCode('\n') }}}) break; } if (bytesRead) { stream.node.atime = Date.now(); diff --git a/test/codesize/test_codesize_cxx_ctors1.json b/test/codesize/test_codesize_cxx_ctors1.json index 028e5928dea30..978d75d04d604 100644 --- a/test/codesize/test_codesize_cxx_ctors1.json +++ b/test/codesize/test_codesize_cxx_ctors1.json @@ -1,10 +1,10 @@ { - "a.out.js": 19208, - "a.out.js.gz": 8121, + "a.out.js": 19224, + "a.out.js.gz": 8124, "a.out.nodebug.wasm": 134735, "a.out.nodebug.wasm.gz": 51558, - "total": 153943, - "total_gz": 59679, + "total": 153959, + "total_gz": 59682, "sent": [ "__cxa_throw", "_abort_js", diff --git a/test/codesize/test_codesize_cxx_ctors2.json b/test/codesize/test_codesize_cxx_ctors2.json index cae944460444b..cef0e864668a6 100644 --- a/test/codesize/test_codesize_cxx_ctors2.json +++ b/test/codesize/test_codesize_cxx_ctors2.json @@ -1,10 +1,10 @@ { - "a.out.js": 19185, - "a.out.js.gz": 8104, + "a.out.js": 19201, + "a.out.js.gz": 8107, "a.out.nodebug.wasm": 134164, "a.out.nodebug.wasm.gz": 51206, - "total": 153349, - "total_gz": 59310, + "total": 153365, + "total_gz": 59313, "sent": [ "__cxa_throw", "_abort_js", diff --git a/test/codesize/test_codesize_cxx_except.json b/test/codesize/test_codesize_cxx_except.json index 4ea2eae87573d..c6baa9768e2f3 100644 --- a/test/codesize/test_codesize_cxx_except.json +++ b/test/codesize/test_codesize_cxx_except.json @@ -1,10 +1,10 @@ { - "a.out.js": 22901, - "a.out.js.gz": 9079, + "a.out.js": 22917, + "a.out.js.gz": 9078, "a.out.nodebug.wasm": 177192, "a.out.nodebug.wasm.gz": 59082, - "total": 200093, - "total_gz": 68161, + "total": 200109, + "total_gz": 68160, "sent": [ "__cxa_begin_catch", "__cxa_end_catch", diff --git a/test/codesize/test_codesize_cxx_except_wasm.json b/test/codesize/test_codesize_cxx_except_wasm.json index daa3323f37a54..b7782decd9f17 100644 --- a/test/codesize/test_codesize_cxx_except_wasm.json +++ b/test/codesize/test_codesize_cxx_except_wasm.json @@ -1,10 +1,10 @@ { - "a.out.js": 19007, - "a.out.js.gz": 8036, + "a.out.js": 19023, + "a.out.js.gz": 8039, "a.out.nodebug.wasm": 150448, "a.out.nodebug.wasm.gz": 56601, - "total": 169455, - "total_gz": 64637, + "total": 169471, + "total_gz": 64640, "sent": [ "_abort_js", "_tzset_js", diff --git a/test/codesize/test_codesize_cxx_except_wasm_legacy.json b/test/codesize/test_codesize_cxx_except_wasm_legacy.json index c40d310dbd854..ae3d1d4fb1743 100644 --- a/test/codesize/test_codesize_cxx_except_wasm_legacy.json +++ b/test/codesize/test_codesize_cxx_except_wasm_legacy.json @@ -1,10 +1,10 @@ { - "a.out.js": 19085, - "a.out.js.gz": 8060, + "a.out.js": 19101, + "a.out.js.gz": 8065, "a.out.nodebug.wasm": 148230, "a.out.nodebug.wasm.gz": 56281, - "total": 167315, - "total_gz": 64341, + "total": 167331, + "total_gz": 64346, "sent": [ "_abort_js", "_tzset_js", diff --git a/test/codesize/test_codesize_cxx_lto.json b/test/codesize/test_codesize_cxx_lto.json index 999799f4b6af7..4ecf2ff281967 100644 --- a/test/codesize/test_codesize_cxx_lto.json +++ b/test/codesize/test_codesize_cxx_lto.json @@ -1,10 +1,10 @@ { - "a.out.js": 18552, - "a.out.js.gz": 7814, + "a.out.js": 18568, + "a.out.js.gz": 7818, "a.out.nodebug.wasm": 100987, "a.out.nodebug.wasm.gz": 38221, - "total": 119539, - "total_gz": 46035, + "total": 119555, + "total_gz": 46039, "sent": [ "a (emscripten_resize_heap)", "b (_setitimer_js)", diff --git a/test/codesize/test_codesize_cxx_mangle.json b/test/codesize/test_codesize_cxx_mangle.json index 8f80863f539ea..a652566832528 100644 --- a/test/codesize/test_codesize_cxx_mangle.json +++ b/test/codesize/test_codesize_cxx_mangle.json @@ -1,10 +1,10 @@ { - "a.out.js": 22951, - "a.out.js.gz": 9099, + "a.out.js": 22967, + "a.out.js.gz": 9098, "a.out.nodebug.wasm": 243472, "a.out.nodebug.wasm.gz": 81286, - "total": 266423, - "total_gz": 90385, + "total": 266439, + "total_gz": 90384, "sent": [ "__cxa_begin_catch", "__cxa_end_catch", diff --git a/test/codesize/test_codesize_cxx_noexcept.json b/test/codesize/test_codesize_cxx_noexcept.json index 5312df21688d1..4b9882384479b 100644 --- a/test/codesize/test_codesize_cxx_noexcept.json +++ b/test/codesize/test_codesize_cxx_noexcept.json @@ -1,10 +1,10 @@ { - "a.out.js": 19208, - "a.out.js.gz": 8121, + "a.out.js": 19224, + "a.out.js.gz": 8124, "a.out.nodebug.wasm": 136645, "a.out.nodebug.wasm.gz": 52165, - "total": 155853, - "total_gz": 60286, + "total": 155869, + "total_gz": 60289, "sent": [ "__cxa_throw", "_abort_js", diff --git a/test/codesize/test_codesize_file_preload.expected.js b/test/codesize/test_codesize_file_preload.expected.js index 024496f7d7701..6999515586248 100644 --- a/test/codesize/test_codesize_file_preload.expected.js +++ b/test/codesize/test_codesize_file_preload.expected.js @@ -865,6 +865,9 @@ var TTY = { if (result === null || result === undefined) break; bytesRead++; buffer[offset + i] = result; + // We currently only support canonical mode (ICANON), where + // read(2) returns as soon as a line delimiter is read. + if (result === 10) break; } if (bytesRead) { stream.node.atime = Date.now(); diff --git a/test/codesize/test_codesize_file_preload.json b/test/codesize/test_codesize_file_preload.json index b953023499e05..b76156ab8090a 100644 --- a/test/codesize/test_codesize_file_preload.json +++ b/test/codesize/test_codesize_file_preload.json @@ -1,10 +1,10 @@ { - "a.out.js": 22187, - "a.out.js.gz": 9259, + "a.out.js": 22203, + "a.out.js.gz": 9267, "a.out.nodebug.wasm": 1666, "a.out.nodebug.wasm.gz": 945, - "total": 23853, - "total_gz": 10204, + "total": 23869, + "total_gz": 10212, "sent": [ "a (fd_write)" ], diff --git a/test/codesize/test_codesize_files_js_fs.json b/test/codesize/test_codesize_files_js_fs.json index b60d8b6a54be8..0c033a7242071 100644 --- a/test/codesize/test_codesize_files_js_fs.json +++ b/test/codesize/test_codesize_files_js_fs.json @@ -1,10 +1,10 @@ { - "a.out.js": 17855, - "a.out.js.gz": 7462, + "a.out.js": 17871, + "a.out.js.gz": 7460, "a.out.nodebug.wasm": 381, "a.out.nodebug.wasm.gz": 258, - "total": 18236, - "total_gz": 7720, + "total": 18252, + "total_gz": 7718, "sent": [ "a (fd_write)", "b (fd_read)", diff --git a/test/codesize/test_codesize_hello_dylink.json b/test/codesize/test_codesize_hello_dylink.json index b1be2c6b58494..d8207cfb3f576 100644 --- a/test/codesize/test_codesize_hello_dylink.json +++ b/test/codesize/test_codesize_hello_dylink.json @@ -1,10 +1,10 @@ { - "a.out.js": 26242, - "a.out.js.gz": 11203, + "a.out.js": 26258, + "a.out.js.gz": 11211, "a.out.nodebug.wasm": 17854, "a.out.nodebug.wasm.gz": 9021, - "total": 44096, - "total_gz": 20224, + "total": 44112, + "total_gz": 20232, "sent": [ "__syscall_stat64", "emscripten_resize_heap", diff --git a/test/codesize/test_codesize_hello_dylink_all.json b/test/codesize/test_codesize_hello_dylink_all.json index b1aad64974b3a..fc70737a0da12 100644 --- a/test/codesize/test_codesize_hello_dylink_all.json +++ b/test/codesize/test_codesize_hello_dylink_all.json @@ -1,7 +1,7 @@ { - "a.out.js": 270568, - "a.out.nodebug.wasm": 588318, - "total": 858886, + "a.out.js": 270584, + "a.out.nodebug.wasm": 588573, + "total": 859157, "sent": [ "IMG_Init", "IMG_Load", @@ -2829,6 +2829,7 @@ "getopt_long", "getopt_long_only", "getpagesize", + "getpass", "getpeername", "getpgid", "getpgrp", @@ -4671,6 +4672,7 @@ "$getopt_long", "$getopt_long_only", "$getpagesize", + "$getpass", "$getpeername", "$getpgid", "$getpriority", diff --git a/test/decorators.py b/test/decorators.py index 823f6f3d33749..d998394494af7 100644 --- a/test/decorators.py +++ b/test/decorators.py @@ -123,6 +123,8 @@ def disabled(note=''): needs_make = skip_if('tool not available on windows bots', lambda _: WINDOWS) +requires_login_tty = skip_if_simple('requires os.login_tty (python 3.11+)', lambda _: not hasattr(os, 'login_tty')) + def requires_node(func): assert callable(func) diff --git a/test/test_other.py b/test/test_other.py index 1cd03d2dfa221..50dff783b8f7c 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -4,6 +4,7 @@ # found in the LICENSE file. +import errno import glob import hashlib import importlib @@ -14,7 +15,6 @@ import platform import random import re -import select import shlex import shutil import struct @@ -84,6 +84,7 @@ parameterized, requires_dev_dependency, requires_jspi, + requires_login_tty, requires_native_clang, requires_network, requires_node, @@ -332,23 +333,44 @@ def assertIsWasmDylib(self, filename): def do_other_test(self, testname, cflags=None, **kwargs): return self.do_runf_out_file(test_file('other', testname), cflags=cflags, **kwargs) - def run_on_pty(self, cmd): + def run_on_pty(self, cmd, input=None): master, slave = os.openpty() output = [] print(cmd) + if input: + assert hasattr(os, 'login_tty'), 'passing input to run_on_pty requires os.login_tty (python 3.11+)' + if isinstance(input, str): + input = input.encode('utf-8') + os.write(master, input) + try: with env_modify({'TERM': 'xterm-color'}): - proc = subprocess.Popen(cmd, stdout=slave, stderr=slave) - while proc.poll() is None: - r, _w, _x = select.select([master], [], [], 1) - if r: - output.append(os.read(master, 1024)) - return (proc.returncode, b''.join(output)) + if hasattr(os, 'login_tty'): + proc = subprocess.Popen(cmd, preexec_fn=lambda: os.login_tty(slave), close_fds=True) # ruff: ignore[subprocess-popen-preexec-fn] + else: + proc = subprocess.Popen(cmd, stdout=slave, stderr=slave, close_fds=True) finally: - os.close(master) os.close(slave) + try: + while True: + try: + data = os.read(master, 1024) + if not data: + break + output.append(data) + except OSError as e: + # On Linux, once all slave descriptors are closed and all buffered + # output has been consumed, reading from master raises EIO (EOF). + if e.errno == errno.EIO: + break + raise + proc.wait() + return (proc.returncode, b''.join(output)) + finally: + os.close(master) + def create_huge_file(self, name, length): f = open(name, "wb") f.seek(length - 1) @@ -9702,6 +9724,15 @@ def test_noderawfs_readfile_prerun(self): self.add_pre_run("console.log(FS.readFile('foo', { encoding: 'utf8' }));") self.do_runf('hello_world.c', 'bar', cflags=['-sNODERAWFS', '-sFORCE_FILESYSTEM']) + @also_with_noderawfs + @no_windows('ptys and select are not available on windows') + @requires_login_tty + def test_getpass(self): + self.run_process([EMCC, test_file('unistd/getpass.c'), '-sFORCE_FILESYSTEM'] + self.get_cflags()) + returncode, output = self.run_on_pty(config.NODE_JS + ['a.out.js'], input='secret\n') + self.assertEqual(returncode, 0) + self.assertIn(b'done', output) + @disabled('https://github.com/nodejs/node/issues/18265') def test_node_code_caching(self): self.run_process([EMCC, test_file('hello_world.c'), diff --git a/test/unistd/getpass.c b/test/unistd/getpass.c new file mode 100644 index 0000000000000..37cadadad8019 --- /dev/null +++ b/test/unistd/getpass.c @@ -0,0 +1,19 @@ +/* + * Copyright 2026 The Emscripten Authors. All rights reserved. + * Emscripten is available under two separate licenses, the MIT license and the + * University of Illinois/NCSA Open Source License. Both these licenses can be + * found in the LICENSE file. + */ + +#include +#include +#include +#include + +int main() { + char* pass = getpass("Password: "); + assert(pass != NULL); + assert(strcmp(pass, "secret") == 0); + puts("done"); + return 0; +} diff --git a/tools/native_sigs.py b/tools/native_sigs.py index f8ccb9ff2829d..20a459d4f090d 100644 --- a/tools/native_sigs.py +++ b/tools/native_sigs.py @@ -1039,6 +1039,7 @@ 'getopt': '__pp', 'getopt_long': '__pppp', 'getopt_long_only': '__pppp', + 'getpass': 'pp', 'getpeername': '__pp', 'getpwent': 'p', 'getpwnam': 'pp', diff --git a/tools/system_libs.py b/tools/system_libs.py index e1270eb7dab65..9ccda61b7ed8d 100644 --- a/tools/system_libs.py +++ b/tools/system_libs.py @@ -1341,7 +1341,7 @@ def get_files(self): libc_files += files_in_path( path='system/lib/libc/musl/src/legacy', - filenames=['getpagesize.c', 'err.c', 'euidaccess.c']) + filenames=['getpagesize.c', 'err.c', 'euidaccess.c', 'getpass.c']) libc_files += files_in_path( path='system/lib/libc/musl/src/linux',