├── .github └── workflows │ └── ci.yml ├── .golangci.yml ├── LICENSE ├── README.md ├── command_linux.go ├── command_other.go ├── console.go ├── console_test.go ├── container.go ├── events.go ├── go.mod ├── go.sum ├── io.go ├── io_unix.go ├── io_windows.go ├── monitor.go ├── runc.go ├── runc_test.go └── utils.go /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | 11 | checks: 12 | name: Project Checks 13 | runs-on: ${{ matrix.os }} 14 | timeout-minutes: 5 15 | 16 | strategy: 17 | matrix: 18 | go-version: [1.20.x] 19 | os: [ubuntu-22.04] 20 | 21 | steps: 22 | - uses: actions/setup-go@v3 23 | with: 24 | go-version: ${{ matrix.go-version }} 25 | 26 | - name: Set env 27 | shell: bash 28 | run: | 29 | echo "GOPATH=${{ github.workspace }}" >> $GITHUB_ENV 30 | echo "${{ github.workspace }}/bin" >> $GITHUB_PATH 31 | 32 | - uses: actions/checkout@v3 33 | with: 34 | path: src/github.com/containerd/go-runc 35 | fetch-depth: 25 36 | 37 | - uses: containerd/project-checks@v1.1.0 38 | with: 39 | working-directory: src/github.com/containerd/go-runc 40 | 41 | linters: 42 | name: Linters 43 | runs-on: ${{ matrix.os }} 44 | timeout-minutes: 10 45 | 46 | strategy: 47 | matrix: 48 | go-version: [1.20.x] 49 | os: [ubuntu-22.04] 50 | 51 | steps: 52 | - uses: actions/setup-go@v3 53 | with: 54 | go-version: ${{ matrix.go-version }} 55 | 56 | - uses: actions/checkout@v3 57 | with: 58 | path: src/github.com/containerd/go-runc 59 | 60 | - name: Set env 61 | shell: bash 62 | run: | 63 | echo "GOPATH=${{ github.workspace }}" >> $GITHUB_ENV 64 | echo "${{ github.workspace }}/bin" >> $GITHUB_PATH 65 | 66 | - uses: golangci/golangci-lint-action@v3 67 | with: 68 | version: v1.51.1 69 | working-directory: src/github.com/containerd/go-runc 70 | args: --timeout=5m 71 | 72 | tests: 73 | name: Tests 74 | runs-on: ${{ matrix.os }} 75 | timeout-minutes: 5 76 | 77 | strategy: 78 | matrix: 79 | go-version: [1.18.x, 1.19.x, 1.20.x] 80 | os: [ubuntu-22.04] 81 | 82 | steps: 83 | - uses: actions/checkout@v3 84 | with: 85 | path: src/github.com/containerd/go-runc 86 | 87 | - uses: actions/setup-go@v3 88 | with: 89 | go-version: ${{ matrix.go-version }} 90 | 91 | - name: Set env 92 | shell: bash 93 | run: | 94 | echo "GOPATH=${{ github.workspace }}" >> $GITHUB_ENV 95 | echo "${{ github.workspace }}/bin" >> $GITHUB_PATH 96 | 97 | - run: | 98 | go test -v -race -covermode=atomic -coverprofile=coverage.txt ./... 99 | bash <(curl -s https://codecov.io/bash) 100 | working-directory: src/github.com/containerd/go-runc 101 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | enable: 3 | - gofmt 4 | - goimports 5 | - ineffassign 6 | - misspell 7 | - revive 8 | - staticcheck 9 | - unconvert 10 | - unused 11 | - vet 12 | disable: 13 | - errcheck 14 | 15 | issues: 16 | include: 17 | - EXC0002 18 | 19 | run: 20 | timeout: 2m 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-runc 2 | 3 | [![Build Status](https://github.com/containerd/go-runc/workflows/CI/badge.svg)](https://github.com/containerd/go-runc/actions?query=workflow%3ACI) 4 | [![codecov](https://codecov.io/gh/containerd/go-runc/branch/main/graph/badge.svg)](https://codecov.io/gh/containerd/go-runc) 5 | 6 | This is a package for consuming the [runc](https://github.com/opencontainers/runc) binary in your Go applications. 7 | It tries to expose all the settings and features of the runc CLI. If there is something missing then add it, its opensource! 8 | 9 | This needs runc @ [a9610f2c0](https://github.com/opencontainers/runc/commit/a9610f2c0237d2636d05a031ec8659a70e75ffeb) 10 | or greater. 11 | 12 | ## Docs 13 | 14 | Docs can be found at [godoc.org](https://godoc.org/github.com/containerd/go-runc). 15 | 16 | ## Project details 17 | 18 | The go-runc is a containerd sub-project, licensed under the [Apache 2.0 license](./LICENSE). 19 | As a containerd sub-project, you will find the: 20 | 21 | * [Project governance](https://github.com/containerd/project/blob/main/GOVERNANCE.md), 22 | * [Maintainers](https://github.com/containerd/project/blob/main/MAINTAINERS), 23 | * and [Contributing guidelines](https://github.com/containerd/project/blob/main/CONTRIBUTING.md) 24 | 25 | information in our [`containerd/project`](https://github.com/containerd/project) repository. 26 | -------------------------------------------------------------------------------- /command_linux.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package runc 18 | 19 | import ( 20 | "context" 21 | "os" 22 | "os/exec" 23 | "strings" 24 | "syscall" 25 | ) 26 | 27 | func (r *Runc) command(context context.Context, args ...string) *exec.Cmd { 28 | command := r.Command 29 | if command == "" { 30 | command = DefaultCommand 31 | } 32 | cmd := exec.CommandContext(context, command, append(r.args(), args...)...) 33 | cmd.SysProcAttr = &syscall.SysProcAttr{ 34 | Setpgid: r.Setpgid, 35 | } 36 | cmd.Env = filterEnv(os.Environ(), "NOTIFY_SOCKET") // NOTIFY_SOCKET introduces a special behavior in runc but should only be set if invoked from systemd 37 | if r.PdeathSignal != 0 { 38 | cmd.SysProcAttr.Pdeathsig = r.PdeathSignal 39 | } 40 | 41 | return cmd 42 | } 43 | 44 | func filterEnv(in []string, names ...string) []string { 45 | out := make([]string, 0, len(in)) 46 | loop0: 47 | for _, v := range in { 48 | for _, k := range names { 49 | if strings.HasPrefix(v, k+"=") { 50 | continue loop0 51 | } 52 | } 53 | out = append(out, v) 54 | } 55 | return out 56 | } 57 | -------------------------------------------------------------------------------- /command_other.go: -------------------------------------------------------------------------------- 1 | //go:build !linux 2 | 3 | /* 4 | Copyright The containerd Authors. 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | package runc 20 | 21 | import ( 22 | "context" 23 | "os" 24 | "os/exec" 25 | ) 26 | 27 | func (r *Runc) command(context context.Context, args ...string) *exec.Cmd { 28 | command := r.Command 29 | if command == "" { 30 | command = DefaultCommand 31 | } 32 | cmd := exec.CommandContext(context, command, append(r.args(), args...)...) 33 | cmd.Env = os.Environ() 34 | return cmd 35 | } 36 | -------------------------------------------------------------------------------- /console.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | 3 | /* 4 | Copyright The containerd Authors. 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | package runc 20 | 21 | import ( 22 | "fmt" 23 | "net" 24 | "os" 25 | "path/filepath" 26 | 27 | "github.com/containerd/console" 28 | "golang.org/x/sys/unix" 29 | ) 30 | 31 | // NewConsoleSocket creates a new unix socket at the provided path to accept a 32 | // pty master created by runc for use by the container 33 | func NewConsoleSocket(path string) (*Socket, error) { 34 | abs, err := filepath.Abs(path) 35 | if err != nil { 36 | return nil, err 37 | } 38 | addr, err := net.ResolveUnixAddr("unix", abs) 39 | if err != nil { 40 | return nil, err 41 | } 42 | l, err := net.ListenUnix("unix", addr) 43 | if err != nil { 44 | return nil, err 45 | } 46 | return &Socket{ 47 | l: l, 48 | }, nil 49 | } 50 | 51 | // NewTempConsoleSocket returns a temp console socket for use with a container 52 | // On Close(), the socket is deleted 53 | func NewTempConsoleSocket() (*Socket, error) { 54 | runtimeDir := os.Getenv("XDG_RUNTIME_DIR") 55 | dir, err := os.MkdirTemp(runtimeDir, "pty") 56 | if err != nil { 57 | return nil, err 58 | } 59 | abs, err := filepath.Abs(filepath.Join(dir, "pty.sock")) 60 | if err != nil { 61 | return nil, err 62 | } 63 | addr, err := net.ResolveUnixAddr("unix", abs) 64 | if err != nil { 65 | return nil, err 66 | } 67 | l, err := net.ListenUnix("unix", addr) 68 | if err != nil { 69 | return nil, err 70 | } 71 | if runtimeDir != "" { 72 | if err := os.Chmod(abs, 0o755|os.ModeSticky); err != nil { 73 | return nil, err 74 | } 75 | } 76 | return &Socket{ 77 | l: l, 78 | rmdir: true, 79 | }, nil 80 | } 81 | 82 | // Socket is a unix socket that accepts the pty master created by runc 83 | type Socket struct { 84 | rmdir bool 85 | l *net.UnixListener 86 | } 87 | 88 | // Path returns the path to the unix socket on disk 89 | func (c *Socket) Path() string { 90 | return c.l.Addr().String() 91 | } 92 | 93 | // recvFd waits for a file descriptor to be sent over the given AF_UNIX 94 | // socket. The file name of the remote file descriptor will be recreated 95 | // locally (it is sent as non-auxiliary data in the same payload). 96 | func recvFd(socket *net.UnixConn) (*os.File, error) { 97 | const MaxNameLen = 4096 98 | oobSpace := unix.CmsgSpace(4) 99 | 100 | name := make([]byte, MaxNameLen) 101 | oob := make([]byte, oobSpace) 102 | 103 | n, oobn, _, _, err := socket.ReadMsgUnix(name, oob) 104 | if err != nil { 105 | return nil, err 106 | } 107 | 108 | if n >= MaxNameLen || oobn != oobSpace { 109 | return nil, fmt.Errorf("recvfd: incorrect number of bytes read (n=%d oobn=%d)", n, oobn) 110 | } 111 | 112 | // Truncate. 113 | name = name[:n] 114 | oob = oob[:oobn] 115 | 116 | scms, err := unix.ParseSocketControlMessage(oob) 117 | if err != nil { 118 | return nil, err 119 | } 120 | if len(scms) != 1 { 121 | return nil, fmt.Errorf("recvfd: number of SCMs is not 1: %d", len(scms)) 122 | } 123 | scm := scms[0] 124 | 125 | fds, err := unix.ParseUnixRights(&scm) 126 | if err != nil { 127 | return nil, err 128 | } 129 | if len(fds) != 1 { 130 | return nil, fmt.Errorf("recvfd: number of fds is not 1: %d", len(fds)) 131 | } 132 | fd := uintptr(fds[0]) 133 | 134 | return os.NewFile(fd, string(name)), nil 135 | } 136 | 137 | // ReceiveMaster blocks until the socket receives the pty master 138 | func (c *Socket) ReceiveMaster() (console.Console, error) { 139 | conn, err := c.l.Accept() 140 | if err != nil { 141 | return nil, err 142 | } 143 | defer conn.Close() 144 | uc, ok := conn.(*net.UnixConn) 145 | if !ok { 146 | return nil, fmt.Errorf("received connection which was not a unix socket") 147 | } 148 | f, err := recvFd(uc) 149 | if err != nil { 150 | return nil, err 151 | } 152 | return console.ConsoleFromFile(f) 153 | } 154 | 155 | // Close closes the unix socket 156 | func (c *Socket) Close() error { 157 | err := c.l.Close() 158 | if c.rmdir { 159 | if rerr := os.RemoveAll(filepath.Dir(c.Path())); err == nil { 160 | err = rerr 161 | } 162 | } 163 | return err 164 | } 165 | -------------------------------------------------------------------------------- /console_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package runc 18 | 19 | import ( 20 | "errors" 21 | "os" 22 | "testing" 23 | ) 24 | 25 | func TestTempConsole(t *testing.T) { 26 | if err := os.Setenv("XDG_RUNTIME_DIR", ""); err != nil { 27 | t.Fatalf("failed to clear the XDG_RUNTIME_DIR env: %v", err) 28 | } 29 | 30 | c, path := testSocketWithCorrectStickyBitMode(t, 0) 31 | ensureSocketCleanup(t, c, path) 32 | } 33 | 34 | func TestTempConsoleWithXdgRuntimeDir(t *testing.T) { 35 | tmpDir := "/tmp/foo" 36 | // prevent interfering with other tests 37 | defer os.Setenv("XDG_RUNTIME_DIR", os.Getenv("XDG_RUNTIME_DIR")) 38 | if err := os.Setenv("XDG_RUNTIME_DIR", tmpDir); err != nil { 39 | t.Fatal(err) 40 | } 41 | if err := os.MkdirAll(tmpDir, 0o755); err != nil { 42 | t.Fatal(err) 43 | } 44 | 45 | c, path := testSocketWithCorrectStickyBitMode(t, os.ModeSticky) 46 | ensureSocketCleanup(t, c, path) 47 | 48 | if err := os.RemoveAll(tmpDir); err != nil { 49 | t.Fatal(err) 50 | } 51 | } 52 | 53 | func testSocketWithCorrectStickyBitMode(t *testing.T, expectedMode os.FileMode) (*Socket, string) { 54 | c, err := NewTempConsoleSocket() 55 | if err != nil { 56 | t.Fatal(err) 57 | } 58 | path := c.Path() 59 | info, err := os.Stat(path) 60 | if err != nil { 61 | t.Fatal(err) 62 | } 63 | 64 | if (info.Mode() & os.ModeSticky) != expectedMode { 65 | t.Fatal(errors.New("socket has incorrect mode")) 66 | } 67 | return c, path 68 | } 69 | 70 | func ensureSocketCleanup(t *testing.T, c *Socket, path string) { 71 | if err := c.Close(); err != nil { 72 | t.Fatal(err) 73 | } 74 | if _, err := os.Stat(path); err == nil { 75 | t.Fatal("path still exists") 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /container.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package runc 18 | 19 | import "time" 20 | 21 | // Container hold information for a runc container 22 | type Container struct { 23 | ID string `json:"id"` 24 | Pid int `json:"pid"` 25 | Status string `json:"status"` 26 | Bundle string `json:"bundle"` 27 | Rootfs string `json:"rootfs"` 28 | Created time.Time `json:"created"` 29 | Annotations map[string]string `json:"annotations"` 30 | } 31 | -------------------------------------------------------------------------------- /events.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package runc 18 | 19 | // Event is a struct to pass runc event information 20 | type Event struct { 21 | // Type are the event type generated by runc 22 | // If the type is "error" then check the Err field on the event for 23 | // the actual error 24 | Type string `json:"type"` 25 | ID string `json:"id"` 26 | Stats *Stats `json:"data,omitempty"` 27 | // Err has a read error if we were unable to decode the event from runc 28 | Err error `json:"-"` 29 | } 30 | 31 | // Stats is statistical information from the runc process 32 | type Stats struct { 33 | Cpu Cpu `json:"cpu"` //revive:disable 34 | Memory Memory `json:"memory"` 35 | Pids Pids `json:"pids"` 36 | Blkio Blkio `json:"blkio"` 37 | Hugetlb map[string]Hugetlb `json:"hugetlb"` 38 | NetworkInterfaces []*NetworkInterface `json:"network_interfaces"` 39 | } 40 | 41 | // Hugetlb represents the detailed hugetlb component of the statistics data 42 | type Hugetlb struct { 43 | Usage uint64 `json:"usage,omitempty"` 44 | Max uint64 `json:"max,omitempty"` 45 | Failcnt uint64 `json:"failcnt"` 46 | } 47 | 48 | // BlkioEntry represents a block IO entry in the IO stats 49 | type BlkioEntry struct { 50 | Major uint64 `json:"major,omitempty"` 51 | Minor uint64 `json:"minor,omitempty"` 52 | Op string `json:"op,omitempty"` 53 | Value uint64 `json:"value,omitempty"` 54 | } 55 | 56 | // Blkio represents the statistical information from block IO devices 57 | type Blkio struct { 58 | IoServiceBytesRecursive []BlkioEntry `json:"ioServiceBytesRecursive,omitempty"` 59 | IoServicedRecursive []BlkioEntry `json:"ioServicedRecursive,omitempty"` 60 | IoQueuedRecursive []BlkioEntry `json:"ioQueueRecursive,omitempty"` 61 | IoServiceTimeRecursive []BlkioEntry `json:"ioServiceTimeRecursive,omitempty"` 62 | IoWaitTimeRecursive []BlkioEntry `json:"ioWaitTimeRecursive,omitempty"` 63 | IoMergedRecursive []BlkioEntry `json:"ioMergedRecursive,omitempty"` 64 | IoTimeRecursive []BlkioEntry `json:"ioTimeRecursive,omitempty"` 65 | SectorsRecursive []BlkioEntry `json:"sectorsRecursive,omitempty"` 66 | } 67 | 68 | // Pids represents the process ID information 69 | type Pids struct { 70 | Current uint64 `json:"current,omitempty"` 71 | Limit uint64 `json:"limit,omitempty"` 72 | } 73 | 74 | // Throttling represents the throttling statistics 75 | type Throttling struct { 76 | Periods uint64 `json:"periods,omitempty"` 77 | ThrottledPeriods uint64 `json:"throttledPeriods,omitempty"` 78 | ThrottledTime uint64 `json:"throttledTime,omitempty"` 79 | } 80 | 81 | // CpuUsage represents the CPU usage statistics 82 | // 83 | //revive:disable-next-line 84 | type CpuUsage struct { 85 | // Units: nanoseconds. 86 | Total uint64 `json:"total,omitempty"` 87 | Percpu []uint64 `json:"percpu,omitempty"` 88 | Kernel uint64 `json:"kernel"` 89 | User uint64 `json:"user"` 90 | } 91 | 92 | // Cpu represents the CPU usage and throttling statistics 93 | // 94 | //revive:disable-next-line 95 | type Cpu struct { 96 | Usage CpuUsage `json:"usage,omitempty"` 97 | Throttling Throttling `json:"throttling,omitempty"` 98 | } 99 | 100 | // MemoryEntry represents an item in the memory use/statistics 101 | type MemoryEntry struct { 102 | Limit uint64 `json:"limit"` 103 | Usage uint64 `json:"usage,omitempty"` 104 | Max uint64 `json:"max,omitempty"` 105 | Failcnt uint64 `json:"failcnt"` 106 | } 107 | 108 | // Memory represents the collection of memory statistics from the process 109 | type Memory struct { 110 | Cache uint64 `json:"cache,omitempty"` 111 | Usage MemoryEntry `json:"usage,omitempty"` 112 | Swap MemoryEntry `json:"swap,omitempty"` 113 | Kernel MemoryEntry `json:"kernel,omitempty"` 114 | KernelTCP MemoryEntry `json:"kernelTCP,omitempty"` 115 | Raw map[string]uint64 `json:"raw,omitempty"` 116 | } 117 | 118 | type NetworkInterface struct { 119 | // Name is the name of the network interface. 120 | Name string 121 | 122 | RxBytes uint64 123 | RxPackets uint64 124 | RxErrors uint64 125 | RxDropped uint64 126 | TxBytes uint64 127 | TxPackets uint64 128 | TxErrors uint64 129 | TxDropped uint64 130 | } 131 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/containerd/go-runc 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/containerd/console v1.0.3 7 | github.com/opencontainers/runtime-spec v1.1.0 8 | github.com/sirupsen/logrus v1.9.3 9 | golang.org/x/sys v0.13.0 10 | ) 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= 2 | github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/opencontainers/runtime-spec v1.1.0 h1:HHUyrt9mwHUjtasSbXSMvs4cyFxh+Bll4AjJ9odEGpg= 7 | github.com/opencontainers/runtime-spec v1.1.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= 8 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 9 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 10 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 11 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 12 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 13 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 14 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 15 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 16 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 17 | golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= 18 | golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 19 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 20 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 21 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 22 | -------------------------------------------------------------------------------- /io.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package runc 18 | 19 | import ( 20 | "io" 21 | "os" 22 | "os/exec" 23 | ) 24 | 25 | // IO is the terminal IO interface 26 | type IO interface { 27 | io.Closer 28 | Stdin() io.WriteCloser 29 | Stdout() io.ReadCloser 30 | Stderr() io.ReadCloser 31 | Set(*exec.Cmd) 32 | } 33 | 34 | // StartCloser is an interface to handle IO closure after start 35 | type StartCloser interface { 36 | CloseAfterStart() error 37 | } 38 | 39 | // IOOpt sets I/O creation options 40 | type IOOpt func(*IOOption) 41 | 42 | // IOOption holds I/O creation options 43 | type IOOption struct { 44 | OpenStdin bool 45 | OpenStdout bool 46 | OpenStderr bool 47 | } 48 | 49 | func defaultIOOption() *IOOption { 50 | return &IOOption{ 51 | OpenStdin: true, 52 | OpenStdout: true, 53 | OpenStderr: true, 54 | } 55 | } 56 | 57 | func newPipe() (*pipe, error) { 58 | r, w, err := os.Pipe() 59 | if err != nil { 60 | return nil, err 61 | } 62 | return &pipe{ 63 | r: r, 64 | w: w, 65 | }, nil 66 | } 67 | 68 | type pipe struct { 69 | r *os.File 70 | w *os.File 71 | } 72 | 73 | func (p *pipe) Close() error { 74 | err := p.w.Close() 75 | if rerr := p.r.Close(); err == nil { 76 | err = rerr 77 | } 78 | return err 79 | } 80 | 81 | // NewPipeIO creates pipe pairs to be used with runc. It is not implemented 82 | // on Windows. 83 | func NewPipeIO(uid, gid int, opts ...IOOpt) (i IO, err error) { 84 | return newPipeIO(uid, gid, opts...) 85 | } 86 | 87 | type pipeIO struct { 88 | in *pipe 89 | out *pipe 90 | err *pipe 91 | } 92 | 93 | func (i *pipeIO) Stdin() io.WriteCloser { 94 | if i.in == nil { 95 | return nil 96 | } 97 | return i.in.w 98 | } 99 | 100 | func (i *pipeIO) Stdout() io.ReadCloser { 101 | if i.out == nil { 102 | return nil 103 | } 104 | return i.out.r 105 | } 106 | 107 | func (i *pipeIO) Stderr() io.ReadCloser { 108 | if i.err == nil { 109 | return nil 110 | } 111 | return i.err.r 112 | } 113 | 114 | func (i *pipeIO) Close() error { 115 | var err error 116 | for _, v := range []*pipe{ 117 | i.in, 118 | i.out, 119 | i.err, 120 | } { 121 | if v != nil { 122 | if cerr := v.Close(); err == nil { 123 | err = cerr 124 | } 125 | } 126 | } 127 | return err 128 | } 129 | 130 | func (i *pipeIO) CloseAfterStart() error { 131 | for _, f := range []*pipe{ 132 | i.out, 133 | i.err, 134 | } { 135 | if f != nil { 136 | f.w.Close() 137 | } 138 | } 139 | return nil 140 | } 141 | 142 | // Set sets the io to the exec.Cmd 143 | func (i *pipeIO) Set(cmd *exec.Cmd) { 144 | if i.in != nil { 145 | cmd.Stdin = i.in.r 146 | } 147 | if i.out != nil { 148 | cmd.Stdout = i.out.w 149 | } 150 | if i.err != nil { 151 | cmd.Stderr = i.err.w 152 | } 153 | } 154 | 155 | // NewSTDIO returns I/O setup for standard OS in/out/err usage 156 | func NewSTDIO() (IO, error) { 157 | return &stdio{}, nil 158 | } 159 | 160 | type stdio struct{} 161 | 162 | func (s *stdio) Close() error { 163 | return nil 164 | } 165 | 166 | func (s *stdio) Set(cmd *exec.Cmd) { 167 | cmd.Stdin = os.Stdin 168 | cmd.Stdout = os.Stdout 169 | cmd.Stderr = os.Stderr 170 | } 171 | 172 | func (s *stdio) Stdin() io.WriteCloser { 173 | return os.Stdin 174 | } 175 | 176 | func (s *stdio) Stdout() io.ReadCloser { 177 | return os.Stdout 178 | } 179 | 180 | func (s *stdio) Stderr() io.ReadCloser { 181 | return os.Stderr 182 | } 183 | 184 | // NewNullIO returns IO setup for /dev/null use with runc 185 | func NewNullIO() (IO, error) { 186 | f, err := os.Open(os.DevNull) 187 | if err != nil { 188 | return nil, err 189 | } 190 | return &nullIO{ 191 | devNull: f, 192 | }, nil 193 | } 194 | 195 | type nullIO struct { 196 | devNull *os.File 197 | } 198 | 199 | func (n *nullIO) Close() error { 200 | // this should be closed after start but if not 201 | // make sure we close the file but don't return the error 202 | n.devNull.Close() 203 | return nil 204 | } 205 | 206 | func (n *nullIO) Stdin() io.WriteCloser { 207 | return nil 208 | } 209 | 210 | func (n *nullIO) Stdout() io.ReadCloser { 211 | return nil 212 | } 213 | 214 | func (n *nullIO) Stderr() io.ReadCloser { 215 | return nil 216 | } 217 | 218 | func (n *nullIO) Set(c *exec.Cmd) { 219 | // don't set STDIN here 220 | c.Stdout = n.devNull 221 | c.Stderr = n.devNull 222 | } 223 | 224 | func (n *nullIO) CloseAfterStart() error { 225 | return n.devNull.Close() 226 | } 227 | -------------------------------------------------------------------------------- /io_unix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | 3 | /* 4 | Copyright The containerd Authors. 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | package runc 20 | 21 | import ( 22 | "fmt" 23 | "runtime" 24 | 25 | "github.com/sirupsen/logrus" 26 | "golang.org/x/sys/unix" 27 | ) 28 | 29 | // newPipeIO creates pipe pairs to be used with runc 30 | func newPipeIO(uid, gid int, opts ...IOOpt) (i IO, err error) { 31 | option := defaultIOOption() 32 | for _, o := range opts { 33 | o(option) 34 | } 35 | var ( 36 | pipes []*pipe 37 | stdin, stdout, stderr *pipe 38 | ) 39 | // cleanup in case of an error 40 | defer func() { 41 | if err != nil { 42 | for _, p := range pipes { 43 | p.Close() 44 | } 45 | } 46 | }() 47 | if option.OpenStdin { 48 | if stdin, err = newPipe(); err != nil { 49 | return nil, err 50 | } 51 | pipes = append(pipes, stdin) 52 | if err = unix.Fchown(int(stdin.r.Fd()), uid, gid); err != nil { 53 | // TODO: revert with proper darwin solution, skipping for now 54 | // as darwin chown is returning EINVAL on anonymous pipe 55 | if runtime.GOOS == "darwin" { 56 | logrus.WithError(err).Debug("failed to chown stdin, ignored") 57 | } else { 58 | return nil, fmt.Errorf("failed to chown stdin: %w", err) 59 | } 60 | } 61 | } 62 | if option.OpenStdout { 63 | if stdout, err = newPipe(); err != nil { 64 | return nil, err 65 | } 66 | pipes = append(pipes, stdout) 67 | if err = unix.Fchown(int(stdout.w.Fd()), uid, gid); err != nil { 68 | // TODO: revert with proper darwin solution, skipping for now 69 | // as darwin chown is returning EINVAL on anonymous pipe 70 | if runtime.GOOS == "darwin" { 71 | logrus.WithError(err).Debug("failed to chown stdout, ignored") 72 | } else { 73 | return nil, fmt.Errorf("failed to chown stdout: %w", err) 74 | } 75 | } 76 | } 77 | if option.OpenStderr { 78 | if stderr, err = newPipe(); err != nil { 79 | return nil, err 80 | } 81 | pipes = append(pipes, stderr) 82 | if err = unix.Fchown(int(stderr.w.Fd()), uid, gid); err != nil { 83 | // TODO: revert with proper darwin solution, skipping for now 84 | // as darwin chown is returning EINVAL on anonymous pipe 85 | if runtime.GOOS == "darwin" { 86 | logrus.WithError(err).Debug("failed to chown stderr, ignored") 87 | } else { 88 | return nil, fmt.Errorf("failed to chown stderr: %w", err) 89 | } 90 | } 91 | } 92 | return &pipeIO{ 93 | in: stdin, 94 | out: stdout, 95 | err: stderr, 96 | }, nil 97 | } 98 | -------------------------------------------------------------------------------- /io_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | /* 4 | Copyright The containerd Authors. 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | package runc 20 | 21 | import "errors" 22 | 23 | func newPipeIO(uid, gid int, opts ...IOOpt) (i IO, err error) { 24 | return nil, errors.New("not implemented on Windows") 25 | } 26 | -------------------------------------------------------------------------------- /monitor.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package runc 18 | 19 | import ( 20 | "os/exec" 21 | "runtime" 22 | "syscall" 23 | "time" 24 | ) 25 | 26 | // Monitor is the default ProcessMonitor for handling runc process exit 27 | var Monitor ProcessMonitor = &defaultMonitor{} 28 | 29 | // Exit holds the exit information from a process 30 | type Exit struct { 31 | Timestamp time.Time 32 | Pid int 33 | Status int 34 | } 35 | 36 | // ProcessMonitor is an interface for process monitoring. 37 | // 38 | // It allows daemons using go-runc to have a SIGCHLD handler 39 | // to handle exits without introducing races between the handler 40 | // and go's exec.Cmd. 41 | // 42 | // ProcessMonitor also provides a StartLocked method which is similar to 43 | // Start, but locks the goroutine used to start the process to an OS thread 44 | // (for example: when Pdeathsig is set). 45 | type ProcessMonitor interface { 46 | Start(*exec.Cmd) (chan Exit, error) 47 | StartLocked(*exec.Cmd) (chan Exit, error) 48 | Wait(*exec.Cmd, chan Exit) (int, error) 49 | } 50 | 51 | type defaultMonitor struct{} 52 | 53 | func (m *defaultMonitor) Start(c *exec.Cmd) (chan Exit, error) { 54 | if err := c.Start(); err != nil { 55 | return nil, err 56 | } 57 | ec := make(chan Exit, 1) 58 | go func() { 59 | var status int 60 | if err := c.Wait(); err != nil { 61 | status = 255 62 | if exitErr, ok := err.(*exec.ExitError); ok { 63 | if ws, ok := exitErr.Sys().(syscall.WaitStatus); ok { 64 | status = ws.ExitStatus() 65 | } 66 | } 67 | } 68 | ec <- Exit{ 69 | Timestamp: time.Now(), 70 | Pid: c.Process.Pid, 71 | Status: status, 72 | } 73 | close(ec) 74 | }() 75 | return ec, nil 76 | } 77 | 78 | // StartLocked is like Start, but locks the goroutine used to start the process to 79 | // the OS thread for use-cases where the parent thread matters to the child process 80 | // (for example: when Pdeathsig is set). 81 | func (m *defaultMonitor) StartLocked(c *exec.Cmd) (chan Exit, error) { 82 | started := make(chan error) 83 | ec := make(chan Exit, 1) 84 | go func() { 85 | runtime.LockOSThread() 86 | defer runtime.UnlockOSThread() 87 | 88 | if err := c.Start(); err != nil { 89 | started <- err 90 | return 91 | } 92 | close(started) 93 | var status int 94 | if err := c.Wait(); err != nil { 95 | status = 255 96 | if exitErr, ok := err.(*exec.ExitError); ok { 97 | if ws, ok := exitErr.Sys().(syscall.WaitStatus); ok { 98 | status = ws.ExitStatus() 99 | } 100 | } 101 | } 102 | ec <- Exit{ 103 | Timestamp: time.Now(), 104 | Pid: c.Process.Pid, 105 | Status: status, 106 | } 107 | close(ec) 108 | }() 109 | if err := <-started; err != nil { 110 | return nil, err 111 | } 112 | return ec, nil 113 | } 114 | 115 | func (m *defaultMonitor) Wait(c *exec.Cmd, ec chan Exit) (int, error) { 116 | e := <-ec 117 | return e.Status, nil 118 | } 119 | -------------------------------------------------------------------------------- /runc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package runc 18 | 19 | import ( 20 | "bytes" 21 | "context" 22 | "encoding/json" 23 | "errors" 24 | "fmt" 25 | "io" 26 | "os" 27 | "os/exec" 28 | "path/filepath" 29 | "strconv" 30 | "strings" 31 | "syscall" 32 | "time" 33 | 34 | specs "github.com/opencontainers/runtime-spec/specs-go" 35 | "github.com/opencontainers/runtime-spec/specs-go/features" 36 | ) 37 | 38 | // Format is the type of log formatting options available 39 | type Format string 40 | 41 | // TopResults represents the structured data of the full ps output 42 | type TopResults struct { 43 | // Processes running in the container, where each is process is an array of values corresponding to the headers 44 | Processes [][]string `json:"Processes"` 45 | 46 | // Headers are the names of the columns 47 | Headers []string `json:"Headers"` 48 | } 49 | 50 | const ( 51 | none Format = "" 52 | // JSON represents the JSON format 53 | JSON Format = "json" 54 | // Text represents plain text format 55 | Text Format = "text" 56 | ) 57 | 58 | // DefaultCommand is the default command for Runc 59 | var DefaultCommand = "runc" 60 | 61 | // Runc is the client to the runc cli 62 | type Runc struct { 63 | // Command overrides the name of the runc binary. If empty, DefaultCommand 64 | // is used. 65 | Command string 66 | Root string 67 | Debug bool 68 | Log string 69 | LogFormat Format 70 | // PdeathSignal sets a signal the child process will receive when the 71 | // parent dies. 72 | // 73 | // When Pdeathsig is set, command invocations will call runtime.LockOSThread 74 | // to prevent OS thread termination from spuriously triggering the 75 | // signal. See https://github.com/golang/go/issues/27505 and 76 | // https://github.com/golang/go/blob/126c22a09824a7b52c019ed9a1d198b4e7781676/src/syscall/exec_linux.go#L48-L51 77 | // 78 | // A program with GOMAXPROCS=1 might hang because of the use of 79 | // runtime.LockOSThread. Callers should ensure they retain at least one 80 | // unlocked thread. 81 | PdeathSignal syscall.Signal // using syscall.Signal to allow compilation on non-unix (unix.Syscall is an alias for syscall.Signal) 82 | Setpgid bool 83 | 84 | // Criu sets the path to the criu binary used for checkpoint and restore. 85 | // 86 | // Deprecated: runc option --criu is now ignored (with a warning), and the 87 | // option will be removed entirely in a future release. Users who need a non- 88 | // standard criu binary should rely on the standard way of looking up binaries 89 | // in $PATH. 90 | Criu string 91 | SystemdCgroup bool 92 | Rootless *bool // nil stands for "auto" 93 | ExtraArgs []string 94 | } 95 | 96 | // List returns all containers created inside the provided runc root directory 97 | func (r *Runc) List(context context.Context) ([]*Container, error) { 98 | data, err := r.cmdOutput(r.command(context, "list", "--format=json"), false, nil) 99 | defer putBuf(data) 100 | if err != nil { 101 | return nil, err 102 | } 103 | var out []*Container 104 | if err := json.Unmarshal(data.Bytes(), &out); err != nil { 105 | return nil, err 106 | } 107 | return out, nil 108 | } 109 | 110 | // State returns the state for the container provided by id 111 | func (r *Runc) State(context context.Context, id string) (*Container, error) { 112 | data, err := r.cmdOutput(r.command(context, "state", id), true, nil) 113 | defer putBuf(data) 114 | if err != nil { 115 | return nil, fmt.Errorf("%s: %s", err, data.String()) 116 | } 117 | var c Container 118 | if err := json.Unmarshal(data.Bytes(), &c); err != nil { 119 | return nil, err 120 | } 121 | return &c, nil 122 | } 123 | 124 | // ConsoleSocket handles the path of the socket for console access 125 | type ConsoleSocket interface { 126 | Path() string 127 | } 128 | 129 | // CreateOpts holds all the options information for calling runc with supported options 130 | type CreateOpts struct { 131 | IO 132 | // PidFile is a path to where a pid file should be created 133 | PidFile string 134 | ConsoleSocket ConsoleSocket 135 | Detach bool 136 | NoPivot bool 137 | NoNewKeyring bool 138 | ExtraFiles []*os.File 139 | Started chan<- int 140 | ExtraArgs []string 141 | } 142 | 143 | func (o *CreateOpts) args() (out []string, err error) { 144 | if o.PidFile != "" { 145 | abs, err := filepath.Abs(o.PidFile) 146 | if err != nil { 147 | return nil, err 148 | } 149 | out = append(out, "--pid-file", abs) 150 | } 151 | if o.ConsoleSocket != nil { 152 | out = append(out, "--console-socket", o.ConsoleSocket.Path()) 153 | } 154 | if o.NoPivot { 155 | out = append(out, "--no-pivot") 156 | } 157 | if o.NoNewKeyring { 158 | out = append(out, "--no-new-keyring") 159 | } 160 | if o.Detach { 161 | out = append(out, "--detach") 162 | } 163 | if o.ExtraFiles != nil { 164 | out = append(out, "--preserve-fds", strconv.Itoa(len(o.ExtraFiles))) 165 | } 166 | if len(o.ExtraArgs) > 0 { 167 | out = append(out, o.ExtraArgs...) 168 | } 169 | return out, nil 170 | } 171 | 172 | func (r *Runc) startCommand(cmd *exec.Cmd) (chan Exit, error) { 173 | if r.PdeathSignal != 0 { 174 | return Monitor.StartLocked(cmd) 175 | } 176 | return Monitor.Start(cmd) 177 | } 178 | 179 | // Create creates a new container and returns its pid if it was created successfully 180 | func (r *Runc) Create(context context.Context, id, bundle string, opts *CreateOpts) error { 181 | args := []string{"create", "--bundle", bundle} 182 | if opts == nil { 183 | opts = &CreateOpts{} 184 | } 185 | 186 | oargs, err := opts.args() 187 | if err != nil { 188 | return err 189 | } 190 | args = append(args, oargs...) 191 | cmd := r.command(context, append(args, id)...) 192 | if opts.IO != nil { 193 | opts.Set(cmd) 194 | } 195 | cmd.ExtraFiles = opts.ExtraFiles 196 | 197 | if cmd.Stdout == nil && cmd.Stderr == nil { 198 | data, err := r.cmdOutput(cmd, true, nil) 199 | defer putBuf(data) 200 | if err != nil { 201 | return fmt.Errorf("%s: %s", err, data.String()) 202 | } 203 | return nil 204 | } 205 | ec, err := r.startCommand(cmd) 206 | if err != nil { 207 | return err 208 | } 209 | if opts.IO != nil { 210 | if c, ok := opts.IO.(StartCloser); ok { 211 | if err := c.CloseAfterStart(); err != nil { 212 | return err 213 | } 214 | } 215 | } 216 | status, err := Monitor.Wait(cmd, ec) 217 | if err == nil && status != 0 { 218 | err = fmt.Errorf("%s did not terminate successfully: %w", cmd.Args[0], &ExitError{status}) 219 | } 220 | return err 221 | } 222 | 223 | // Start will start an already created container 224 | func (r *Runc) Start(context context.Context, id string) error { 225 | return r.runOrError(r.command(context, "start", id)) 226 | } 227 | 228 | // ExecOpts holds optional settings when starting an exec process with runc 229 | type ExecOpts struct { 230 | IO 231 | PidFile string 232 | ConsoleSocket ConsoleSocket 233 | Detach bool 234 | Started chan<- int 235 | ExtraArgs []string 236 | } 237 | 238 | func (o *ExecOpts) args() (out []string, err error) { 239 | if o.ConsoleSocket != nil { 240 | out = append(out, "--console-socket", o.ConsoleSocket.Path()) 241 | } 242 | if o.Detach { 243 | out = append(out, "--detach") 244 | } 245 | if o.PidFile != "" { 246 | abs, err := filepath.Abs(o.PidFile) 247 | if err != nil { 248 | return nil, err 249 | } 250 | out = append(out, "--pid-file", abs) 251 | } 252 | if len(o.ExtraArgs) > 0 { 253 | out = append(out, o.ExtraArgs...) 254 | } 255 | return out, nil 256 | } 257 | 258 | // Exec executes an additional process inside the container based on a full 259 | // OCI Process specification 260 | func (r *Runc) Exec(context context.Context, id string, spec specs.Process, opts *ExecOpts) error { 261 | if opts == nil { 262 | opts = &ExecOpts{} 263 | } 264 | if opts.Started != nil { 265 | defer close(opts.Started) 266 | } 267 | f, err := os.CreateTemp(os.Getenv("XDG_RUNTIME_DIR"), "runc-process") 268 | if err != nil { 269 | return err 270 | } 271 | defer os.Remove(f.Name()) 272 | err = json.NewEncoder(f).Encode(spec) 273 | f.Close() 274 | if err != nil { 275 | return err 276 | } 277 | args := []string{"exec", "--process", f.Name()} 278 | oargs, err := opts.args() 279 | if err != nil { 280 | return err 281 | } 282 | args = append(args, oargs...) 283 | cmd := r.command(context, append(args, id)...) 284 | if opts.IO != nil { 285 | opts.Set(cmd) 286 | } 287 | if cmd.Stdout == nil && cmd.Stderr == nil { 288 | data, err := r.cmdOutput(cmd, true, opts.Started) 289 | defer putBuf(data) 290 | if err != nil { 291 | return fmt.Errorf("%w: %s", err, data.String()) 292 | } 293 | return nil 294 | } 295 | ec, err := r.startCommand(cmd) 296 | if err != nil { 297 | return err 298 | } 299 | if opts.Started != nil { 300 | opts.Started <- cmd.Process.Pid 301 | } 302 | if opts.IO != nil { 303 | if c, ok := opts.IO.(StartCloser); ok { 304 | if err := c.CloseAfterStart(); err != nil { 305 | return err 306 | } 307 | } 308 | } 309 | status, err := Monitor.Wait(cmd, ec) 310 | if err == nil && status != 0 { 311 | err = fmt.Errorf("%s did not terminate successfully: %w", cmd.Args[0], &ExitError{status}) 312 | } 313 | return err 314 | } 315 | 316 | // Run runs the create, start, delete lifecycle of the container 317 | // and returns its exit status after it has exited 318 | func (r *Runc) Run(context context.Context, id, bundle string, opts *CreateOpts) (int, error) { 319 | if opts == nil { 320 | opts = &CreateOpts{} 321 | } 322 | if opts.Started != nil { 323 | defer close(opts.Started) 324 | } 325 | args := []string{"run", "--bundle", bundle} 326 | oargs, err := opts.args() 327 | if err != nil { 328 | return -1, err 329 | } 330 | args = append(args, oargs...) 331 | cmd := r.command(context, append(args, id)...) 332 | if opts.IO != nil { 333 | opts.Set(cmd) 334 | } 335 | cmd.ExtraFiles = opts.ExtraFiles 336 | ec, err := r.startCommand(cmd) 337 | if err != nil { 338 | return -1, err 339 | } 340 | if opts.Started != nil { 341 | opts.Started <- cmd.Process.Pid 342 | } 343 | status, err := Monitor.Wait(cmd, ec) 344 | if err == nil && status != 0 { 345 | err = fmt.Errorf("%s did not terminate successfully: %w", cmd.Args[0], &ExitError{status}) 346 | } 347 | return status, err 348 | } 349 | 350 | // DeleteOpts holds the deletion options for calling `runc delete` 351 | type DeleteOpts struct { 352 | Force bool 353 | ExtraArgs []string 354 | } 355 | 356 | func (o *DeleteOpts) args() (out []string) { 357 | if o.Force { 358 | out = append(out, "--force") 359 | } 360 | if len(o.ExtraArgs) > 0 { 361 | out = append(out, o.ExtraArgs...) 362 | } 363 | return out 364 | } 365 | 366 | // Delete deletes the container 367 | func (r *Runc) Delete(context context.Context, id string, opts *DeleteOpts) error { 368 | args := []string{"delete"} 369 | if opts != nil { 370 | args = append(args, opts.args()...) 371 | } 372 | return r.runOrError(r.command(context, append(args, id)...)) 373 | } 374 | 375 | // KillOpts specifies options for killing a container and its processes 376 | type KillOpts struct { 377 | All bool 378 | ExtraArgs []string 379 | } 380 | 381 | func (o *KillOpts) args() (out []string) { 382 | if o.All { 383 | out = append(out, "--all") 384 | } 385 | if len(o.ExtraArgs) > 0 { 386 | out = append(out, o.ExtraArgs...) 387 | } 388 | return out 389 | } 390 | 391 | // Kill sends the specified signal to the container 392 | func (r *Runc) Kill(context context.Context, id string, sig int, opts *KillOpts) error { 393 | args := []string{ 394 | "kill", 395 | } 396 | if opts != nil { 397 | args = append(args, opts.args()...) 398 | } 399 | return r.runOrError(r.command(context, append(args, id, strconv.Itoa(sig))...)) 400 | } 401 | 402 | // Stats return the stats for a container like cpu, memory, and io 403 | func (r *Runc) Stats(context context.Context, id string) (*Stats, error) { 404 | cmd := r.command(context, "events", "--stats", id) 405 | rd, err := cmd.StdoutPipe() 406 | if err != nil { 407 | return nil, err 408 | } 409 | ec, err := r.startCommand(cmd) 410 | if err != nil { 411 | return nil, err 412 | } 413 | defer func() { 414 | rd.Close() 415 | Monitor.Wait(cmd, ec) 416 | }() 417 | var e Event 418 | if err := json.NewDecoder(rd).Decode(&e); err != nil { 419 | return nil, err 420 | } 421 | return e.Stats, nil 422 | } 423 | 424 | // Events returns an event stream from runc for a container with stats and OOM notifications 425 | func (r *Runc) Events(context context.Context, id string, interval time.Duration) (chan *Event, error) { 426 | cmd := r.command(context, "events", fmt.Sprintf("--interval=%ds", int(interval.Seconds())), id) 427 | rd, err := cmd.StdoutPipe() 428 | if err != nil { 429 | return nil, err 430 | } 431 | ec, err := r.startCommand(cmd) 432 | if err != nil { 433 | rd.Close() 434 | return nil, err 435 | } 436 | var ( 437 | dec = json.NewDecoder(rd) 438 | c = make(chan *Event, 128) 439 | ) 440 | go func() { 441 | defer func() { 442 | close(c) 443 | rd.Close() 444 | Monitor.Wait(cmd, ec) 445 | }() 446 | for { 447 | var e Event 448 | if err := dec.Decode(&e); err != nil { 449 | if err == io.EOF { 450 | return 451 | } 452 | e = Event{ 453 | Type: "error", 454 | Err: err, 455 | } 456 | } 457 | c <- &e 458 | } 459 | }() 460 | return c, nil 461 | } 462 | 463 | // Pause the container with the provided id 464 | func (r *Runc) Pause(context context.Context, id string) error { 465 | return r.runOrError(r.command(context, "pause", id)) 466 | } 467 | 468 | // Resume the container with the provided id 469 | func (r *Runc) Resume(context context.Context, id string) error { 470 | return r.runOrError(r.command(context, "resume", id)) 471 | } 472 | 473 | // Ps lists all the processes inside the container returning their pids 474 | func (r *Runc) Ps(context context.Context, id string) ([]int, error) { 475 | data, err := r.cmdOutput(r.command(context, "ps", "--format", "json", id), true, nil) 476 | defer putBuf(data) 477 | if err != nil { 478 | return nil, fmt.Errorf("%s: %s", err, data.String()) 479 | } 480 | var pids []int 481 | if err := json.Unmarshal(data.Bytes(), &pids); err != nil { 482 | return nil, err 483 | } 484 | return pids, nil 485 | } 486 | 487 | // Top lists all the processes inside the container returning the full ps data 488 | func (r *Runc) Top(context context.Context, id string, psOptions string) (*TopResults, error) { 489 | data, err := r.cmdOutput(r.command(context, "ps", "--format", "table", id, psOptions), true, nil) 490 | defer putBuf(data) 491 | if err != nil { 492 | return nil, fmt.Errorf("%s: %s", err, data.String()) 493 | } 494 | 495 | topResults, err := ParsePSOutput(data.Bytes()) 496 | if err != nil { 497 | return nil, fmt.Errorf("%s: ", err) 498 | } 499 | return topResults, nil 500 | } 501 | 502 | // CheckpointOpts holds the options for performing a criu checkpoint using runc 503 | type CheckpointOpts struct { 504 | // ImagePath is the path for saving the criu image file 505 | ImagePath string 506 | // WorkDir is the working directory for criu 507 | WorkDir string 508 | // ParentPath is the path for previous image files from a pre-dump 509 | ParentPath string 510 | // AllowOpenTCP allows open tcp connections to be checkpointed 511 | AllowOpenTCP bool 512 | // AllowExternalUnixSockets allows external unix sockets to be checkpointed 513 | AllowExternalUnixSockets bool 514 | // AllowTerminal allows the terminal(pty) to be checkpointed with a container 515 | AllowTerminal bool 516 | // CriuPageServer is the address:port for the criu page server 517 | CriuPageServer string 518 | // FileLocks handle file locks held by the container 519 | FileLocks bool 520 | // Cgroups is the cgroup mode for how to handle the checkpoint of a container's cgroups 521 | Cgroups CgroupMode 522 | // EmptyNamespaces creates a namespace for the container but does not save its properties 523 | // Provide the namespaces you wish to be checkpointed without their settings on restore 524 | EmptyNamespaces []string 525 | // LazyPages uses userfaultfd to lazily restore memory pages 526 | LazyPages bool 527 | // StatusFile is the file criu writes \0 to once lazy-pages is ready 528 | StatusFile *os.File 529 | ExtraArgs []string 530 | } 531 | 532 | // CgroupMode defines the cgroup mode used for checkpointing 533 | type CgroupMode string 534 | 535 | const ( 536 | // Soft is the "soft" cgroup mode 537 | Soft CgroupMode = "soft" 538 | // Full is the "full" cgroup mode 539 | Full CgroupMode = "full" 540 | // Strict is the "strict" cgroup mode 541 | Strict CgroupMode = "strict" 542 | ) 543 | 544 | func (o *CheckpointOpts) args() (out []string) { 545 | if o.ImagePath != "" { 546 | out = append(out, "--image-path", o.ImagePath) 547 | } 548 | if o.WorkDir != "" { 549 | out = append(out, "--work-path", o.WorkDir) 550 | } 551 | if o.ParentPath != "" { 552 | out = append(out, "--parent-path", o.ParentPath) 553 | } 554 | if o.AllowOpenTCP { 555 | out = append(out, "--tcp-established") 556 | } 557 | if o.AllowExternalUnixSockets { 558 | out = append(out, "--ext-unix-sk") 559 | } 560 | if o.AllowTerminal { 561 | out = append(out, "--shell-job") 562 | } 563 | if o.CriuPageServer != "" { 564 | out = append(out, "--page-server", o.CriuPageServer) 565 | } 566 | if o.FileLocks { 567 | out = append(out, "--file-locks") 568 | } 569 | if string(o.Cgroups) != "" { 570 | out = append(out, "--manage-cgroups-mode", string(o.Cgroups)) 571 | } 572 | for _, ns := range o.EmptyNamespaces { 573 | out = append(out, "--empty-ns", ns) 574 | } 575 | if o.LazyPages { 576 | out = append(out, "--lazy-pages") 577 | } 578 | if len(o.ExtraArgs) > 0 { 579 | out = append(out, o.ExtraArgs...) 580 | } 581 | return out 582 | } 583 | 584 | // CheckpointAction represents specific actions executed during checkpoint/restore 585 | type CheckpointAction func([]string) []string 586 | 587 | // LeaveRunning keeps the container running after the checkpoint has been completed 588 | func LeaveRunning(args []string) []string { 589 | return append(args, "--leave-running") 590 | } 591 | 592 | // PreDump allows a pre-dump of the checkpoint to be made and completed later 593 | func PreDump(args []string) []string { 594 | return append(args, "--pre-dump") 595 | } 596 | 597 | // Checkpoint allows you to checkpoint a container using criu 598 | func (r *Runc) Checkpoint(context context.Context, id string, opts *CheckpointOpts, actions ...CheckpointAction) error { 599 | args := []string{"checkpoint"} 600 | extraFiles := []*os.File{} 601 | if opts != nil { 602 | args = append(args, opts.args()...) 603 | if opts.StatusFile != nil { 604 | // pass the status file to the child process 605 | extraFiles = []*os.File{opts.StatusFile} 606 | // set status-fd to 3 as this will be the file descriptor 607 | // of the first file passed with cmd.ExtraFiles 608 | args = append(args, "--status-fd", "3") 609 | } 610 | } 611 | for _, a := range actions { 612 | args = a(args) 613 | } 614 | cmd := r.command(context, append(args, id)...) 615 | cmd.ExtraFiles = extraFiles 616 | return r.runOrError(cmd) 617 | } 618 | 619 | // RestoreOpts holds the options for performing a criu restore using runc 620 | type RestoreOpts struct { 621 | CheckpointOpts 622 | IO 623 | 624 | Detach bool 625 | PidFile string 626 | NoSubreaper bool 627 | NoPivot bool 628 | ConsoleSocket ConsoleSocket 629 | ExtraArgs []string 630 | } 631 | 632 | func (o *RestoreOpts) args() ([]string, error) { 633 | out := o.CheckpointOpts.args() 634 | if o.Detach { 635 | out = append(out, "--detach") 636 | } 637 | if o.PidFile != "" { 638 | abs, err := filepath.Abs(o.PidFile) 639 | if err != nil { 640 | return nil, err 641 | } 642 | out = append(out, "--pid-file", abs) 643 | } 644 | if o.ConsoleSocket != nil { 645 | out = append(out, "--console-socket", o.ConsoleSocket.Path()) 646 | } 647 | if o.NoPivot { 648 | out = append(out, "--no-pivot") 649 | } 650 | if o.NoSubreaper { 651 | out = append(out, "-no-subreaper") 652 | } 653 | if len(o.ExtraArgs) > 0 { 654 | out = append(out, o.ExtraArgs...) 655 | } 656 | return out, nil 657 | } 658 | 659 | // Restore restores a container with the provide id from an existing checkpoint 660 | func (r *Runc) Restore(context context.Context, id, bundle string, opts *RestoreOpts) (int, error) { 661 | args := []string{"restore"} 662 | if opts != nil { 663 | oargs, err := opts.args() 664 | if err != nil { 665 | return -1, err 666 | } 667 | args = append(args, oargs...) 668 | } 669 | args = append(args, "--bundle", bundle) 670 | cmd := r.command(context, append(args, id)...) 671 | if opts != nil && opts.IO != nil { 672 | opts.Set(cmd) 673 | } 674 | ec, err := r.startCommand(cmd) 675 | if err != nil { 676 | return -1, err 677 | } 678 | if opts != nil && opts.IO != nil { 679 | if c, ok := opts.IO.(StartCloser); ok { 680 | if err := c.CloseAfterStart(); err != nil { 681 | return -1, err 682 | } 683 | } 684 | } 685 | status, err := Monitor.Wait(cmd, ec) 686 | if err == nil && status != 0 { 687 | err = fmt.Errorf("%s did not terminate successfully: %w", cmd.Args[0], &ExitError{status}) 688 | } 689 | return status, err 690 | } 691 | 692 | // Update updates the current container with the provided resource spec 693 | func (r *Runc) Update(context context.Context, id string, resources *specs.LinuxResources) error { 694 | buf := getBuf() 695 | defer putBuf(buf) 696 | 697 | if err := json.NewEncoder(buf).Encode(resources); err != nil { 698 | return err 699 | } 700 | args := []string{"update", "--resources=-", id} 701 | cmd := r.command(context, args...) 702 | cmd.Stdin = buf 703 | return r.runOrError(cmd) 704 | } 705 | 706 | // ErrParseRuncVersion is used when the runc version can't be parsed 707 | var ErrParseRuncVersion = errors.New("unable to parse runc version") 708 | 709 | // Version represents the runc version information 710 | type Version struct { 711 | Runc string 712 | Commit string 713 | Spec string 714 | } 715 | 716 | // Version returns the runc and runtime-spec versions 717 | func (r *Runc) Version(context context.Context) (Version, error) { 718 | data, err := r.cmdOutput(r.command(context, "--version"), false, nil) 719 | defer putBuf(data) 720 | if err != nil { 721 | return Version{}, err 722 | } 723 | return parseVersion(data.Bytes()) 724 | } 725 | 726 | func parseVersion(data []byte) (Version, error) { 727 | var v Version 728 | parts := strings.Split(strings.TrimSpace(string(data)), "\n") 729 | 730 | if len(parts) > 0 { 731 | if !strings.HasPrefix(parts[0], "runc version ") { 732 | return v, nil 733 | } 734 | v.Runc = parts[0][13:] 735 | 736 | for _, part := range parts[1:] { 737 | if strings.HasPrefix(part, "commit: ") { 738 | v.Commit = part[8:] 739 | } else if strings.HasPrefix(part, "spec: ") { 740 | v.Spec = part[6:] 741 | } 742 | } 743 | } 744 | 745 | return v, nil 746 | } 747 | 748 | // Features shows the features implemented by the runtime. 749 | // 750 | // Availability: 751 | // 752 | // - runc: supported since runc v1.1.0 753 | // - crun: https://github.com/containers/crun/issues/1177 754 | // - youki: https://github.com/containers/youki/issues/815 755 | func (r *Runc) Features(context context.Context) (*features.Features, error) { 756 | data, err := r.cmdOutput(r.command(context, "features"), false, nil) 757 | defer putBuf(data) 758 | if err != nil { 759 | return nil, err 760 | } 761 | var feat features.Features 762 | if err := json.Unmarshal(data.Bytes(), &feat); err != nil { 763 | return nil, err 764 | } 765 | return &feat, nil 766 | } 767 | 768 | func (r *Runc) args() (out []string) { 769 | if r.Root != "" { 770 | out = append(out, "--root", r.Root) 771 | } 772 | if r.Debug { 773 | out = append(out, "--debug") 774 | } 775 | if r.Log != "" { 776 | out = append(out, "--log", r.Log) 777 | } 778 | if r.LogFormat != none { 779 | out = append(out, "--log-format", string(r.LogFormat)) 780 | } 781 | if r.SystemdCgroup { 782 | out = append(out, "--systemd-cgroup") 783 | } 784 | if r.Rootless != nil { 785 | // nil stands for "auto" (differs from explicit "false") 786 | out = append(out, "--rootless="+strconv.FormatBool(*r.Rootless)) 787 | } 788 | if len(r.ExtraArgs) > 0 { 789 | out = append(out, r.ExtraArgs...) 790 | } 791 | return out 792 | } 793 | 794 | // runOrError will run the provided command. If an error is 795 | // encountered and neither Stdout or Stderr was set the error and the 796 | // stderr of the command will be returned in the format of : 797 | // 798 | func (r *Runc) runOrError(cmd *exec.Cmd) error { 799 | if cmd.Stdout != nil || cmd.Stderr != nil { 800 | ec, err := r.startCommand(cmd) 801 | if err != nil { 802 | return err 803 | } 804 | status, err := Monitor.Wait(cmd, ec) 805 | if err == nil && status != 0 { 806 | err = fmt.Errorf("%s did not terminate successfully: %w", cmd.Args[0], &ExitError{status}) 807 | } 808 | return err 809 | } 810 | data, err := r.cmdOutput(cmd, true, nil) 811 | defer putBuf(data) 812 | if err != nil { 813 | return fmt.Errorf("%s: %s", err, data.String()) 814 | } 815 | return nil 816 | } 817 | 818 | // callers of cmdOutput are expected to call putBuf on the returned Buffer 819 | // to ensure it is released back to the shared pool after use. 820 | func (r *Runc) cmdOutput(cmd *exec.Cmd, combined bool, started chan<- int) (*bytes.Buffer, error) { 821 | b := getBuf() 822 | 823 | cmd.Stdout = b 824 | if combined { 825 | cmd.Stderr = b 826 | } 827 | ec, err := r.startCommand(cmd) 828 | if err != nil { 829 | return nil, err 830 | } 831 | if started != nil { 832 | started <- cmd.Process.Pid 833 | } 834 | 835 | status, err := Monitor.Wait(cmd, ec) 836 | if err == nil && status != 0 { 837 | err = fmt.Errorf("%s did not terminate successfully: %w", cmd.Args[0], &ExitError{status}) 838 | } 839 | 840 | return b, err 841 | } 842 | 843 | // ExitError holds the status return code when a process exits with an error code 844 | type ExitError struct { 845 | Status int 846 | } 847 | 848 | func (e *ExitError) Error() string { 849 | return fmt.Sprintf("exit status %d", e.Status) 850 | } 851 | -------------------------------------------------------------------------------- /runc_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package runc 18 | 19 | import ( 20 | "context" 21 | "errors" 22 | "os" 23 | "os/exec" 24 | "sync" 25 | "syscall" 26 | "testing" 27 | "time" 28 | 29 | specs "github.com/opencontainers/runtime-spec/specs-go" 30 | ) 31 | 32 | func TestParseVersion(t *testing.T) { 33 | testParseVersion := func(t *testing.T, input string, expected Version) { 34 | actual, err := parseVersion([]byte(input)) 35 | if err != nil { 36 | t.Fatalf("unexpected error: %v", err) 37 | } 38 | if expected != actual { 39 | t.Fatalf("expected: %v, actual: %v", expected, actual) 40 | } 41 | } 42 | 43 | t.Run("Full", func(t *testing.T) { 44 | input := `runc version 1.0.0-rc3 45 | commit: 17f3e2a07439a024e54566774d597df9177ee216 46 | spec: 1.0.0-rc5-dev 47 | ` 48 | expected := Version{ 49 | Runc: "1.0.0-rc3", 50 | Commit: "17f3e2a07439a024e54566774d597df9177ee216", 51 | Spec: "1.0.0-rc5-dev", 52 | } 53 | testParseVersion(t, input, expected) 54 | }) 55 | 56 | t.Run("WithoutCommit", func(t *testing.T) { 57 | input := `runc version 1.0.0-rc9 58 | spec: 1.0.1-dev 59 | ` 60 | expected := Version{ 61 | Runc: "1.0.0-rc9", 62 | Commit: "", 63 | Spec: "1.0.1-dev", 64 | } 65 | testParseVersion(t, input, expected) 66 | }) 67 | 68 | t.Run("Oneline", func(t *testing.T) { 69 | input := `runc version 1.0.0-rc8+dev 70 | ` 71 | expected := Version{ 72 | Runc: "1.0.0-rc8+dev", 73 | Commit: "", 74 | Spec: "", 75 | } 76 | testParseVersion(t, input, expected) 77 | }) 78 | 79 | t.Run("Garbage", func(t *testing.T) { 80 | input := `Garbage 81 | spec: nope 82 | ` 83 | expected := Version{ 84 | Runc: "", 85 | Commit: "", 86 | Spec: "", 87 | } 88 | testParseVersion(t, input, expected) 89 | }) 90 | } 91 | 92 | func TestParallelCmds(t *testing.T) { 93 | rc := &Runc{ 94 | // we don't need a real runc, we just want to test running a caller of cmdOutput in parallel 95 | Command: "/bin/true", 96 | } 97 | var wg sync.WaitGroup 98 | 99 | ctx, cancel := context.WithCancel(context.Background()) 100 | defer cancel() 101 | 102 | for i := 0; i < 256; i++ { 103 | wg.Add(1) 104 | go func() { 105 | defer wg.Done() 106 | // We just want to fail if there is a race condition detected by 107 | // "-race", so we ignore the (expected) error here. 108 | _, _ = rc.Version(ctx) 109 | }() 110 | } 111 | wg.Wait() 112 | } 113 | 114 | func TestRuncRunExit(t *testing.T) { 115 | ctx := context.Background() 116 | okRunc := &Runc{ 117 | Command: "/bin/true", 118 | } 119 | 120 | status, err := okRunc.Run(ctx, "fake-id", "fake-bundle", &CreateOpts{}) 121 | if err != nil { 122 | t.Fatalf("Unexpected error from Run: %s", err) 123 | } 124 | if status != 0 { 125 | t.Fatalf("Expected exit status 0 from Run, got %d", status) 126 | } 127 | 128 | failRunc := &Runc{ 129 | Command: "/bin/false", 130 | } 131 | 132 | status, err = failRunc.Run(ctx, "fake-id", "fake-bundle", &CreateOpts{}) 133 | if err == nil { 134 | t.Fatal("Expected error from Run, but got nil") 135 | } 136 | if status != 1 { 137 | t.Fatalf("Expected exit status 1 from Run, got %d", status) 138 | } 139 | extractedStatus := extractStatus(err) 140 | if extractedStatus != status { 141 | t.Fatalf("Expected extracted exit status %d from Run, got %d", status, extractedStatus) 142 | } 143 | } 144 | 145 | func TestRuncExecExit(t *testing.T) { 146 | ctx := context.Background() 147 | okRunc := &Runc{ 148 | Command: "/bin/true", 149 | } 150 | err := okRunc.Exec(ctx, "fake-id", specs.Process{}, &ExecOpts{}) 151 | if err != nil { 152 | t.Fatalf("Unexpected error from Exec: %s", err) 153 | } 154 | status := extractStatus(err) 155 | if status != 0 { 156 | t.Fatalf("Expected exit status 0 from Exec, got %d", status) 157 | } 158 | 159 | failRunc := &Runc{ 160 | Command: "/bin/false", 161 | } 162 | 163 | err = failRunc.Exec(ctx, "fake-id", specs.Process{}, &ExecOpts{}) 164 | if err == nil { 165 | t.Fatal("Expected error from Exec, but got nil") 166 | } 167 | status = extractStatus(err) 168 | if status != 1 { 169 | t.Fatalf("Expected exit status 1 from Exec, got %d", status) 170 | } 171 | 172 | io, err := NewSTDIO() 173 | if err != nil { 174 | t.Fatalf("Unexpected error from NewSTDIO: %s", err) 175 | } 176 | err = failRunc.Exec(ctx, "fake-id", specs.Process{}, &ExecOpts{ 177 | IO: io, 178 | }) 179 | if err == nil { 180 | t.Fatal("Expected error from Exec, but got nil") 181 | } 182 | status = extractStatus(err) 183 | if status != 1 { 184 | t.Fatalf("Expected exit status 1 from Exec, got %d", status) 185 | } 186 | } 187 | 188 | func TestRuncStarted(t *testing.T) { 189 | ctx, timeout := context.WithTimeout(context.Background(), 10*time.Second) 190 | defer timeout() 191 | 192 | dummyCommand, err := dummySleepRunc() 193 | if err != nil { 194 | t.Fatalf("Failed to create dummy sleep runc: %s", err) 195 | } 196 | defer os.Remove(dummyCommand) 197 | sleepRunc := &Runc{ 198 | Command: dummyCommand, 199 | } 200 | 201 | var wg sync.WaitGroup 202 | defer wg.Wait() 203 | 204 | started := make(chan int) 205 | wg.Add(1) 206 | go func() { 207 | defer wg.Done() 208 | interrupt(ctx, t, started) 209 | }() 210 | status, err := sleepRunc.Run(ctx, "fake-id", "fake-bundle", &CreateOpts{ 211 | Started: started, 212 | }) 213 | if err == nil { 214 | t.Fatal("Expected error from Run, but got nil") 215 | } 216 | if status != -1 { 217 | t.Fatalf("Expected exit status 0 from Run, got %d", status) 218 | } 219 | 220 | started = make(chan int) 221 | wg.Add(1) 222 | go func() { 223 | defer wg.Done() 224 | interrupt(ctx, t, started) 225 | }() 226 | err = sleepRunc.Exec(ctx, "fake-id", specs.Process{}, &ExecOpts{ 227 | Started: started, 228 | }) 229 | if err == nil { 230 | t.Fatal("Expected error from Exec, but got nil") 231 | } 232 | status = extractStatus(err) 233 | if status != -1 { 234 | t.Fatalf("Expected exit status -1 from Exec, got %d", status) 235 | } 236 | 237 | started = make(chan int) 238 | wg.Add(1) 239 | go func() { 240 | defer wg.Done() 241 | interrupt(ctx, t, started) 242 | }() 243 | io, err := NewSTDIO() 244 | if err != nil { 245 | t.Fatalf("Unexpected error from NewSTDIO: %s", err) 246 | } 247 | err = sleepRunc.Exec(ctx, "fake-id", specs.Process{}, &ExecOpts{ 248 | IO: io, 249 | Started: started, 250 | }) 251 | if err == nil { 252 | t.Fatal("Expected error from Exec, but got nil") 253 | } 254 | status = extractStatus(err) 255 | if status != -1 { 256 | t.Fatalf("Expected exit status 1 from Exec, got %d", status) 257 | } 258 | } 259 | 260 | func extractStatus(err error) int { 261 | if err == nil { 262 | return 0 263 | } 264 | var exitError *ExitError 265 | if errors.As(err, &exitError) { 266 | return exitError.Status 267 | } 268 | return -1 269 | } 270 | 271 | // interrupt waits for the pid over the started channel then sends a 272 | // SIGINT to the process. 273 | func interrupt(ctx context.Context, t *testing.T, started <-chan int) { 274 | select { 275 | case <-ctx.Done(): 276 | t.Fatal("Timed out waiting for started message") 277 | case pid, ok := <-started: 278 | if !ok { 279 | t.Fatal("Started channel closed without sending pid") 280 | } 281 | process, _ := os.FindProcess(pid) 282 | defer process.Release() 283 | err := process.Signal(syscall.SIGINT) 284 | if err != nil { 285 | t.Fatalf("Failed to send SIGINT to %d: %s", pid, err) 286 | } 287 | } 288 | } 289 | 290 | // dummySleepRunc creates s simple script that just runs `sleep 10` to replace 291 | // runc for testing process that are longer running. 292 | func dummySleepRunc() (_ string, err error) { 293 | fh, err := os.CreateTemp("", "*.sh") 294 | if err != nil { 295 | return "", err 296 | } 297 | defer func() { 298 | if err != nil { 299 | os.Remove(fh.Name()) 300 | } 301 | }() 302 | _, err = fh.Write([]byte("#!/bin/sh\nexec /bin/sleep 10")) 303 | if err != nil { 304 | return "", err 305 | } 306 | err = fh.Close() 307 | if err != nil { 308 | return "", err 309 | } 310 | err = os.Chmod(fh.Name(), 0o755) 311 | if err != nil { 312 | return "", err 313 | } 314 | return fh.Name(), nil 315 | } 316 | 317 | func TestCreateArgs(t *testing.T) { 318 | o := &CreateOpts{} 319 | args, err := o.args() 320 | if err != nil { 321 | t.Fatal(err) 322 | } 323 | if len(args) != 0 { 324 | t.Fatal("args should be empty") 325 | } 326 | o.ExtraArgs = []string{"--other"} 327 | args, err = o.args() 328 | if err != nil { 329 | t.Fatal(err) 330 | } 331 | if len(args) != 1 { 332 | t.Fatal("args should have 1 arg") 333 | } 334 | if a := args[0]; a != "--other" { 335 | t.Fatalf("arg should be --other but got %q", a) 336 | } 337 | } 338 | 339 | func TestRuncFeatures(t *testing.T) { 340 | ctx := context.Background() 341 | if _, err := exec.LookPath(DefaultCommand); err != nil { 342 | t.Skipf("%q was not found in PATH", DefaultCommand) 343 | } 344 | runc := &Runc{} 345 | feat, err := runc.Features(ctx) 346 | if err != nil { 347 | t.Fatal(err) 348 | } 349 | var rroPresent bool 350 | for _, f := range feat.MountOptions { 351 | if f == "rro" { 352 | rroPresent = true 353 | } 354 | } 355 | if !rroPresent { 356 | t.Fatalf("\"rro\" was not found in feat.MountOptions (feat=%+v)", feat) 357 | } 358 | } 359 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The containerd Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package runc 18 | 19 | import ( 20 | "bytes" 21 | "os" 22 | "strconv" 23 | "strings" 24 | "sync" 25 | ) 26 | 27 | // ReadPidFile reads the pid file at the provided path and returns 28 | // the pid or an error if the read and conversion is unsuccessful 29 | func ReadPidFile(path string) (int, error) { 30 | data, err := os.ReadFile(path) 31 | if err != nil { 32 | return -1, err 33 | } 34 | return strconv.Atoi(string(data)) 35 | } 36 | 37 | var bytesBufferPool = sync.Pool{ 38 | New: func() interface{} { 39 | return bytes.NewBuffer(nil) 40 | }, 41 | } 42 | 43 | func getBuf() *bytes.Buffer { 44 | return bytesBufferPool.Get().(*bytes.Buffer) 45 | } 46 | 47 | func putBuf(b *bytes.Buffer) { 48 | if b == nil { 49 | return 50 | } 51 | 52 | b.Reset() 53 | bytesBufferPool.Put(b) 54 | } 55 | 56 | // fieldsASCII is similar to strings.Fields but only allows ASCII whitespaces 57 | func fieldsASCII(s string) []string { 58 | fn := func(r rune) bool { 59 | switch r { 60 | case '\t', '\n', '\f', '\r', ' ': 61 | return true 62 | } 63 | return false 64 | } 65 | return strings.FieldsFunc(s, fn) 66 | } 67 | 68 | // ParsePSOutput parses the runtime's ps raw output and returns a TopResults 69 | func ParsePSOutput(output []byte) (*TopResults, error) { 70 | topResults := &TopResults{} 71 | 72 | lines := strings.Split(string(output), "\n") 73 | topResults.Headers = fieldsASCII(lines[0]) 74 | 75 | pidIndex := -1 76 | for i, name := range topResults.Headers { 77 | if name == "PID" { 78 | pidIndex = i 79 | } 80 | } 81 | 82 | for _, line := range lines[1:] { 83 | if len(line) == 0 { 84 | continue 85 | } 86 | 87 | fields := fieldsASCII(line) 88 | 89 | if fields[pidIndex] == "-" { 90 | continue 91 | } 92 | 93 | process := fields[:len(topResults.Headers)-1] 94 | process = append(process, strings.Join(fields[len(topResults.Headers)-1:], " ")) 95 | topResults.Processes = append(topResults.Processes, process) 96 | 97 | } 98 | return topResults, nil 99 | } 100 | --------------------------------------------------------------------------------