mirror of
https://github.com/hanzoai/visor.git
synced 2026-09-24 17:04:14 +00:00
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:
+1
-12
@@ -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.
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user