Repository: firecracker-microvm/firecracker-containerd
Pull request #421 (2020-05-12 17:46:09)

Fix Exec()'s timeout issue
Signed-off-by: Kazuyoshi Kato <katokazu@amazon.com>

*Issue #, if available:*

NA

*Description of changes:*

* The first commit reproduces the issue
* The second commit fixes the issue. Opening a FIFO with O_RDWR is unnecessary and makes EOF invisible.
* The third commit fixes a timing issue. Closing all connectors by the cancellation of the passed context seems too early. We need to copy bytes even after the end of the process, since a pipe can buffer some bytes.

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.


kzys: cc @ddyzhang

Patch:
diff --git a/agent/service.go b/agent/service.go
index 6700ebf..bbacf96 100644
--- a/agent/service.go
+++ b/agent/service.go
@@ -228,7 +228,7 @@ func (ts *TaskService) Create(requestCtx context.Context, req *taskAPI.CreateTas
 			req.Stdin = fifoSet.Stdin
 			stdinConnectorPair = &vm.IOConnectorPair{
 				ReadConnector:  vm.VSockAcceptConnector(extraData.StdinPort),
-				WriteConnector: vm.FIFOConnector(fifoSet.Stdin),
+				WriteConnector: vm.WriteFIFOConnector(fifoSet.Stdin),
 			}
 		}
 
@@ -236,7 +236,7 @@ func (ts *TaskService) Create(requestCtx context.Context, req *taskAPI.CreateTas
 		if req.Stdout != "" {
 			req.Stdout = fifoSet.Stdout
 			stdoutConnectorPair = &vm.IOConnectorPair{
-				ReadConnector:  vm.FIFOConnector(fifoSet.Stdout),
+				ReadConnector:  vm.ReadFIFOConnector(fifoSet.Stdout),
 				WriteConnector: vm.VSockAcceptConnector(extraData.StdoutPort),
 			}
 		}
@@ -245,7 +245,7 @@ func (ts *TaskService) Create(requestCtx context.Context, req *taskAPI.CreateTas
 		if req.Stderr != "" {
 			req.Stderr = fifoSet.Stderr
 			stderrConnectorPair = &vm.IOConnectorPair{
-				ReadConnector:  vm.FIFOConnector(fifoSet.Stderr),
+				ReadConnector:  vm.ReadFIFOConnector(fifoSet.Stderr),
 				WriteConnector: vm.VSockAcceptConnector(extraData.StderrPort),
 			}
 		}
@@ -454,7 +454,7 @@ func (ts *TaskService) Exec(requestCtx context.Context, req *taskAPI.ExecProcess
 			req.Stdin = fifoSet.Stdin
 			stdinConnectorPair = &vm.IOConnectorPair{
 				ReadConnector:  vm.VSockAcceptConnector(extraData.StdinPort),
-				WriteConnector: vm.FIFOConnector(fifoSet.Stdin),
+				WriteConnector: vm.WriteFIFOConnector(fifoSet.Stdin),
 			}
 			ts.addCleanup(taskExecID, func() error {
 				return os.RemoveAll(req.Stdin)
@@ -465,7 +465,7 @@ func (ts *TaskService) Exec(requestCtx context.Context, req *taskAPI.ExecProcess
 		if req.Stdout != "" {
 			req.Stdout = fifoSet.Stdout
 			stdoutConnectorPair = &vm.IOConnectorPair{
-				ReadConnector:  vm.FIFOConnector(fifoSet.Stdout),
+				ReadConnector:  vm.ReadFIFOConnector(fifoSet.Stdout),
 				WriteConnector: vm.VSockAcceptConnector(extraData.StdoutPort),
 			}
 			ts.addCleanup(taskExecID, func() error {
@@ -477,7 +477,7 @@ func (ts *TaskService) Exec(requestCtx context.Context, req *taskAPI.ExecProcess
 		if req.Stderr != "" {
 			req.Stderr = fifoSet.Stderr
 			stderrConnectorPair = &vm.IOConnectorPair{
-				ReadConnector:  vm.FIFOConnector(fifoSet.Stderr),
+				ReadConnector:  vm.ReadFIFOConnector(fifoSet.Stderr),
 				WriteConnector: vm.VSockAcceptConnector(extraData.StderrPort),
 			}
 			ts.addCleanup(taskExecID, func() error {
diff --git a/internal/vm/fifo.go b/internal/vm/fifo.go
index 3fe6f98..6cf25d9 100644
--- a/internal/vm/fifo.go
+++ b/internal/vm/fifo.go
@@ -21,16 +21,16 @@ import (
 	"github.com/sirupsen/logrus"
 )
 
-// FIFOConnector adapts containerd's fifo package to the IOConnector interface
-func FIFOConnector(path string) IOConnector {
+// fifoConnector adapts containerd's fifo package to the IOConnector interface
+func fifoConnector(path string, flag int) IOConnector {
 	return func(procCtx context.Context, logger *logrus.Entry) <-chan IOConnectorResult {
 		returnCh := make(chan IOConnectorResult, 1)
 		defer close(returnCh)
 
 		// We open the FIFO synchronously to ensure that the FIFO is created (via O_CREAT) before
-		// it is passed to any task service. O_RDWR ensures that we don't block on the syscall
+		// it is passed to any task service. O_NONBLOCK ensures that we don't block on the syscall
 		// level (as documented in the fifo pkg).
-		fifo, err := fifo.OpenFifo(procCtx, path, syscall.O_CREAT|syscall.O_RDWR, 0300)
+		fifo, err := fifo.OpenFifo(procCtx, path, syscall.O_CREAT|syscall.O_NONBLOCK|flag, 0300)
 		returnCh <- IOConnectorResult{
 			ReadWriteCloser: fifo,
 			Err:             err,
@@ -39,3 +39,13 @@ func FIFOConnector(path string) IOConnector {
 		return returnCh
 	}
 }
+
+// ReadFIFOConnector returns a FIFO which is open for reading
+func ReadFIFOConnector(path string) IOConnector {
+	return fifoConnector(path, syscall.O_RDONLY)
+}
+
+// WriteFIFOConnector returns a FIFO which is open for writing
+func WriteFIFOConnector(path string) IOConnector {
+	return fifoConnector(path, syscall.O_WRONLY)
+}
diff --git a/internal/vm/ioproxy.go b/internal/vm/ioproxy.go
index ddc50b0..c267501 100644
--- a/internal/vm/ioproxy.go
+++ b/internal/vm/ioproxy.go
@@ -67,20 +67,23 @@ type IOConnectorPair struct {
 }
 
 func (connectorPair *IOConnectorPair) proxy(
-	proc *vmProc,
+	ctx context.Context,
 	logger *logrus.Entry,
 	timeoutAfterExit time.Duration,
 ) (ioInitDone <-chan error, ioCopyDone <-chan error) {
 	initDone := make(chan error, 2)
 	copyDone := make(chan error)
 
+	ioCtx, ioCancel := context.WithCancel(context.Background())
+
 	// Start the initialization process. Any synchronous setup made by the connectors will
 	// be completed after these lines. Async setup will be done once initDone is closed in
 	// the goroutine below.
-	readerResultCh := connectorPair.ReadConnector(proc.ctx, logger.WithField("direction", "read"))
-	writerResultCh := connectorPair.WriteConnector(proc.ctx, logger.WithField("direction", "write"))
+	readerResultCh := connectorPair.ReadConnector(ioCtx, logger.WithField("direction", "read"))
+	writerResultCh := connectorPair.WriteConnector(ioCtx, logger.WithField("direction", "write"))
 
 	go func() {
+		defer ioCancel()
 		defer close(copyDone)
 
 		var reader io.ReadCloser
@@ -119,7 +122,7 @@ func (connectorPair *IOConnectorPair) proxy(
 		// If the io streams close on their own before the timeout, the Close calls here
 		// should just be no-ops.
 		go func() {
-			<-proc.ctx.Done()
+			<-ctx.Done()
 			time.AfterFunc(timeoutAfterExit, func() {
 				logClose(logger, reader, writer)
 			})
@@ -128,7 +131,8 @@ func (connectorPair *IOConnectorPair) proxy(
 		logger.Debug("begin copying io")
 		defer logger.Debug("end copying io")
 
-		_, err := io.CopyBuffer(writer, reader, make([]byte, internal.DefaultBufferSize))
+		size, err := io.CopyBuffer(writer, reader, make([]byte, internal.DefaultBufferSize))
+		logger.Debugf("copied %d", size)
 		if err != nil {
 			if strings.Contains(err.Error(), "use of closed network connection") ||
 				strings.Contains(err.Error(), "file already closed") {
@@ -138,6 +142,7 @@ func (connectorPair *IOConnectorPair) proxy(
 			}
 			copyDone <- err
 		}
+		defer logClose(logger, reader, writer)
 	}()
 
 	return initDone, copyDone
@@ -172,19 +177,20 @@ func (ioConnectorSet *ioConnectorSet) start(proc *vmProc) (ioInitDone <-chan err
 	if ioConnectorSet.stdin != nil {
 		// For Stdin only, provide 0 as the timeout to wait after the proc exits before closing IO streams.
 		// There's no reason to send stdin data to a proc that's already dead.
-		waitErrs(ioConnectorSet.stdin.proxy(proc, proc.logger.WithField("stream", "stdin"), 0))
+		waitErrs(ioConnectorSet.stdin.proxy(proc.ctx, proc.logger.WithField("stream", "stdin"), 0))
+
 	} else {
 		proc.logger.Debug("skipping proxy io for unset stdin")
 	}
 
 	if ioConnectorSet.stdout != nil {
-		waitErrs(ioConnectorSet.stdout.proxy(proc, proc.logger.WithField("stream", "stdout"), defaultIOFlushTimeout))
+		waitErrs(ioConnectorSet.stdout.proxy(proc.ctx, proc.logger.WithField("stream", "stdout"), defaultIOFlushTimeout))
 	} else {
 		proc.logger.Debug("skipping proxy io for unset stdout")
 	}
 
 	if ioConnectorSet.stderr != nil {
-		waitErrs(ioConnectorSet.stderr.proxy(proc, proc.logger.WithField("stream", "stderr"), defaultIOFlushTimeout))
+		waitErrs(ioConnectorSet.stderr.proxy(proc.ctx, proc.logger.WithField("stream", "stderr"), defaultIOFlushTimeout))
 	} else {
 		proc.logger.Debug("skipping proxy io for unset stderr")
 	}
diff --git a/proto/firecracker.pb.go b/proto/firecracker.pb.go
index ef7db13..41f796f 100644
--- a/proto/firecracker.pb.go
+++ b/proto/firecracker.pb.go
@@ -552,15 +552,9 @@ type JailerConfig struct {
 	//
 	// This is formatted as specified in the cpuset man page under "List Format"
 	// http://man7.org/linux/man-pages/man7/cpuset.7.html
-	Mems string `protobuf:"bytes,3,opt,name=Mems,json=mems,proto3" json:"Mems,omitempty"`
-	UID  uint32 `protobuf:"varint,4,opt,name=UID,json=uID,proto3" json:"UID,omitempty"`
-	GID  uint32 `protobuf:"varint,5,opt,name=GID,json=gID,proto3" json:"GID,omitempty"`
-	// CgroupPath is used to dictate where the cgroup should be located
-	// relative to the cgroup directory which is
-	// /sys/fs/cgroup/cpu/<CgroupPath>/<vmID>
-	// if no value was provided, then /firecracker-containerd will be used as
-	// the default value
-	CgroupPath           string   `protobuf:"bytes,6,opt,name=CgroupPath,json=cgroupPath,proto3" json:"CgroupPath,omitempty"`
+	Mems                 string   `protobuf:"bytes,3,opt,name=Mems,json=mems,proto3" json:"Mems,omitempty"`
+	UID                  uint32   `protobuf:"varint,4,opt,name=UID,json=uID,proto3" json:"UID,omitempty"`
+	GID                  uint32   `protobuf:"varint,5,opt,name=GID,json=gID,proto3" json:"GID,omitempty"`
 	XXX_NoUnkeyedLiteral struct{} `json:"-"`
 	XXX_unrecognized     []byte   `json:"-"`
 	XXX_sizecache        int32    `json:"-"`
@@ -625,13 +619,6 @@ func (m *JailerConfig) GetGID() uint32 {
 	return 0
 }
 
-func (m *JailerConfig) GetCgroupPath() string {
-	if m != nil {
-		return m.CgroupPath
-	}
-	return ""
-}
-
 func init() {
 	proto.RegisterType((*CreateVMRequest)(nil), "CreateVMRequest")
 	proto.RegisterType((*CreateVMResponse)(nil), "CreateVMResponse")
@@ -648,43 +635,43 @@ func init() {
 func init() { proto.RegisterFile("firecracker.proto", fileDescriptor_a73317e9fb8da571) }
 
 var fileDescriptor_a73317e9fb8da571 = []byte{
-	// 603 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xd4, 0x54, 0xc1, 0x6e, 0xd3, 0x40,
-	0x10, 0x95, 0x9b, 0xa4, 0x4d, 0xc6, 0x4d, 0xd2, 0xac, 0x5a, 0x58, 0x55, 0xa8, 0xb2, 0x7c, 0xa8,
-	0x22, 0x0e, 0x95, 0x68, 0x2f, 0x88, 0x13, 0x25, 0xa6, 0x95, 0x5b, 0x5c, 0x55, 0x9b, 0xa6, 0x07,
-	0x6e, 0x8b, 0x33, 0x76, 0x4d, 0xe2, 0xdd, 0xb0, 0xbb, 0x29, 0x70, 0xe6, 0x07, 0xf8, 0x16, 0xf8,
-	0x41, 0xe4, 0x4d, 0x9a, 0x38, 0x11, 0x54, 0x48, 0x9c, 0x38, 0x65, 0xe7, 0xcd, 0x9b, 0x99, 0xa7,
-	0x79, 0x13, 0x43, 0x27, 0xc9, 0x14, 0xc6, 0x8a, 0xc7, 0x23, 0x54, 0x47, 0x13, 0x25, 0x8d, 0xdc,
-	0x77, 0xcd, 0xd7, 0x09, 0xea, 0x59, 0xe0, 0x7f, 0xab, 0x42, 0xbb, 0xa7, 0x90, 0x1b, 0xbc, 0x8d,
-	0x18, 0x7e, 0x9a, 0xa2, 0x36, 0x84, 0x40, 0xf5, 0x36, 0x0a, 0x03, 0xea, 0x78, 0x4e, 0xb7, 0xc1,
-	0xaa, 0xf7, 0x51, 0x18, 0x90, 0xd7, 0x00, 0x11, 0x8f, 0xef, 0x32, 0x81, 0xbd, 0x24, 0xa5, 0x1b,
-	0x9e, 0xd3, 0x75, 0x8f, 0xbd, 0xa3, 0xb3, 0x65, 0xf3, 0x87, 0xac, 0x14, 0x49, 0x96, 0x4e, 0x15,
-	0x37, 0x99, 0x14, 0x0c, 0xf2, 0x45, 0x0d, 0xe9, 0x42, 0xfb, 0x12, 0x95, 0xc0, 0x71, 0x98, 0xf3,
-	0x14, 0xaf, 0xb9, 0xb9, 0xa3, 0x15, 0x3b, 0xa0, 0x3d, 0x5a, 0x85, 0xc9, 0x01, 0xc0, 0x8c, 0x79,
-	0xaa, 0x52, 0x4d, 0xab, 0x96, 0x04, 0xa3, 0x05, 0x42, 0x4e, 0xa0, 0xc1, 0xa4, 0x34, 0x81, 0xca,
-	0xee, 0x91, 0xd6, 0xac, 0x94, 0xbd, 0xb2, 0x94, 0x45, 0x92, 0x35, 0xd4, 0xc3, 0x93, 0xbc, 0x04,
-	0xd7, 0x3e, 0x22, 0x39, 0x15, 0x46, 0xd3, 0x4d, 0xaf, 0xd2, 0x75, 0x8f, 0x9f, 0x94, 0xcb, 0x96,
-	0x69, 0xe6, 0x0e, 0x97, 0x54, 0x72, 0x01, 0x9d, 0x2b, 0x34, 0x9f, 0xa5, 0x1a, 0x85, 0xc2, 0xa0,
-	0x4a, 0x78, 0x8c, 0x9a, 0x6e, 0xd9, 0xfa, 0x67, 0xe5, 0xfa, 0x75, 0x12, 0xeb, 0x88, 0xf5, 0x32,
-	0x72, 0x08, 0xad, 0x9e, 0x14, 0x86, 0x67, 0x02, 0x55, 0xaf, 0x68, 0x4f, 0xeb, 0x9e, 0xd3, 0xad,
-	0xb1, 0x56, 0xbc, 0x82, 0x92, 0x57, 0x40, 0xdf, 0x7e, 0xc9, 0xcc, 0x69, 0x62, 0x50, 0x9d, 0x8e,
-	0xc7, 0x37, 0x5c, 0x8f, 0x74, 0x80, 0x63, 0x34, 0x38, 0xa4, 0x0d, 0xcf, 0xe9, 0xd6, 0x19, 0xc5,
-	0x3f, 0xe4, 0xc9, 0x0b, 0xd8, 0xbe, 0xe0, 0xd9, 0xb8, 0x68, 0x55, 0x78, 0x41, 0xc1, 0x6e, 0xa8,
-	0x79, 0x54, 0x06, 0xd9, 0xf6, 0xc7, 0x52, 0xe4, 0xff, 0x70, 0x60, 0x67, 0x79, 0x05, 0x7a, 0x22,
-	0x85, 0xc6, 0xdf, 0x9e, 0xc1, 0x01, 0x40, 0x5f, 0xc6, 0x23, 0x34, 0xd6, 0xbf, 0x8d, 0x99, 0x35,
-	0x7a, 0x81, 0x10, 0x0f, 0xdc, 0x77, 0x32, 0x3d, 0xcb, 0x12, 0x59, 0x32, 0xd8, 0x1d, 0x2f, 0xa1,
-	0xe2, 0x0c, 0x22, 0x34, 0x2a, 0x8b, 0xf5, 0x82, 0x35, 0x73, 0xb8, 0x9d, 0xaf, 0xc2, 0xc5, 0xac,
-	0x5e, 0xaa, 0xe4, 0x74, 0x62, 0x49, 0xb5, 0xd9, 0xac, 0x78, 0x81, 0xf8, 0x97, 0xd0, 0xec, 0x1b,
-	0x39, 0x79, 0xfc, 0x6e, 0x0f, 0xa1, 0x75, 0x93, 0xe5, 0x28, 0xa7, 0xa6, 0x8f, 0xb1, 0x14, 0x43,
-	0x6d, 0x45, 0x37, 0x59, 0xcb, 0xac, 0xa0, 0xfe, 0x21, 0xec, 0x9c, 0xa3, 0xb9, 0x8d, 0x42, 0x91,
-	0xc8, 0x47, 0xfa, 0xf9, 0x3f, 0x1d, 0xe8, 0x94, 0x88, 0xff, 0xc9, 0xaa, 0xce, 0x60, 0xb7, 0x5f,
-	0x88, 0x8e, 0xd0, 0xf0, 0x21, 0x37, 0xfc, 0xb1, 0x8d, 0xed, 0x43, 0xfd, 0x81, 0x36, 0x57, 0x5d,
-	0xcf, 0xe7, 0xb1, 0x1f, 0xc2, 0xd3, 0xc1, 0x64, 0x68, 0xcf, 0xe4, 0x5f, 0x5b, 0x3d, 0x87, 0xdd,
-	0xf3, 0xbf, 0x94, 0xe4, 0x9f, 0xc0, 0xde, 0x1a, 0x77, 0xbe, 0xf7, 0xf2, 0x00, 0x67, 0x6d, 0xc0,
-	0x77, 0x67, 0xf5, 0x7f, 0x40, 0x76, 0xa1, 0x76, 0x85, 0xe6, 0xaa, 0x3f, 0x67, 0xd6, 0x44, 0x11,
-	0x14, 0xf3, 0x7a, 0xd7, 0x03, 0x3d, 0xd7, 0x57, 0x8d, 0xaf, 0x07, 0xba, 0xc0, 0x22, 0xcc, 0xf5,
-	0xdc, 0x93, 0x6a, 0x8e, 0xb9, 0x26, 0x3b, 0x50, 0x19, 0x84, 0x81, 0x35, 0xa0, 0xc9, 0x2a, 0xd3,
-	0x30, 0x28, 0x90, 0xf3, 0x30, 0xb0, 0xdb, 0x6e, 0xb2, 0x4a, 0x3a, 0xb3, 0xbc, 0x64, 0xc3, 0xe6,
-	0xba, 0x0d, 0x6f, 0xb6, 0xde, 0xd7, 0xec, 0x57, 0xf7, 0xc3, 0xa6, 0xfd, 0x39, 0xf9, 0x15, 0x00,
-	0x00, 0xff, 0xff, 0x8e, 0xa5, 0x2f, 0xe8, 0x9e, 0x05, 0x00, 0x00,
+	// 596 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xd4, 0x54, 0x41, 0x6f, 0xd3, 0x4c,
+	0x10, 0x95, 0x9b, 0xa4, 0x4d, 0xc6, 0x4d, 0xd2, 0xac, 0xda, 0xef, 0xb3, 0x2a, 0x54, 0x59, 0x3e,
+	0x54, 0x11, 0x87, 0x4a, 0xb4, 0x17, 0xc4, 0x89, 0x12, 0xd3, 0xca, 0x2d, 0xae, 0xaa, 0x4d, 0xd3,
+	0x03, 0xb7, 0xc5, 0x19, 0xbb, 0x26, 0xf1, 0xae, 0xd9, 0xdd, 0x14, 0x38, 0xf3, 0x8f, 0xe0, 0x0f,
+	0x22, 0x6f, 0x52, 0xc7, 0x89, 0x20, 0x42, 0xe2, 0xc4, 0x29, 0x3b, 0x6f, 0xde, 0xcc, 0x3c, 0xcd,
+	0x9b, 0x18, 0x7a, 0x71, 0x2a, 0x31, 0x92, 0x2c, 0x9a, 0xa0, 0x3c, 0xc9, 0xa5, 0xd0, 0xe2, 0xd0,
+	0xd6, 0x5f, 0x73, 0x54, 0xf3, 0xc0, 0xfb, 0x56, 0x87, 0xee, 0x40, 0x22, 0xd3, 0x78, 0x1f, 0x52,
+	0xfc, 0x34, 0x43, 0xa5, 0x09, 0x81, 0xfa, 0x7d, 0x18, 0xf8, 0x8e, 0xe5, 0x5a, 0xfd, 0x16, 0xad,
+	0x3f, 0x86, 0x81, 0x4f, 0x5e, 0x03, 0x84, 0x2c, 0x7a, 0x48, 0x39, 0x0e, 0xe2, 0xc4, 0xd9, 0x72,
+	0xad, 0xbe, 0x7d, 0xea, 0x9e, 0x5c, 0x2c, 0x9b, 0x3f, 0x65, 0x05, 0x8f, 0xd3, 0x64, 0x26, 0x99,
+	0x4e, 0x05, 0xa7, 0x90, 0x95, 0x35, 0xa4, 0x0f, 0xdd, 0x6b, 0x94, 0x1c, 0xa7, 0x41, 0xc6, 0x12,
+	0xbc, 0x65, 0xfa, 0xc1, 0xa9, 0x99, 0x01, 0xdd, 0xc9, 0x2a, 0x4c, 0x8e, 0x00, 0xe6, 0xcc, 0x73,
+	0x99, 0x28, 0xa7, 0x6e, 0x48, 0x30, 0x29, 0x11, 0x72, 0x06, 0x2d, 0x2a, 0x84, 0xf6, 0x65, 0xfa,
+	0x88, 0x4e, 0xc3, 0x48, 0x39, 0xa8, 0x4a, 0x29, 0x93, 0xb4, 0x25, 0x9f, 0x9e, 0xe4, 0x25, 0xd8,
+	0xe6, 0x11, 0x8a, 0x19, 0xd7, 0xca, 0xd9, 0x76, 0x6b, 0x7d, 0xfb, 0xf4, 0xbf, 0x6a, 0xd9, 0x32,
+	0x4d, 0xed, 0xf1, 0x92, 0x4a, 0xae, 0xa0, 0x77, 0x83, 0xfa, 0xb3, 0x90, 0x93, 0x80, 0x6b, 0x94,
+	0x31, 0x8b, 0x50, 0x39, 0x3b, 0xa6, 0xfe, 0x59, 0xb5, 0x7e, 0x9d, 0x44, 0x7b, 0x7c, 0xbd, 0x8c,
+	0x1c, 0x43, 0x67, 0x20, 0xb8, 0x66, 0x29, 0x47, 0x39, 0x28, 0xda, 0x3b, 0x4d, 0xd7, 0xea, 0x37,
+	0x68, 0x27, 0x5a, 0x41, 0xc9, 0x2b, 0x70, 0xde, 0x7e, 0x49, 0xf5, 0x79, 0xac, 0x51, 0x9e, 0x4f,
+	0xa7, 0x77, 0x4c, 0x4d, 0x94, 0x8f, 0x53, 0xd4, 0x38, 0x76, 0x5a, 0xae, 0xd5, 0x6f, 0x52, 0x07,
+	0x7f, 0x93, 0x27, 0x2f, 0x60, 0xf7, 0x8a, 0xa5, 0xd3, 0xa2, 0x55, 0xe1, 0x85, 0x03, 0x66, 0x43,
+	0xed, 0x93, 0x2a, 0x48, 0x77, 0x3f, 0x56, 0x22, 0xef, 0xbb, 0x05, 0x7b, 0xcb, 0x2b, 0x50, 0xb9,
+	0xe0, 0x0a, 0x7f, 0x79, 0x06, 0x47, 0x00, 0x43, 0x11, 0x4d, 0x50, 0x1b, 0xff, 0xb6, 0xe6, 0xd6,
+	0xa8, 0x12, 0x21, 0x2e, 0xd8, 0xef, 0x44, 0x72, 0x91, 0xc6, 0xa2, 0x62, 0xb0, 0x3d, 0x5d, 0x42,
+	0xc5, 0x19, 0x84, 0xa8, 0x65, 0x1a, 0xa9, 0x92, 0x35, 0x77, 0xb8, 0x9b, 0xad, 0xc2, 0xc5, 0xac,
+	0x41, 0x22, 0xc5, 0x2c, 0x37, 0xa4, 0xc6, 0x7c, 0x56, 0x54, 0x22, 0xde, 0x35, 0xb4, 0x87, 0x5a,
+	0xe4, 0x9b, 0xef, 0xf6, 0x18, 0x3a, 0x77, 0x69, 0x86, 0x62, 0xa6, 0x87, 0x18, 0x09, 0x3e, 0x56,
+	0x46, 0x74, 0x9b, 0x76, 0xf4, 0x0a, 0xea, 0x1d, 0xc3, 0xde, 0x25, 0xea, 0xfb, 0x30, 0xe0, 0xb1,
+	0xd8, 0xd0, 0xcf, 0xfb, 0x61, 0x41, 0xaf, 0x42, 0xfc, 0x47, 0x56, 0x75, 0x01, 0xfb, 0xc3, 0x42,
+	0x74, 0x88, 0x9a, 0x8d, 0x99, 0x66, 0x9b, 0x36, 0x76, 0x08, 0xcd, 0x27, 0xda, 0x42, 0x75, 0x33,
+	0x5b, 0xc4, 0x5e, 0x00, 0xff, 0x8f, 0xf2, 0xb1, 0x39, 0x93, 0xbf, 0x6d, 0xf5, 0x1c, 0xf6, 0x2f,
+	0xff, 0x50, 0x92, 0x77, 0x06, 0x07, 0x6b, 0xdc, 0xc5, 0xde, 0xab, 0x03, 0xac, 0xb5, 0x01, 0xf9,
+	0xea, 0xdf, 0x80, 0xec, 0x43, 0xe3, 0x06, 0xf5, 0xcd, 0x70, 0x41, 0x6c, 0xf0, 0x22, 0x28, 0xc6,
+	0x0d, 0x6e, 0x47, 0x6a, 0x21, 0xaf, 0x1e, 0xdd, 0x8e, 0x54, 0x81, 0x85, 0x98, 0xa9, 0x85, 0x25,
+	0xf5, 0x0c, 0x33, 0x45, 0xf6, 0xa0, 0x36, 0x0a, 0x7c, 0xb3, 0xff, 0x36, 0xad, 0xcd, 0x02, 0xbf,
+	0x40, 0x2e, 0x03, 0xdf, 0x2c, 0xbb, 0x4d, 0x6b, 0x49, 0xe0, 0xbf, 0xd9, 0x79, 0xdf, 0x30, 0x1f,
+	0xd5, 0x0f, 0xdb, 0xe6, 0xe7, 0xec, 0x67, 0x00, 0x00, 0x00, 0xff, 0xff, 0x01, 0x7a, 0xf5, 0xcb,
+	0x7d, 0x05, 0x00, 0x00,
 }
diff --git a/proto/firecracker.proto b/proto/firecracker.proto
index 3f7512a..fb0b579 100644
--- a/proto/firecracker.proto
+++ b/proto/firecracker.proto
@@ -102,11 +102,5 @@ message JailerConfig {
     string Mems = 3;
     uint32 UID = 4;
     uint32 GID = 5;
-	// CgroupPath is used to dictate where the cgroup should be located
-	// relative to the cgroup directory which is
-	// /sys/fs/cgroup/cpu/<CgroupPath>/<vmID>
-	// if no value was provided, then /firecracker-containerd will be used as
-	// the default value
-    string CgroupPath = 6;
 }
 
diff --git a/runtime/jailer.go b/runtime/jailer.go
index e48af92..0250401 100644
--- a/runtime/jailer.go
+++ b/runtime/jailer.go
@@ -102,7 +102,6 @@ func newJailer(
 		GID:            request.JailerConfig.GID,
 		CPUs:           request.JailerConfig.CPUs,
 		Mems:           request.JailerConfig.Mems,
-		CgroupPath:     request.JailerConfig.CgroupPath,
 	}
 	return newRuncJailer(ctx, l, service.vmID, config)
 }
diff --git a/runtime/runc_jailer.go b/runtime/runc_jailer.go
index 8ec6d98..90ab2a5 100644
--- a/runtime/runc_jailer.go
+++ b/runtime/runc_jailer.go
@@ -60,7 +60,6 @@ type runcJailerConfig struct {
 	GID            uint32
 	CPUs           string
 	Mems           string
-	CgroupPath     string
 }
 
 func newRuncJailer(ctx context.Context, logger *logrus.Entry, vmID string, cfg runcJailerConfig) (*runcJailer, error) {
@@ -430,12 +429,7 @@ func (j *runcJailer) overwriteConfig(cfg *config.Config, machineConfig *firecrac
 }
 
 func (j runcJailer) CgroupPath() string {
-	basePath := "/firecracker-containerd"
-	if j.Config.CgroupPath != "" {
-		basePath = j.Config.CgroupPath
-	}
-
-	return filepath.Join(basePath, j.vmID)
+	return filepath.Join("/firecracker-containerd", j.vmID)
 }
 
 // setDefaultConfigValues will process the spec file provided and allow any
diff --git a/runtime/service.go b/runtime/service.go
index 524d91c..7b1e896 100644
--- a/runtime/service.go
+++ b/runtime/service.go
@@ -597,17 +597,11 @@ func (s *service) GetVMInfo(requestCtx context.Context, request *proto.GetVMInfo
 		return nil, err
 	}
 
-	cgroupPath := ""
-	if c, ok := s.jailer.(cgroupPather); ok {
-		cgroupPath = c.CgroupPath()
-	}
-
 	return &proto.GetVMInfoResponse{
 		VMID:            s.vmID,
 		SocketPath:      s.shimDir.FirecrackerSockPath(),
 		LogFifoPath:     s.machineConfig.LogFifo,
 		MetricsFifoPath: s.machineConfig.MetricsFifo,
-		CgroupPath:      cgroupPath,
 	}, nil
 }
 
@@ -793,6 +787,46 @@ func (s *service) buildRootDrive(req *proto.CreateVMRequest) []models.Drive {
 	return builder.Build()
 }
 
+func (s *service) newIOProxy(logger *logrus.Entry, stdin, stdout, stderr string, extraData *proto.ExtraData) (vm.IOProxy, error) {
+	var ioConnectorSet vm.IOProxy
+
+	relVSockPath, err := s.jailer.JailPath().FirecrackerVSockRelPath()
+	if err != nil {
+		return nil, errors.Wrapf(err, "failed to get relative path to firecracker vsock")
+	}
+
+	if vm.IsAgentOnlyIO(stdout, logger) {
+		ioConnectorSet = vm.NewNullIOProxy()
+	} else {
+		var stdinConnectorPair *vm.IOConnectorPair
+		if stdin != "" {
+			stdinConnectorPair = &vm.IOConnectorPair{
+				ReadConnector:  vm.ReadFIFOConnector(stdin),
+				WriteConnector: vm.VSockDialConnector(relVSockPath, extraData.StdinPort),
+			}
+		}
+
+		var stdoutConnectorPair *vm.IOConnectorPair
+		if stdout != "" {
+			stdoutConnectorPair = &vm.IOConnectorPair{
+				ReadConnector:  vm.VSockDialConnector(relVSockPath, extraData.StdoutPort),
+				WriteConnector: vm.WriteFIFOConnector(stdout),
+			}
+		}
+
+		var stderrConnectorPair *vm.IOConnectorPair
+		if stderr != "" {
+			stderrConnectorPair = &vm.IOConnectorPair{
+				ReadConnector:  vm.VSockDialConnector(relVSockPath, extraData.StderrPort),
+				WriteConnector: vm.WriteFIFOConnector(stderr),
+			}
+		}
+
+		ioConnectorSet = vm.NewIOConnectorProxy(stdinConnectorPair, stdoutConnectorPair, stderrConnectorPair)
+	}
+	return ioConnectorSet, nil
+}
+
 func (s *service) Create(requestCtx context.Context, request *taskAPI.CreateTaskRequest) (*taskAPI.CreateTaskResponse, error) {
 	logger := s.logger.WithField("task_id", request.ID)
 	defer logPanicAndDie(logger)
@@ -863,41 +897,9 @@ func (s *service) Create(requestCtx context.Context, request *taskAPI.CreateTask
 		return nil, err
 	}
 
-	relVSockPath, err := s.jailer.JailPath().FirecrackerVSockRelPath()
+	ioConnectorSet, err := s.newIOProxy(logger, request.Stdin, request.Stdout, request.Stderr, extraData)
 	if err != nil {
-		return nil, errors.Wrapf(err, "failed to get relative path to firecracker vsock")
-	}
-
-	var ioConnectorSet vm.IOProxy
-
-	if vm.IsAgentOnlyIO(request.Stdout, logger) {
-		ioConnectorSet = vm.NewNullIOProxy()
-	} else {
-		var stdinConnectorPair *vm.IOConnectorPair
-		if request.Stdin != "" {
-			stdinConnectorPair = &vm.IOConnectorPair{
-				ReadConnector:  vm.FIFOConnector(request.Stdin),
-				WriteConnector: vm.VSockDialConnector(relVSockPath, extraData.StdinPort),
-			}
-		}
-
-		var stdoutConnectorPair *vm.IOConnectorPair
-		if request.Stdout != "" {
-			stdoutConnectorPair = &vm.IOConnectorPair{
-				ReadConnector:  vm.VSockDialConnector(relVSockPath, extraData.StdoutPort),
-				WriteConnector: vm.FIFOConnector(request.Stdout),
-			}
-		}
-
-		var stderrConnectorPair *vm.IOConnectorPair
-		if request.Stderr != "" {
-			stderrConnectorPair = &vm.IOConnectorPair{
-				ReadConnector:  vm.VSockDialConnector(relVSockPath, extraData.StderrPort),
-				WriteConnector: vm.FIFOConnector(request.Stderr),
-			}
-		}
-
-		ioConnectorSet = vm.NewIOConnectorProxy(stdinConnectorPair, stdoutConnectorPair, stderrConnectorPair)
+		return nil, err
 	}
 
 	// override the request with the bundle dir that should be used inside the VM
@@ -993,41 +995,9 @@ func (s *service) Exec(requestCtx context.Context, req *taskAPI.ExecProcessReque
 		return nil, err
 	}
 
-	relVSockPath, err := s.jailer.JailPath().FirecrackerVSockRelPath()
+	ioConnectorSet, err := s.newIOProxy(logger, req.Stdin, req.Stdout, req.Stderr, extraData)
 	if err != nil {
-		return nil, errors.Wrapf(err, "failed to get relative path to firecracker vsock")
-	}
-
-	var ioConnectorSet vm.IOProxy
-
-	if vm.IsAgentOnlyIO(req.Stdout, logger) {
-		ioConnectorSet = vm.NewNullIOProxy()
-	} else {
-		var stdinConnectorPair *vm.IOConnectorPair
-		if req.Stdin != "" {
-			stdinConnectorPair = &vm.IOConnectorPair{
-				ReadConnector:  vm.FIFOConnector(req.Stdin),
-				WriteConnector: vm.VSockDialConnector(relVSockPath, extraData.StdinPort),
-			}
-		}
-
-		var stdoutConnectorPair *vm.IOConnectorPair
-		if req.Stdout != "" {
-			stdoutConnectorPair = &vm.IOConnectorPair{
-				ReadConnector:  vm.VSockDialConnector(relVSockPath, extraData.StdoutPort),
-				WriteConnector: vm.FIFOConnector(req.Stdout),
-			}
-		}
-
-		var stderrConnectorPair *vm.IOConnectorPair
-		if req.Stderr != "" {
-			stderrConnectorPair = &vm.IOConnectorPair{
-				ReadConnector:  vm.VSockDialConnector(relVSockPath, extraData.StderrPort),
-				WriteConnector: vm.FIFOConnector(req.Stderr),
-			}
-		}
-
-		ioConnectorSet = vm.NewIOConnectorProxy(stdinConnectorPair, stdoutConnectorPair, stderrConnectorPair)
+		return nil, err
 	}
 
 	resp, err := s.taskManager.ExecProcess(requestCtx, req, s.agentClient, ioConnectorSet)


Test patch:
diff --git a/internal/vm/ioproxy_test.go b/internal/vm/ioproxy_test.go
new file mode 100644
index 0000000..05cdaf7
--- /dev/null
+++ b/internal/vm/ioproxy_test.go
@@ -0,0 +1,66 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License"). You may
+// not use this file except in compliance with the License. A copy of the
+// License is located at
+//
+//	http://aws.amazon.com/apache2.0/
+//
+// or in the "license" file accompanying this file. This file is distributed
+// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+// express or implied. See the License for the specific language governing
+// permissions and limitations under the License.
+
+package vm
+
+import (
+	"context"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/sirupsen/logrus"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func fileConnector(path string, flag int) IOConnector {
+	return func(procCtx context.Context, logger *logrus.Entry) <-chan IOConnectorResult {
+		returnCh := make(chan IOConnectorResult, 1)
+		defer close(returnCh)
+
+		file, err := os.OpenFile(path, flag, 0600)
+		returnCh <- IOConnectorResult{
+			ReadWriteCloser: file,
+			Err:             err,
+		}
+
+		return returnCh
+	}
+}
+
+func TestProxy(t *testing.T) {
+	dir, err := ioutil.TempDir("", t.Name())
+	require.NoError(t, err)
+	defer os.RemoveAll(dir)
+
+	ctx := context.Background()
+	content := "hello world"
+
+	err = ioutil.WriteFile(filepath.Join(dir, "input"), []byte(content), 0600)
+	require.NoError(t, err)
+
+	pair := &IOConnectorPair{
+		ReadConnector:  fileConnector(filepath.Join(dir, "input"), os.O_RDONLY),
+		WriteConnector: fileConnector(filepath.Join(dir, "output"), os.O_CREATE|os.O_WRONLY),
+	}
+	initCh, copyCh := pair.proxy(ctx, logrus.WithFields(logrus.Fields{}), 0)
+
+	assert.Nil(t, <-initCh)
+	assert.Nil(t, <-copyCh)
+
+	bytes, err := ioutil.ReadFile(filepath.Join(dir, "output"))
+	require.NoError(t, err)
+	assert.Equal(t, content, string(bytes))
+}
diff --git a/runtime/jailer_test.go b/runtime/jailer_test.go
index c323544..536941d 100644
--- a/runtime/jailer_test.go
+++ b/runtime/jailer_test.go
@@ -17,7 +17,6 @@ import (
 	"context"
 	"io/ioutil"
 	"os"
-	"os/exec"
 	"path/filepath"
 	"syscall"
 	"testing"
@@ -26,8 +25,6 @@ import (
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 
-	"github.com/firecracker-microvm/firecracker-containerd/config"
-	"github.com/firecracker-microvm/firecracker-containerd/internal/vm"
 	"github.com/firecracker-microvm/firecracker-containerd/proto"
 )
 
@@ -60,7 +57,6 @@ func createSparseFile(path string, size int) error {
 
 	return nil
 }
-
 func TestCopyFile_sparse(t *testing.T) {
 	dir, err := ioutil.TempDir("", t.Name())
 	require.NoError(t, err)
@@ -100,43 +96,3 @@ func TestJailer_invalidUIDGID(t *testing.T) {
 	_, err := newJailer(context.Background(), logrus.NewEntry(logrus.New()), "/foo", &service{}, &req)
 	assert.Error(t, err, "expected invalid uid and gid error, but received none")
 }
-
-func TestNewJailer_Isolated(t *testing.T) {
-	prepareIntegTest(t)
-
-	ociBundlePath, err := ioutil.TempDir("", t.Name())
-	require.NoError(t, err)
-	defer os.RemoveAll(ociBundlePath)
-	b, err := exec.Command(
-		"cp",
-		"firecracker-runc-config.json.example",
-		filepath.Join(ociBundlePath, "config.json"),
-	).CombinedOutput()
-	require.NoErrorf(t, err, "copy runc config failed with: %s", string(b))
-
-	ctx := context.Background()
-	logger := logrus.NewEntry(logrus.New())
-	s := &service{
-		vmID:    "id",
-		shimDir: vm.Dir("/path/to/shim"),
-		config: &config.Config{
-			JailerConfig: config.JailerConfig{
-				RuncBinaryPath: "/path/to/runc_bin_path",
-				RuncConfigPath: filepath.Join(ociBundlePath, "config.json"),
-			},
-		},
-	}
-	req := &proto.CreateVMRequest{
-		JailerConfig: &proto.JailerConfig{
-			UID:        123,
-			GID:        456,
-			CgroupPath: "/my/cgroup_path",
-		},
-	}
-
-	j, err := newJailer(ctx, logger, ociBundlePath, s, req)
-	require.NoError(t, err)
-	runc, ok := j.(*runcJailer)
-	require.True(t, ok)
-	assert.Equal(t, filepath.Join(req.JailerConfig.CgroupPath, s.vmID), runc.CgroupPath())
-}
diff --git a/runtime/service_integ_test.go b/runtime/service_integ_test.go
index 1cf2b43..7a32328 100644
--- a/runtime/service_integ_test.go
+++ b/runtime/service_integ_test.go
@@ -273,15 +273,6 @@ func TestMultipleVMs_Isolated(t *testing.T) {
 				NetNS: netns.Path(),
 			},
 		},
-		{
-			MaxContainers: 3,
-			JailerConfig: &proto.JailerConfig{
-				UID:        300000,
-				GID:        300000,
-				NetNS:      netns.Path(),
-				CgroupPath: "/mycgroup",
-			},
-		},
 	}
 
 	testTimeout := 600 * time.Second
@@ -379,7 +370,6 @@ func TestMultipleVMs_Isolated(t *testing.T) {
 			require.Equal(t, vmInfoResp.SocketPath, filepath.Join(cfg.ShimBaseDir, defaultNamespace, strconv.Itoa(vmID), "firecracker.sock"))
 			require.Equal(t, vmInfoResp.LogFifoPath, filepath.Join(cfg.ShimBaseDir, defaultNamespace, strconv.Itoa(vmID), "fc-logs.fifo"))
 			require.Equal(t, vmInfoResp.MetricsFifoPath, filepath.Join(cfg.ShimBaseDir, defaultNamespace, strconv.Itoa(vmID), "fc-metrics.fifo"))
-			require.Equal(t, resp.CgroupPath, vmInfoResp.CgroupPath)
 
 			// just verify that updating the metadata doesn't return an error, a separate test case is needed
 			// to very the MMDS update propagates to the container correctly
@@ -480,7 +470,7 @@ func testMultipleExecs(
 		require.NoError(t, err, "failed to stat root path of jailer")
 		_, err = os.Stat(filepath.Join("/sys/fs/cgroup/cpu", cgroupPath))
 		require.NoError(t, err, "failed to stat cgroup path of jailer")
-		assert.Regexp(t, fmt.Sprintf(".+/%s", vmIDStr), cgroupPath)
+		assert.Equal(t, filepath.Join("/firecracker-containerd", vmIDStr), cgroupPath)
 	}
 
 	// Verify each exec had the same stdout and use that value as the mount namespace that will be compared
@@ -1566,6 +1556,112 @@ func TestStopVM_Isolated(t *testing.T) {
 		})
 	}
 }
+
+func TestExec_Isolated(t *testing.T) {
+	prepareIntegTest(t)
+
+	client, err := containerd.New(containerdSockPath, containerd.WithDefaultRuntime(firecrackerRuntime))
+	require.NoError(t, err, "unable to create client to containerd service at %s, is containerd running?", containerdSockPath)
+	defer client.Close()
+
+	ctx := namespaces.WithNamespace(context.Background(), "default")
+
+	image, err := alpineImage(ctx, client, defaultSnapshotterName)
+	require.NoError(t, err, "failed to get alpine image")
+
+	pluginClient, err := ttrpcutil.NewClient(containerdSockPath + ".ttrpc")
+	require.NoError(t, err, "failed to create ttrpc client")
+
+	vmID := testNameToVMID(t.Name())
+
+	fcClient := fccontrol.NewFirecrackerClient(pluginClient.Client())
+	_, err = fcClient.CreateVM(ctx, &proto.CreateVMRequest{VMID: vmID})
+	require.NoError(t, err)
+
+	c, err := client.NewContainer(ctx,
+		"container-"+vmID,
+		containerd.WithSnapshotter(defaultSnapshotterName),
+		containerd.WithNewSnapshot("snapshot-"+vmID, image),
+		containerd.WithNewSpec(oci.WithProcessArgs("/bin/sleep", "3"), firecrackeroci.WithVMID(vmID)),
+	)
+	require.NoError(t, err)
+
+	var taskStdout bytes.Buffer
+	var taskStderr bytes.Buffer
+
+	task, err := c.NewTask(ctx, cio.NewCreator(cio.WithStreams(os.Stdin, &taskStdout, &taskStderr)))
+	require.NoError(t, err, "failed to create task for container %s", c.ID())
+
+	taskExitCh, err := task.Wait(ctx)
+	require.NoError(t, err, "failed to wait on task for container %s", c.ID())
+
+	err = task.Start(ctx)
+	require.NoError(t, err, "failed to start task for container %s", c.ID())
+
+	var execStdout bytes.Buffer
+	var execStderr bytes.Buffer
+
+	taskExec, err := task.Exec(ctx, "exec", &specs.Process{
+		Args: []string{"/bin/date"},
+		Cwd:  "/",
+	}, cio.NewCreator(cio.WithStreams(os.Stdin, &execStdout, &execStderr)))
+	require.NoError(t, err)
+
+	execExitCh, err := taskExec.Wait(ctx)
+	require.NoError(t, err, "failed to wait on exec %s", "exec")
+
+	execBegin := time.Now()
+	err = taskExec.Start(ctx)
+	require.NoError(t, err, "failed to start exec %s", "exec")
+
+	select {
+	case execStatus := <-execExitCh:
+		assert.NoError(t, execStatus.Error())
+		assert.Equal(t, uint32(0), execStatus.ExitCode())
+
+		execElapsed := time.Since(execBegin)
+		assert.Truef(
+			t, execElapsed < 1*time.Second,
+			"The exec took %s to finish, which was too slow.", execElapsed,
+		)
+
+		_, err := taskExec.Delete(ctx)
+		assert.NoError(t, err)
+
+		deleteElapsed := time.Since(execBegin)
+		assert.Truef(
+			t, deleteElapsed < 1*time.Second,
+			"The deletion of the exec took %s to finish, which was too slow.", deleteElapsed,
+		)
+
+		assert.NotEqual(t, "", execStdout.String())
+		assert.Equal(t, "", execStderr.String())
+	case <-ctx.Done():
+		require.Fail(t, "context cancelled",
+			"context cancelled while waiting for container %s to exit, err: %v", c.ID(), ctx.Err())
+	}
+
+	select {
+	case taskStatus := <-taskExitCh:
+		assert.NoError(t, taskStatus.Error())
+		assert.Equal(t, uint32(0), taskStatus.ExitCode())
+
+		deleteResult, err := task.Delete(ctx)
+		assert.NoErrorf(t, err, "failed to delete task %q after exit", c.ID())
+		assert.NoError(t, deleteResult.Error())
+
+		assert.Equal(t, "", taskStdout.String())
+		assert.Equal(t, "", taskStderr.String())
+	case <-ctx.Done():
+		require.Fail(t, "context cancelled",
+			"context cancelled while waiting for container %s to exit, err: %v", c.ID(), ctx.Err())
+	}
+
+	_, err = fcClient.StopVM(ctx, &proto.StopVMRequest{VMID: vmID})
+	assert.NoError(t, err)
+	require.Equal(t, status.Code(err), codes.OK)
+}
+
 func TestEvents_Isolated(t *testing.T) {
 	prepareIntegTest(t)
 	require := require.New(t)