exec: honor ConsoleSize so the terminal is sized at creation

The exec API accepts a ConsoleSize but it is dropped: the exec
pseudo-terminal is created at its default size and only corrected
afterwards by an asynchronous resize. A short-lived exec that reads its
window size at startup (e.g. `stty size`) can therefore observe the wrong
size, because the resize may arrive after the process has already read it.
docker applies the size at creation.

Carry the requested ConsoleSize through ExecConfig and into the exec OCI
process spec (process.consoleSize) so the runtime sizes the terminal
before the process starts, removing the race. The local and remote CLIs
capture the caller's terminal size when -t is given and pass it through
ExecOptions, matching the behavior of `podman run`.

Re-enable the previously flaky `podman exec` case in the interactive
system test, which this change makes deterministic.

Signed-off-by: Shuai Yuan <shuaiyuanzju@gmail.com>
This commit is contained in:
Shuai Yuan 2026-06-18 10:52:14 +08:00 committed by Shawn Yuan
parent d84fb8be61
commit f5efef5043
9 changed files with 63 additions and 7 deletions

View file

@ -11,6 +11,7 @@ import (
"github.com/spf13/cobra"
"go.podman.io/common/pkg/completion"
"go.podman.io/common/pkg/resize"
"go.podman.io/podman/v6/cmd/podman/common"
"go.podman.io/podman/v6/cmd/podman/registry"
"go.podman.io/podman/v6/cmd/podman/validate"
@ -18,6 +19,7 @@ import (
"go.podman.io/podman/v6/pkg/domain/entities"
envLib "go.podman.io/podman/v6/pkg/env"
"go.podman.io/podman/v6/pkg/rootless"
"golang.org/x/term"
)
var (
@ -189,6 +191,16 @@ func exec(cmd *cobra.Command, args []string) error {
streams.AttachOutput = true
streams.AttachError = true
// When allocating a TTY, capture the current terminal size up front so the
// exec pseudo-terminal is created at the right size, rather than relying on
// the asynchronous resize that follows attach. This matters for short-lived
// commands that read their window size at startup (e.g. `stty size`).
if execOpts.Tty {
if w, h, err := term.GetSize(int(os.Stdin.Fd())); err == nil {
execOpts.ConsoleSize = &resize.TerminalSize{Width: uint16(w), Height: uint16(h)}
}
}
if execNoSession {
exitCode, err := registry.ContainerEngine().ContainerExecNoSession(registry.Context(), nameOrID, execOpts, streams)
registry.SetExitCode(exitCode)

View file

@ -29,6 +29,12 @@ type ExecConfig struct {
Command []string `json:"command"`
// Terminal is whether the exec session will allocate a pseudoterminal.
Terminal bool `json:"terminal,omitempty"`
// ConsoleSize is an optional initial size for the exec session's
// pseudoterminal, applied at creation when Terminal is true. This lets a
// one-shot exec read the correct window size immediately instead of
// relying on an asynchronous resize that may arrive after the process has
// already started.
ConsoleSize *resize.TerminalSize `json:"consoleSize,omitempty"`
// AttachStdin is whether the STDIN stream will be forwarded to the exec
// session's first process when attaching. Only available if Terminal is
// false.
@ -1180,6 +1186,7 @@ func prepareForExec(c *Container, session *ExecSession) (*ExecOptions, error) {
opts.ExitCommand = session.Config.ExitCommand
opts.ExitCommandDelay = session.Config.ExitCommandDelay
opts.Privileged = session.Config.Privileged
opts.ConsoleSize = session.Config.ConsoleSize
return opts, nil
}

View file

@ -201,6 +201,9 @@ type ExecOptions struct {
Env map[string]string
// Terminal is whether to create a new TTY for the exec session.
Terminal bool
// ConsoleSize is an optional initial size for the exec session's TTY,
// applied at creation when Terminal is true.
ConsoleSize *resize.TerminalSize
// Cwd is the working directory for the executed command. If unset, the
// working directory of the container will be used.
Cwd string

View file

@ -704,6 +704,16 @@ func (c *Container) prepareProcessExec(options *ExecOptions, env []string, sessi
pspec.Terminal = false
if options.Terminal {
pspec.Terminal = true
// Size the exec PTY at creation so the process reads the correct
// window size immediately. Without this the PTY starts at 0x0 and is
// only corrected by an asynchronous resize, which can arrive after a
// one-shot process has already read its size.
if options.ConsoleSize != nil {
pspec.ConsoleSize = &spec.Box{
Height: uint(options.ConsoleSize.Height),
Width: uint(options.ConsoleSize.Width),
}
}
}
if len(env) > 0 {
pspec.Env = append(pspec.Env, env...)

View file

@ -42,6 +42,14 @@ func ExecCreateHandler(w http.ResponseWriter, r *http.Request) {
libpodConfig := new(libpod.ExecConfig)
libpodConfig.Command = input.Cmd
libpodConfig.Terminal = input.Tty
// Preserve the requested initial terminal size so the exec PTY can be sized
// at creation (docker sends ConsoleSize as [height, width]).
if input.ConsoleSize != nil {
libpodConfig.ConsoleSize = &resize.TerminalSize{
Height: uint16(input.ConsoleSize[0]),
Width: uint16(input.ConsoleSize[1]),
}
}
libpodConfig.AttachStdin = input.AttachStdin
libpodConfig.AttachStderr = input.AttachStderr
libpodConfig.AttachStdout = input.AttachStdout

View file

@ -7,6 +7,7 @@ import (
"time"
nettypes "go.podman.io/common/libnetwork/types"
"go.podman.io/common/pkg/resize"
imageTypes "go.podman.io/image/v5/types"
"go.podman.io/podman/v6/libpod/define"
"go.podman.io/podman/v6/pkg/domain/entities/types"
@ -283,6 +284,7 @@ type ContainerLogsOptions struct {
// a container
type ExecOptions struct {
Cmd []string
ConsoleSize *resize.TerminalSize
DetachKeys string
Envs map[string]string
Interactive bool

View file

@ -890,6 +890,13 @@ func makeExecConfig(options entities.ExecOptions, rt *libpod.Runtime, noSession
execConfig.PreserveFD = options.PreserveFD
execConfig.AttachStdin = options.Interactive
// Size the exec terminal at creation so a process that reads its window size
// at startup (e.g. `stty size`) observes the right value, rather than relying
// on the asynchronous resize that follows attach.
if options.Tty {
execConfig.ConsoleSize = options.ConsoleSize
}
// Only set up exit command for regular exec sessions, not no-session mode
if !noSession {
// Make an exit command

View file

@ -618,6 +618,12 @@ func makeExecConfig(options entities.ExecOptions) *handlers.ExecCreateConfig {
createConfig.Env = env
createConfig.WorkingDir = options.WorkDir
createConfig.Cmd = options.Cmd
// Size the exec terminal at creation, mirroring the local path, so the
// service does not have to rely on the asynchronous resize that follows
// attach. ConsoleSize is ordered [height, width].
if options.Tty && options.ConsoleSize != nil {
createConfig.ConsoleSize = &[2]uint{uint(options.ConsoleSize.Height), uint(options.ConsoleSize.Width)}
}
return createConfig
}

View file

@ -70,14 +70,15 @@ function teardown() {
run_podman rm -t 0 -f mystty
# FIXME: the checks below are flaking a lot (see #10710).
# The same must hold for podman exec. The exec pseudo-terminal is now sized
# at creation (honoring the requested ConsoleSize), so stty reads the right
# dimensions immediately instead of racing the asynchronous resize that
# previously followed attach (see #10710).
run_podman run -d --name mystty $IMAGE top
run_podman exec -it mystty stty size <$PODMAN_TEST_PTY
is "$output" "$rows $cols$CR" "stty under podman exec reads the correct dimensions"
# check that the same works for podman exec
# run_podman run -d --name mystty $IMAGE top
# run_podman exec -it mystty stty size <$PODMAN_TEST_PTY
# is "$output" "$rows $cols" "stty under podman exec reads the correct dimensions"
#
# run_podman rm -t 0 -f mystty
run_podman rm -t 0 -f mystty
}