Fix SIGWINCH delivery on PTY window size change (TIOCSWINSZ)

When SIGWINCH causes the window size to change, signal the replica's foreground
process group. This is similar to what Linux does.

Also implement TIOCSCTTY/TIOCNOTTY and SIGWINCH delivery on TIOCSWINSZ
for host-imported TTYs, which previously did not support controlling
terminals or window size change signals.

Add syscall tests in pty.cc and host_pty.cc to verify the behavior.

Co-authored-by: Skywalkr-dev <[email protected]>

Fixes #13317

PiperOrigin-RevId: 927482382
This commit is contained in:
Ayush Ranjan
2026-06-05 14:29:26 -07:00
committed by gVisor bot
parent d98391202b
commit fbc1d79883
6 changed files with 302 additions and 19 deletions
+1 -12
View File
@@ -28,6 +28,7 @@ const (
// Winsize is struct winsize, defined in uapi/asm-generic/termios.h.
//
// +stateify savable
// +marshal
type Winsize struct {
_ structs.HostLayout
@@ -344,15 +345,3 @@ var DefaultReplicaTermios = KernelTermios{
InputSpeed: 38400,
OutputSpeed: 38400,
}
// WindowSize corresponds to struct winsize defined in
// include/uapi/asm-generic/termios.h.
//
// +stateify savable
// +marshal
type WindowSize struct {
_ structs.HostLayout
Rows uint16
Cols uint16
_ [4]byte // Padding for 2 unused shorts.
}
+9 -2
View File
@@ -89,7 +89,7 @@ type lineDiscipline struct {
sizeMu sync.Mutex `state:"nosave"`
// size is the terminal size (width and height).
size linux.WindowSize
size linux.Winsize
// inQueue is the input queue of the terminal.
inQueue queue
@@ -232,8 +232,15 @@ func (l *lineDiscipline) windowSize(t *kernel.Task, args arch.SyscallArguments)
func (l *lineDiscipline) setWindowSize(t *kernel.Task, args arch.SyscallArguments) error {
l.sizeMu.Lock()
defer l.sizeMu.Unlock()
oldSize := l.size
_, err := l.size.CopyIn(t, args[2].Pointer())
return err
if err != nil {
return err
}
if oldSize != l.size {
l.terminal.replicaKTTY.SignalForegroundProcessGroup(kernel.SignalInfoPriv(linux.SIGWINCH))
}
return nil
}
func (l *lineDiscipline) masterReadiness() waiter.EventMask {
+21 -4
View File
@@ -246,8 +246,27 @@ func (t *TTYFileDescription) Ioctl(ctx context.Context, io usermem.IO, sysno uin
if _, err := winsize.CopyIn(task, args[2].Pointer()); err != nil {
return 0, err
}
err := ioctlSetWinsize(fd, &winsize)
return 0, err
oldSize, err := ioctlGetWinsize(fd)
if err != nil {
return 0, err
}
if err := ioctlSetWinsize(fd, &winsize); err != nil {
return 0, err
}
if *oldSize != winsize {
t.TTY().SignalForegroundProcessGroup(kernel.SignalInfoPriv(linux.SIGWINCH))
}
return 0, nil
case linux.TIOCSCTTY:
// Make the given terminal the controlling terminal of the
// calling process.
steal := args[2].Int() == 1
return 0, task.ThreadGroup().SetControllingTTY(ctx, t.TTY(), steal, t.vfsfd.IsReadable())
case linux.TIOCNOTTY:
// Release this process's controlling terminal.
return 0, task.ThreadGroup().ReleaseControllingTTY(t.TTY())
// Unimplemented commands.
case linux.TIOCSETD,
@@ -261,8 +280,6 @@ func (t *TTYFileDescription) Ioctl(ctx context.Context, io usermem.IO, sysno uin
linux.TIOCEXCL,
linux.TIOCNXCL,
linux.TIOCGEXCL,
linux.TIOCNOTTY,
linux.TIOCSCTTY,
linux.TIOCGSID,
linux.TIOCGETD,
linux.TIOCVHANGUP,
+1
View File
@@ -4961,6 +4961,7 @@ cc_binary(
linkstatic = 1,
malloc = "//test/util:errno_safe_allocator",
deps = select_gtest() + [
"//test/util:logging",
"//test/util:pty_util",
"//test/util:test_main",
"//test/util:test_util",
+143 -1
View File
@@ -12,13 +12,20 @@
// See the License for the specific language governing permissions and
// limitations under the License.
#include <signal.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <termios.h>
#include <unistd.h>
#include <ctime>
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "absl/strings/numbers.h"
#include "test/util/logging.h"
#include "test/util/pty_util.h"
#include "test/util/test_util.h"
@@ -63,7 +70,142 @@ TEST(HostPtyTest, Termios2) {
ASSERT_THAT(ioctl(fd, TCSETS2, &t2), SyscallSucceeds());
}
} // namespace
TEST(HostPtyTest, SigwinchOnWindowSizeChange) {
char* fd_str = getenv("TEST_HOST_PTY_FD");
ASSERT_NE(fd_str, nullptr) << "TEST_HOST_PTY_FD environment variable not set";
int fd;
ASSERT_TRUE(absl::SimpleAtoi(fd_str, &fd))
<< "Invalid TEST_HOST_PTY_FD: " << fd_str;
int sync_pipe[2];
ASSERT_THAT(pipe(sync_pipe), SyscallSucceeds());
pid_t child = fork();
if (child == 0) {
close(sync_pipe[0]);
// Create new session and set the imported TTY as controlling terminal.
TEST_PCHECK(setsid() >= 0);
// Try TIOCSCTTY with steal=0, if it fails, try with steal=1. This is
// necessary because the host TTY might already be the controlling
// terminal of a process on the host.
if (ioctl(fd, TIOCSCTTY, 0) < 0) {
TEST_PCHECK(ioctl(fd, TIOCSCTTY, 1) >= 0);
}
// Block SIGWINCH.
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGWINCH);
sigset_t old_set;
TEST_PCHECK(sigprocmask(SIG_BLOCK, &set, &old_set) == 0);
// Notify parent we are ready.
char c = 'r';
TEST_PCHECK(WriteFd(sync_pipe[1], &c, 1) == 1);
// Wait for SIGWINCH.
struct timespec timeout = {};
timeout.tv_sec = 10;
int sig = RetryEINTR(sigtimedwait)(&set, nullptr, &timeout);
if (sig != SIGWINCH) {
_exit(1); // Failed to receive SIGWINCH.
}
// Notify parent we got it.
c = '1';
TEST_PCHECK(WriteFd(sync_pipe[1], &c, 1) == 1);
// Now wait for another SIGWINCH.
sig = RetryEINTR(sigtimedwait)(&set, nullptr, &timeout);
if (sig != SIGWINCH) {
_exit(2); // Failed to receive second SIGWINCH.
}
// Notify parent we got it.
c = '2';
TEST_PCHECK(WriteFd(sync_pipe[1], &c, 1) == 1);
// Now expect NO SIGWINCH if we set the same size.
timeout.tv_sec = 2;
sig = RetryEINTR(sigtimedwait)(&set, nullptr, &timeout);
if (sig == SIGWINCH) {
_exit(3); // Unexpected SIGWINCH.
}
// Notify parent.
c = '3';
TEST_PCHECK(WriteFd(sync_pipe[1], &c, 1) == 1);
// Test TIOCSWINSZ from replica (self-signaling).
struct winsize ws = {};
TEST_PCHECK(ioctl(fd, TIOCGWINSZ, &ws) == 0);
ws.ws_row++;
ws.ws_col++;
TEST_PCHECK(ioctl(fd, TIOCSWINSZ, &ws) == 0);
timeout.tv_sec = 10;
sig = RetryEINTR(sigtimedwait)(&set, nullptr, &timeout);
if (sig != SIGWINCH) {
_exit(4); // Failed to receive SIGWINCH after replica TIOCSWINSZ.
}
// Notify parent.
c = '4';
TEST_PCHECK(WriteFd(sync_pipe[1], &c, 1) == 1);
close(sync_pipe[1]);
_exit(42); // Success.
}
ASSERT_GT(child, 0);
close(sync_pipe[1]);
// Wait for child to be ready.
char c;
ASSERT_THAT(ReadFd(sync_pipe[0], &c, 1), SyscallSucceedsWithValue(1));
EXPECT_EQ(c, 'r');
// Get current size.
struct winsize ws = {};
ASSERT_THAT(ioctl(fd, TIOCGWINSZ, &ws), SyscallSucceeds());
// 1. Change size.
ws.ws_row++;
ws.ws_col++;
ASSERT_THAT(ioctl(fd, TIOCSWINSZ, &ws), SyscallSucceeds());
// Wait for child.
ASSERT_THAT(ReadFd(sync_pipe[0], &c, 1), SyscallSucceedsWithValue(1));
EXPECT_EQ(c, '1');
// 2. Change size again.
ws.ws_row++;
ws.ws_col++;
ASSERT_THAT(ioctl(fd, TIOCSWINSZ, &ws), SyscallSucceeds());
// Wait for child.
ASSERT_THAT(ReadFd(sync_pipe[0], &c, 1), SyscallSucceedsWithValue(1));
EXPECT_EQ(c, '2');
// 3. Set same size.
ASSERT_THAT(ioctl(fd, TIOCSWINSZ, &ws), SyscallSucceeds());
// Wait for child.
ASSERT_THAT(ReadFd(sync_pipe[0], &c, 1), SyscallSucceedsWithValue(1));
EXPECT_EQ(c, '3');
// Wait for child to perform self-signaling test.
ASSERT_THAT(ReadFd(sync_pipe[0], &c, 1), SyscallSucceedsWithValue(1));
EXPECT_EQ(c, '4');
close(sync_pipe[0]);
// Wait for child.
int wstatus;
ASSERT_THAT(waitpid(child, &wstatus, 0), SyscallSucceedsWithValue(child));
ASSERT_TRUE(WIFEXITED(wstatus));
EXPECT_EQ(WEXITSTATUS(wstatus), 42);
}
} // namespace
} // namespace testing
} // namespace gvisor
+127
View File
@@ -32,6 +32,7 @@
#include <unistd.h>
#include <csignal>
#include <ctime>
#include <iostream>
#include <string>
@@ -2512,6 +2513,132 @@ TEST_F(JobControlTest, SIGHUPOnMasterClose) {
EXPECT_EQ(WEXITSTATUS(wstatus), 42);
}
TEST_F(JobControlTest, SigwinchOnWindowSizeChange) {
int sync_pipe[2];
ASSERT_THAT(pipe(sync_pipe), SyscallSucceeds());
pid_t child = fork();
if (child == 0) {
close(sync_pipe[0]);
// Close inherited master.
master_.reset();
// Create new session and set replica as controlling terminal.
TEST_PCHECK(setsid() >= 0);
TEST_PCHECK(ioctl(replica_.get(), TIOCSCTTY, 0) >= 0);
// Block SIGWINCH.
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGWINCH);
sigset_t old_set;
TEST_PCHECK(sigprocmask(SIG_BLOCK, &set, &old_set) == 0);
// Notify parent we are ready.
char c = 'r';
TEST_PCHECK(WriteFd(sync_pipe[1], &c, 1) == 1);
// Wait for SIGWINCH (triggered by parent changing size on master).
struct timespec timeout = {};
timeout.tv_sec = 10;
int sig = RetryEINTR(sigtimedwait)(&set, nullptr, &timeout);
if (sig != SIGWINCH) {
_exit(1); // Failed to receive SIGWINCH.
}
// Notify parent we got it.
c = '1';
TEST_PCHECK(WriteFd(sync_pipe[1], &c, 1) == 1);
// Now wait for another SIGWINCH (triggered by parent changing size again).
sig = RetryEINTR(sigtimedwait)(&set, nullptr, &timeout);
if (sig != SIGWINCH) {
_exit(2); // Failed to receive second SIGWINCH.
}
// Notify parent we got it.
c = '2';
TEST_PCHECK(WriteFd(sync_pipe[1], &c, 1) == 1);
// Now expect NO SIGWINCH if we set the same size.
timeout.tv_sec = 2;
sig = RetryEINTR(sigtimedwait)(&set, nullptr, &timeout);
if (sig == SIGWINCH) {
_exit(3); // Unexpected SIGWINCH.
}
// Notify parent.
c = '3';
TEST_PCHECK(WriteFd(sync_pipe[1], &c, 1) == 1);
// Test TIOCSWINSZ from replica (self-signaling).
struct winsize ws = {};
TEST_PCHECK(ioctl(replica_.get(), TIOCGWINSZ, &ws) == 0);
ws.ws_row++;
ws.ws_col++;
TEST_PCHECK(ioctl(replica_.get(), TIOCSWINSZ, &ws) == 0);
timeout.tv_sec = 10;
sig = RetryEINTR(sigtimedwait)(&set, nullptr, &timeout);
if (sig != SIGWINCH) {
_exit(4); // Failed to receive SIGWINCH after replica TIOCSWINSZ.
}
// Notify parent.
c = '4';
TEST_PCHECK(WriteFd(sync_pipe[1], &c, 1) == 1);
close(sync_pipe[1]);
_exit(42); // Success.
}
ASSERT_GT(child, 0);
close(sync_pipe[1]);
// Wait for child to be ready.
char c;
ASSERT_THAT(ReadFd(sync_pipe[0], &c, 1), SyscallSucceedsWithValue(1));
EXPECT_EQ(c, 'r');
// 1. Change size on master.
struct winsize ws = {};
ASSERT_THAT(ioctl(master_.get(), TIOCGWINSZ, &ws), SyscallSucceeds());
ws.ws_row++;
ws.ws_col++;
ASSERT_THAT(ioctl(master_.get(), TIOCSWINSZ, &ws), SyscallSucceeds());
// Wait for child to receive it.
ASSERT_THAT(ReadFd(sync_pipe[0], &c, 1), SyscallSucceedsWithValue(1));
EXPECT_EQ(c, '1');
// 2. Change size on master again.
ws.ws_row++;
ws.ws_col++;
ASSERT_THAT(ioctl(master_.get(), TIOCSWINSZ, &ws), SyscallSucceeds());
// Wait for child to receive it.
ASSERT_THAT(ReadFd(sync_pipe[0], &c, 1), SyscallSucceedsWithValue(1));
EXPECT_EQ(c, '2');
// 3. Set same size on master.
ASSERT_THAT(ioctl(master_.get(), TIOCSWINSZ, &ws), SyscallSucceeds());
// Wait for child to finish waiting.
ASSERT_THAT(ReadFd(sync_pipe[0], &c, 1), SyscallSucceedsWithValue(1));
EXPECT_EQ(c, '3');
// Wait for child to perform self-signaling test.
ASSERT_THAT(ReadFd(sync_pipe[0], &c, 1), SyscallSucceedsWithValue(1));
EXPECT_EQ(c, '4');
close(sync_pipe[0]);
// Wait for child.
int wstatus;
ASSERT_THAT(waitpid(child, &wstatus, 0), SyscallSucceedsWithValue(child));
ASSERT_TRUE(WIFEXITED(wstatus));
EXPECT_EQ(WEXITSTATUS(wstatus), 42);
}
} // namespace
} // namespace testing
} // namespace gvisor