Skip to content

Commit 3597742

Browse files
committed
v1.2.7
Temporarily disable the caller's local terminal ECHO while a PTY-backed child runs so typed passwords are not visible on the host terminal. Adds makeRaw/restoreTerminal hooks and OS-specific setEcho implementations (Linux/BSD) and uses them in the PTY startup path. Includes unit tests (TestExecute_SetsHostTerminalRaw and PTY simulation) and updates docs, release notes, changelog, and the version bump to v1.2.7. Also includes small lint/quality fixes referenced in the changelog.
1 parent 681d463 commit 3597742

File tree

10 files changed

+158
-1
lines changed

10 files changed

+158
-1
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,16 @@
33

44
All notable changes to this project will be documented in this file.
55

6+
## v1.2.7 - 2026-02-19
7+
8+
- **Bugfix (Executor/TUI):** Prevent passwords from being visible when running interactive commands (e.g., `sudo`). When the executor runs a child in hybrid PTY mode we now temporarily disable *local echo* on the caller's terminal so typed passwords are not echoed back to the host terminal. Only the local ECHO flag is toggled (other terminal output processing is preserved) to avoid changing how child output is rendered.
9+
- Added `makeRaw`/`restoreTerminal` hooks and OS-specific `setEcho` helpers to safely toggle host echo and make the behavior testable.
10+
- Added `TestExecute_SetsHostTerminalRaw` and PTY-simulation helpers to prevent regressions.
11+
- **Quality:** Fixed revive lint warnings and kept `gocyclo` under the configured threshold; all `golangci-lint`, `gocyclo`, and unit tests pass locally.
12+
- **Docs:** Updated `docs/executor.md` and `docs/tui.md` to document the host-terminal echo behavior during interactive PTY-backed runs.
13+
- **Version:** Bump to `v1.2.7`.
14+
15+
616
## v1.2.6 - 2026-02-17
717

818
- **Bugfix (Registry):** Fix rollback creating duplicate version entries — rolling back to a previous version was producing both an "update" and a "rollback" version record because `ApplyVersionByName` called `ReplaceCommands` (which records an "update") and then separately recorded a "rollback". Now uses a single transaction with `replaceCommandsTx` + `recordVersionTx` so only one "rollback" version is created.

RELEASE_NOTES/GITHUB_v1.2.7.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# v1.2.7 - 2026-02-19
2+
3+
This patch prevents passwords from being visible when running interactive commands (for example `sudo`) via `krnr` or the TUI by disabling local echo on the host terminal while a PTY-backed child is running.
4+
5+
## Bug fixes
6+
7+
### Hide password input during interactive runs
8+
Interactive programs (such as `sudo`) already read from the child's PTY, but the host terminal could locally echo typed keystrokes — which made passwords visible to observers of the host terminal.
9+
10+
**Fix:** temporarily disable the host terminal's ECHO flag while the PTY-backed child runs and restore the previous terminal state afterwards. The change only toggles local echo (preserves output post-processing) to avoid affecting child output rendering.
11+
12+
## Tests & docs
13+
- Unit tests added to validate host-terminal echo toggling in PTY scenarios.
14+
- Documentation updated to mention the behavior in `docs/executor.md` and `docs/tui.md`.
15+
16+
## Upgrade notes
17+
No DB or user-facing CLI changes required — upgrade to v1.2.7 to get the fix.

docs/executor.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ Hybrid PTY mode:
2424
be answered while keeping output simple and viewport-friendly.
2525
- PTY output written to `/dev/tty` by the child (e.g., password prompts) is read
2626
from the PTY master and forwarded to the caller's stdout.
27+
- While a PTY-backed child is running, the executor temporarily disables the host
28+
terminal's local echo so password input typed by the user is not visible on
29+
the host terminal. Only the local echo flag is toggled (output post-processing
30+
remains unchanged) to avoid affecting how child output is rendered.
2731

2832
Notes:
2933
- By default, `Shell` is empty and the OS default shell is used. Set `Shell` to

docs/releases/v1.2.7.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# v1.2.7 - 2026-02-19
2+
3+
This patch fixes an issue where passwords typed into interactive child processes (for example `sudo`) could be visible in the host terminal when running commands via `krnr`/TUI.
4+
5+
## Bug fixes
6+
7+
### Prevent passwords from being echoed locally
8+
When the executor runs a child in hybrid PTY mode the child already has a proper `/dev/tty`, but the host terminal could still echo typed characters locally — causing passwords to be visible to someone watching the host terminal.
9+
10+
**Fix:** while a PTY-backed child runs we temporarily disable *local echo* on the caller's terminal and restore the prior terminal state afterward. Only the local ECHO flag is toggled so output post-processing and rendering remain unchanged (no more visual glitches).
11+
12+
**Tests:** added unit tests that simulate terminal-like stdin and verify host-terminal echo toggling.
13+
14+
## Tests & Quality
15+
- Added `TestExecute_SetsHostTerminalRaw` and supporting test hooks for terminal-mode behavior.
16+
- Addressed small linter/test hygiene issues; `golangci-lint`, `gocyclo`, and unit tests pass locally.
17+
18+
## Docs
19+
- Updated `docs/executor.md` and `docs/tui.md` to document the host-terminal echo behavior during interactive runs.
20+
21+
## Upgrade notes
22+
- No DB schema changes. Users should simply upgrade to v1.2.7.

docs/tui.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ Sanitizing run output
5555
Interactive commands & hybrid PTY
5656
- The TUI supports running interactive commands that require user input (e.g., `sudo` password prompts, `pacman` confirmations). When a run is in progress, typed keys are forwarded to the process stdin.
5757
- The executor uses a **hybrid PTY** approach: stdin and the controlling terminal use a PTY so programs that read from `/dev/tty` work, while stdout/stderr remain as pipes for viewport-friendly output.
58+
- While a PTY-backed child runs, the host terminal's local echo is temporarily disabled so password input is not visible to observers of the host terminal; the TUI forwards keystrokes into the process while preserving how output renders in the viewport.
5859
- All prompts and output appear inside the **run output panel** (viewport), not in the footer or bottom bar.
5960
- Output streams live — no keypress required to see results.
6061

internal/executor/executor_pty_simulated_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
"io"
99
"os/exec"
1010
"testing"
11+
12+
"golang.org/x/term"
1113
)
1214

1315
// fakeReader simulates an io.Reader that also exposes a file descriptor.
@@ -49,3 +51,36 @@ func TestExecute_PTYSimulated(t *testing.T) {
4951
t.Fatalf("expected prompt streamed to stdout, got: %q", out.String())
5052
}
5153
}
54+
55+
func TestExecute_SetsHostTerminalRaw(t *testing.T) {
56+
// Ensure we call makeRaw/restoreTerminal when stdin is a terminal-like FD.
57+
origIsTerminal := isTerminal
58+
origMakeRaw := makeRaw
59+
origRestore := restoreTerminal
60+
defer func() { isTerminal = origIsTerminal; makeRaw = origMakeRaw; restoreTerminal = origRestore }()
61+
62+
isTerminal = func(fd uintptr) bool { return fd == 0xdead }
63+
64+
calledMake := false
65+
calledRestore := false
66+
makeRaw = func(_ int) (*term.State, error) {
67+
calledMake = true
68+
return &term.State{}, nil
69+
}
70+
restoreTerminal = func(_ int, _ *term.State) error {
71+
calledRestore = true
72+
return nil
73+
}
74+
75+
ctx := context.Background()
76+
e := &Executor{}
77+
var out bytes.Buffer
78+
var errb bytes.Buffer
79+
stdin := &fakeReader{fd: 0xdead}
80+
if err := e.Execute(ctx, "true", "", stdin, &out, &errb); err != nil {
81+
t.Fatalf("Execute failed: %v", err)
82+
}
83+
if !calledMake || !calledRestore {
84+
t.Fatalf("expected makeRaw and restoreTerminal to be called; got make=%v restore=%v", calledMake, calledRestore)
85+
}
86+
}

internal/executor/pty_unix.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,24 @@ var isTerminal = func(fd uintptr) bool {
1919
return term.IsTerminal(int(fd))
2020
}
2121

22+
// makeRaw/restoreTerminal wrap terminal mode changes so tests can override
23+
// them. Default behavior: save the current terminal state and *disable local
24+
// echo only* (do not flip other flags such as OPOST) so the host terminal
25+
// does not display typed passwords but output post-processing stays intact.
26+
// Tests can still override these hooks.
27+
var makeRaw = func(fd int) (*term.State, error) {
28+
oldState, err := term.GetState(fd)
29+
if err != nil {
30+
return nil, err
31+
}
32+
// disable local echo only
33+
if err := setEcho(fd, false); err != nil {
34+
return nil, err
35+
}
36+
return oldState, nil
37+
}
38+
var restoreTerminal = func(fd int, state *term.State) error { return term.Restore(fd, state) }
39+
2240
// ptyStarter encapsulates starting a command with a hybrid PTY setup.
2341
// The child's stdin and controlling terminal use a PTY so programs like
2442
// sudo that open /dev/tty work correctly. Stdout and stderr remain as
@@ -58,6 +76,18 @@ var ptyStarter = func(cmd *exec.Cmd, stdin io.Reader, stdout, stderr io.Writer)
5876
}
5977
_ = pts.Close() // child has its own copy; close ours
6078

79+
// If the caller's stdin is a terminal, put the *caller* terminal into
80+
// raw mode while the child runs. This prevents the host terminal from
81+
// locally echoing keystrokes (so password entry remains hidden) while
82+
// we forward input into the child's PTY master.
83+
if f, ok := stdin.(interface{ Fd() uintptr }); ok {
84+
if isTerminal(f.Fd()) {
85+
if oldState, err := makeRaw(int(f.Fd())); err == nil {
86+
defer func() { _ = restoreTerminal(int(f.Fd()), oldState) }()
87+
}
88+
}
89+
}
90+
6191
// Forward user input from the caller's reader into the PTY master
6292
// so interactive prompts (sudo password, etc.) receive keystrokes.
6393
go func() { _, _ = io.Copy(ptmx, stdin) }()
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
//go:build darwin || freebsd || netbsd || openbsd || dragonfly
2+
3+
package executor
4+
5+
import "golang.org/x/sys/unix"
6+
7+
// setEcho toggles the ECHO bit on the terminal referenced by fd (BSD / macOS).
8+
func setEcho(fd int, enabled bool) error {
9+
t, err := unix.IoctlGetTermios(fd, unix.TIOCGETA)
10+
if err != nil {
11+
return err
12+
}
13+
if enabled {
14+
t.Lflag |= unix.ECHO
15+
} else {
16+
t.Lflag &^= unix.ECHO
17+
}
18+
return unix.IoctlSetTermios(fd, unix.TIOCSETA, t)
19+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
//go:build linux || android
2+
3+
package executor
4+
5+
import "golang.org/x/sys/unix"
6+
7+
// setEcho toggles the ECHO bit on the terminal referenced by fd (Linux).
8+
func setEcho(fd int, enabled bool) error {
9+
t, err := unix.IoctlGetTermios(fd, unix.TCGETS)
10+
if err != nil {
11+
return err
12+
}
13+
if enabled {
14+
t.Lflag |= unix.ECHO
15+
} else {
16+
t.Lflag &^= unix.ECHO
17+
}
18+
return unix.IoctlSetTermios(fd, unix.TCSETS, t)
19+
}

internal/version/version.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ package version
33

44
// Version is set at build time via -ldflags "-X github.com/VoxDroid/krnr/internal/version.Version=<value>"
55
// The default is a development placeholder.
6-
var Version = "v1.2.6"
6+
var Version = "v1.2.7"

0 commit comments

Comments
 (0)