Repository navigation
In raw mode Enter still arrives as 0x0a, because tcsetattr cannot clear ICRNL (openkal-musl 0.20.1, openkal-linux 0.16.1) #48
Description
A program that enters raw mode with cfmakeraw and tcsetattr above openkal-musl still receives Enter as 0x0a, the same byte as Ctrl+J, because the terminal goes on translating CR to LF on input. Above the system's own C library the same program receives 0x0d for Enter and 0x0a for Ctrl+J. A terminal application I build for x86_64-linux-musl above openkal wants Ctrl+J to insert a line break and Enter to submit, so it needs the two apart. It enters raw mode the way the port's own examples/terminal program does.
Versions: mcpp 2026.10.8.1, llvm@23.1.3, openkal-llvm-runtime 0.15.4, which resolves to openkal-musl 0.20.1, openkal 0.15.0 and openkal-linux 0.16.1 on Linux. The host terminal is an ordinary pty with ICRNL on, which is the default.
ICRNL never reaches the kernel. musl's cfmakeraw clears it together with the other input flags (cfmakeraw.c L6). okm_termios_set then builds the mode word from ICANON, ECHO and ISIG only and drops everything else (okm_syscall.c L623-L626):
if (t->c_lflag & ICANON) mode |= KAL_TERM_LINE_EDIT;
if (t->c_lflag & ECHO) mode |= KAL_TERM_ECHO;
if (!(t->c_lflag & ISIG)) mode |= KAL_TERM_PASS_CONTROL;
const int e = kal_terminal_set_mode(s, mode);Even if it were passed down there would be nowhere for it to go. kal_terminal_set_mode in openkal-linux reads the kernel termios, edits ICANON, ECHO and, for KAL_TERM_PASS_CONTROL, the trio ISIG, IEXTEN and IXON, and writes the structure back (terminal.cpp L86-L94). ICRNL keeps whatever value the host had, so the line discipline goes on turning CR into LF. The read path is a plain read(2), so nothing else rewrites the byte.
okm_termios_get reports c_iflag = ICRNL unconditionally (L578). Given the above that report is accurate, and it means a program that reads the flags back after tcsetattr can see that its request did not take effect.
This is close to a documented limitation, so I am not calling it a regression. The comment above okm_termios_get lists the newline translation the terminal performs among the flags whose effect this environment has and which a program cannot change (L558-L568), and the README row for "a terminal's whole state" says the same of everything the structure carries that openkal does not name (README.md L297). But the list that follows there, and the tcsetattr note in mcpp.toml L163, name output post-processing, the line speed, the control characters and VMIN/VTIME, and no input flag, and examples/terminal prints ixon but not icrnl.
The row also says why the rest is left out: the three positions are the ones a program needs in order to read keystrokes, and the others are the terminal's own or output-side. ICRNL is neither of those, since it decides which byte a key produces on the way in. The review of openkal#37 lists Enter becoming 0x0a, ICRNL, beside the ISIG, IXON and IEXTEN rows and marks it as not covered and open to separate discussion (L81). This report is about that row.
Two sentences in terminal.h point in different directions. L40 says that with KAL_TERM_PASS_CONTROL set every keystroke reaches the program as the bytes it produces, which Enter does not while ICRNL is on. L37 and L57 describe the position as the environment reserving no keystroke and keeping none, and the terminal rewrites Enter rather than keeping it. I read the position as being about reservation, which is why I suggest a separate position below. If L40 is meant to cover this, the Linux backend is out of line with it, and I would like to know which reading is intended.
Reproduction
One C file, built once for x86_64-linux-gnu as the control and once for x86_64-linux-musl. It enters raw mode, prints ICRNL before the call, as requested and as read back, then prints every byte it reads until q. The keys Enter, Ctrl+J and q were sent in that order with tmux send-keys once the program had printed its prompt.
mcpp.toml:
[package]
namespace = "probe"
name = "icrnl"
version = "0.0.0"
[toolchain]
default = "llvm@23.1.3"
[targets.icrnl]
kind = "bin"
main = "src/main.c"
[target.'cfg(all(os = "linux", env = "musl"))'.dependencies]
openkal-llvm-runtime = "0.15.4"src/main.c:
#define _GNU_SOURCE 1
#include <stdio.h>
#include <termios.h>
#include <unistd.h>
int main(void) {
setvbuf(stdout, NULL, _IONBF, 0);
struct termios old, raw, back;
if (tcgetattr(0, &old) != 0) { perror("tcgetattr"); return 1; }
raw = old;
cfmakeraw(&raw);
int rc = tcsetattr(0, TCSANOW, &raw);
if (rc != 0) { perror("tcsetattr"); return 1; }
if (tcgetattr(0, &back) != 0) { perror("tcgetattr"); tcsetattr(0, TCSANOW, &old); return 1; }
printf("ICRNL before=%d requested=%d readback=%d (tcsetattr rc=%d)\r\n",
(old.c_iflag & ICRNL) != 0, (raw.c_iflag & ICRNL) != 0,
(back.c_iflag & ICRNL) != 0, rc);
printf("reading: press Enter, Ctrl+J, then q\r\n");
for (unsigned char c; read(0, &c, 1) == 1; ) {
printf("byte 0x%02x\r\n", c);
if (c == 'q') break;
}
tcsetattr(0, TCSANOW, &old);
return 0;
}Built with mcpp build --target x86_64-linux-gnu and mcpp build --target x86_64-linux-musl, and run in a tmux pane.
Above glibc (x86_64-linux-gnu):
ICRNL before=1 requested=0 readback=0 (tcsetattr rc=0)
reading: press Enter, Ctrl+J, then q
byte 0x0d
byte 0x0a
byte 0x71
Above openkal-musl 0.20.1 (x86_64-linux-musl):
ICRNL before=1 requested=0 readback=1 (tcsetattr rc=0)
reading: press Enter, Ctrl+J, then q
byte 0x0a
byte 0x0a
byte 0x71
A possible change
Offered as a starting point. The final design is yours to decide.
Folding ICRNL into KAL_TERM_PASS_CONTROL would be the smallest change, and I think clauses 6.2 and 8 rule it out. A position, once assigned, retains its meaning (SPEC.md L520), and a revision shall not alter existing declarations (L960). Backends released before the change, such as openkal-linux 0.16.1 and the current openkal-macos, report the position set whenever ISIG, IEXTEN and IXON are clear, and that would become a claim about ICRNL they never made. A port that read ICRNL back from the position would then tell a program that the translation is off while the kernel is still doing it. Clause 11 entry 20 gives this reason for the spelling of the existing position. The change would also alter what a program that clears ISIG alone gets: Enter as 0x0d where glibc gives it 0x0a.
So I would suggest a separate position, for example bit 3, spelled so that zero is the weaker claim: set means the environment hands the program the byte the return key produces and does not translate it. In openkal-linux, mode_of reports it when ICRNL is clear, and kal_terminal_set_mode clears ICRNL when it is requested and sets it again when it is not, under the existing rule that a position already at the requested value is not written. The constant goes next to t_ixon in src/sys.h (0000400 on Linux). openkal-macos needs the same mapping with Darwin's value. In openkal-musl, okm_termios_set requests the position when c_iflag has ICRNL clear and okm_termios_get reports ICRNL from it, so the two calls map one to one onto termios. Against a backend that does not know the position the request is ignored, the read-back is zero and the port reports ICRNL as set, which is true. A program that clears ISIG alone is unaffected, and one that clears ICRNL alone gets what it asked for. GNU readline is of the second kind: it clears ICRNL and INLCR and leaves ISIG on (prepare_terminal_settings in rltty.c, readline 8.2). INLCR and IGNCR, which cfmakeraw also clears, are off by default on a pty, which is why ICRNL is the only one that shows here. A backend that wants the position to be exact would clear them as well.
The cost is a specification revision and work in every backend and in both directions of the port. The same entry declined a position for each class of reserved keystroke. I read this as a different question, since the environment rewrites a byte and keeps no key. I have not looked at what the Windows console delivers for Enter with line input off, so I cannot say what that backend would report, and the review of #37 marks its Windows column n/a for this row.
If the answer is that ICRNL stays out of reach, saying so would already help. The README row and the tcsetattr note could name the input flags, and examples/terminal could print icrnl, so that the next reader finds it before the byte.
Doing this in the port alone is not possible. The port reaches the terminal only through kal_terminal_set_mode, and once ICRNL has run, Enter and Ctrl+J are the same byte, so nothing above the kernel can tell them apart again. Output post-processing (OPOST/ONLCR) is left out on purpose. It is the output side, and the README row already lists it as not carried.
Activity