diff --git a/cmd/nvidia-cdi-hook/create-symlinks/create-symlinks.go b/cmd/nvidia-cdi-hook/create-symlinks/create-symlinks.go index 646cb2665..5ab5ade29 100644 --- a/cmd/nvidia-cdi-hook/create-symlinks/create-symlinks.go +++ b/cmd/nvidia-cdi-hook/create-symlinks/create-symlinks.go @@ -17,14 +17,17 @@ package symlinks import ( + "errors" "fmt" "os" "path/filepath" "strings" + "github.com/moby/sys/symlink" "github.com/urfave/cli/v2" "github.com/NVIDIA/nvidia-container-toolkit/internal/logger" + "github.com/NVIDIA/nvidia-container-toolkit/internal/lookup/symlinks" "github.com/NVIDIA/nvidia-container-toolkit/internal/oci" ) @@ -33,7 +36,6 @@ type command struct { } type config struct { - hostRoot string links cli.StringSlice containerSpec string } @@ -65,12 +67,6 @@ func (m command) build() *cli.Command { Destination: &cfg.links, }, // The following flags are testing-only flags. - &cli.StringFlag{ - Name: "host-root", - Usage: "The root on the host filesystem to use to resolve symlinks. This is only intended for testing.", - Destination: &cfg.hostRoot, - Hidden: true, - }, &cli.StringFlag{ Name: "container-spec", Usage: "Specify the path to the OCI container spec. If empty or '-' the spec will be read from STDIN. This is only intended for testing.", @@ -95,41 +91,58 @@ func (m command) run(c *cli.Context, cfg *config) error { created := make(map[string]bool) for _, l := range cfg.links.Value() { + if created[l] { + m.logger.Debugf("Link %v already processed", l) + continue + } parts := strings.Split(l, "::") if len(parts) != 2 { - m.logger.Warningf("Invalid link specification %v", l) - continue + return fmt.Errorf("invalid symlink specification %v", l) } - err := m.createLink(created, cfg.hostRoot, containerRoot, parts[0], parts[1]) + err := m.createLink(containerRoot, parts[0], parts[1]) if err != nil { - m.logger.Warningf("Failed to create link %v: %v", parts, err) + return fmt.Errorf("failed to create link %v: %w", parts, err) } + created[l] = true } return nil } -func (m command) createLink(created map[string]bool, hostRoot string, containerRoot string, target string, link string) error { - linkPath, err := changeRoot(hostRoot, containerRoot, link) +// createLink creates a symbolic link in the specified container root. +// This is equivalent to: +// +// chroot {{ .containerRoot }} ln -s {{ .target }} {{ .link }} +// +// If the specified link already exists and points to the same target, this +// operation is a no-op. If the link points to a different target, an error is +// returned. +// +// Note that if the link path resolves to an absolute path oudside of the +// specified root, this is treated as an absolute path in this root. +func (m command) createLink(containerRoot string, targetPath string, link string) error { + linkPath := filepath.Join(containerRoot, link) + + exists, err := doesLinkExist(targetPath, linkPath) if err != nil { - m.logger.Warningf("Failed to resolve path for link %v relative to %v: %v", link, containerRoot, err) + return fmt.Errorf("failed to check if link exists: %w", err) } - if created[linkPath] { - m.logger.Debugf("Link %v already created", linkPath) + if exists { + m.logger.Debugf("Link %s already exists", linkPath) return nil } - targetPath, err := changeRoot(hostRoot, "/", target) + resolvedLinkPath, err := symlink.FollowSymlinkInScope(linkPath, containerRoot) if err != nil { - m.logger.Warningf("Failed to resolve path for target %v relative to %v: %v", target, "/", err) + return fmt.Errorf("failed to follow path for link %v relative to %v: %w", link, containerRoot, err) } - m.logger.Infof("Symlinking %v to %v", linkPath, targetPath) - err = os.MkdirAll(filepath.Dir(linkPath), 0755) + m.logger.Infof("Symlinking %v to %v", resolvedLinkPath, targetPath) + err = os.MkdirAll(filepath.Dir(resolvedLinkPath), 0755) if err != nil { return fmt.Errorf("failed to create directory: %v", err) } - err = os.Symlink(target, linkPath) + err = os.Symlink(targetPath, resolvedLinkPath) if err != nil { return fmt.Errorf("failed to create symlink: %v", err) } @@ -137,19 +150,18 @@ func (m command) createLink(created map[string]bool, hostRoot string, containerR return nil } -func changeRoot(current string, new string, path string) (string, error) { - if !filepath.IsAbs(path) { - return path, nil +// doesLinkExist returns true if link exists and points to target. +// An error is returned if link exists but points to a different target. +func doesLinkExist(target string, link string) (bool, error) { + currentTarget, err := symlinks.Resolve(link) + if errors.Is(err, os.ErrNotExist) { + return false, nil } - - relative := path - if current != "" { - r, err := filepath.Rel(current, path) - if err != nil { - return "", err - } - relative = r + if err != nil { + return false, fmt.Errorf("failed to resolve existing symlink %s: %w", link, err) } - - return filepath.Join(new, relative), nil + if currentTarget == target { + return true, nil + } + return true, fmt.Errorf("unexpected link target: %s", currentTarget) } diff --git a/cmd/nvidia-cdi-hook/create-symlinks/create-symlinks_test.go b/cmd/nvidia-cdi-hook/create-symlinks/create-symlinks_test.go new file mode 100644 index 000000000..b9b23c0ff --- /dev/null +++ b/cmd/nvidia-cdi-hook/create-symlinks/create-symlinks_test.go @@ -0,0 +1,282 @@ +package symlinks + +import ( + "os" + "path/filepath" + "strings" + "testing" + + testlog "github.com/sirupsen/logrus/hooks/test" + "github.com/stretchr/testify/require" + + "github.com/NVIDIA/nvidia-container-toolkit/internal/lookup/symlinks" +) + +func TestDoesLinkExist(t *testing.T) { + tmpDir := t.TempDir() + require.NoError( + t, + makeFs(tmpDir, + dirOrLink{path: "/a/b/c", target: "d"}, + dirOrLink{path: "/a/b/e", target: "/a/b/f"}, + ), + ) + + exists, err := doesLinkExist("d", filepath.Join(tmpDir, "/a/b/c")) + require.NoError(t, err) + require.True(t, exists) + + exists, err = doesLinkExist("/a/b/f", filepath.Join(tmpDir, "/a/b/e")) + require.NoError(t, err) + require.True(t, exists) + + _, err = doesLinkExist("different-target", filepath.Join(tmpDir, "/a/b/c")) + require.Error(t, err) + + _, err = doesLinkExist("/a/b/d", filepath.Join(tmpDir, "/a/b/c")) + require.Error(t, err) + + exists, err = doesLinkExist("foo", filepath.Join(tmpDir, "/a/b/does-not-exist")) + require.NoError(t, err) + require.False(t, exists) +} + +func TestCreateLink(t *testing.T) { + type link struct { + path string + target string + } + type expectedLink struct { + link + err error + } + + testCases := []struct { + description string + containerContents []dirOrLink + link link + expectedCreateError error + expectedLinks []expectedLink + }{ + { + description: "link to / resolves to container root", + containerContents: []dirOrLink{ + {path: "/lib/foo", target: "/"}, + }, + link: link{ + path: "/lib/foo/libfoo.so", + target: "libfoo.so.1", + }, + expectedLinks: []expectedLink{ + { + link: link{ + path: "{{ .containerRoot }}/libfoo.so", + target: "libfoo.so.1", + }, + }, + }, + }, + { + description: "link to / resolves to container root; parent relative link", + containerContents: []dirOrLink{ + {path: "/lib/foo", target: "/"}, + }, + link: link{ + path: "/lib/foo/libfoo.so", + target: "../libfoo.so.1", + }, + expectedLinks: []expectedLink{ + { + link: link{ + path: "{{ .containerRoot }}/libfoo.so", + target: "../libfoo.so.1", + }, + }, + }, + }, + { + description: "link to / resolves to container root; absolute link", + containerContents: []dirOrLink{ + {path: "/lib/foo", target: "/"}, + }, + link: link{ + path: "/lib/foo/libfoo.so", + target: "/a-path-in-container/foo/libfoo.so.1", + }, + expectedLinks: []expectedLink{ + { + link: link{ + path: "{{ .containerRoot }}/libfoo.so", + target: "/a-path-in-container/foo/libfoo.so.1", + }, + }, + { + // We also check that the target is NOT created. + link: link{ + path: "{{ .containerRoot }}/a-path-in-container/foo/libfoo.so.1", + }, + err: os.ErrNotExist, + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + tmpDir := t.TempDir() + hostRoot := filepath.Join(tmpDir, "/host-root/") + containerRoot := filepath.Join(tmpDir, "/container-root") + + require.NoError(t, makeFs(hostRoot)) + require.NoError(t, makeFs(containerRoot, tc.containerContents...)) + + // nvidia-cdi-hook create-symlinks --link linkSpec + err := getTestCommand().createLink(containerRoot, tc.link.target, tc.link.path) + // TODO: We may be able to replace this with require.ErrorIs. + if tc.expectedCreateError != nil { + require.Error(t, err) + } else { + require.NoError(t, err) + } + + for _, expectedLink := range tc.expectedLinks { + path := strings.Replace(expectedLink.path, "{{ .containerRoot }}", containerRoot, -1) + path = strings.Replace(path, "{{ .hostRoot }}", hostRoot, -1) + if expectedLink.target != "" { + target, err := symlinks.Resolve(path) + require.ErrorIs(t, err, expectedLink.err) + require.Equal(t, expectedLink.target, target) + } else { + _, err := os.Stat(path) + require.ErrorIs(t, err, expectedLink.err) + } + } + }) + } +} + +func TestCreateLinkRelativePath(t *testing.T) { + tmpDir := t.TempDir() + hostRoot := filepath.Join(tmpDir, "/host-root/") + containerRoot := filepath.Join(tmpDir, "/container-root") + + require.NoError(t, makeFs(hostRoot)) + require.NoError(t, makeFs(containerRoot, dirOrLink{path: "/lib/"})) + + // nvidia-cdi-hook create-symlinks --link libfoo.so.1::/lib/libfoo.so + err := getTestCommand().createLink(containerRoot, "libfoo.so.1", "/lib/libfoo.so") + require.NoError(t, err) + + target, err := symlinks.Resolve(filepath.Join(containerRoot, "/lib/libfoo.so")) + require.NoError(t, err) + require.Equal(t, "libfoo.so.1", target) +} + +func TestCreateLinkAbsolutePath(t *testing.T) { + tmpDir := t.TempDir() + hostRoot := filepath.Join(tmpDir, "/host-root/") + containerRoot := filepath.Join(tmpDir, "/container-root") + + require.NoError(t, makeFs(hostRoot)) + require.NoError(t, makeFs(containerRoot, dirOrLink{path: "/lib/"})) + + // nvidia-cdi-hook create-symlinks --link /lib/libfoo.so.1::/lib/libfoo.so + err := getTestCommand().createLink(containerRoot, "/lib/libfoo.so.1", "/lib/libfoo.so") + require.NoError(t, err) + + target, err := symlinks.Resolve(filepath.Join(containerRoot, "/lib/libfoo.so")) + require.NoError(t, err) + require.Equal(t, "/lib/libfoo.so.1", target) +} + +func TestCreateLinkAlreadyExists(t *testing.T) { + tmpDir := t.TempDir() + hostRoot := filepath.Join(tmpDir, "/host-root/") + containerRoot := filepath.Join(tmpDir, "/container-root") + + require.NoError(t, makeFs(hostRoot)) + require.NoError(t, makeFs(containerRoot, dirOrLink{path: "/lib/libfoo.so", target: "libfoo.so.1"})) + + // nvidia-cdi-hook create-symlinks --link libfoo.so.1::/lib/libfoo.so + err := getTestCommand().createLink(containerRoot, "libfoo.so.1", "/lib/libfoo.so") + require.NoError(t, err) + target, err := symlinks.Resolve(filepath.Join(containerRoot, "lib/libfoo.so")) + require.NoError(t, err) + require.Equal(t, "libfoo.so.1", target) +} + +func TestCreateLinkAlreadyExistsDifferentTarget(t *testing.T) { + tmpDir := t.TempDir() + hostRoot := filepath.Join(tmpDir, "/host-root/") + containerRoot := filepath.Join(tmpDir, "/container-root") + + require.NoError(t, makeFs(hostRoot)) + require.NoError(t, makeFs(containerRoot, dirOrLink{path: "/lib/libfoo.so", target: "different-target"})) + + // nvidia-cdi-hook create-symlinks --link libfoo.so.1::/lib/libfoo.so + err := getTestCommand().createLink(containerRoot, "libfoo.so.1", "/lib/libfoo.so") + require.Error(t, err) + target, err := symlinks.Resolve(filepath.Join(containerRoot, "lib/libfoo.so")) + require.NoError(t, err) + require.Equal(t, "different-target", target) +} + +func TestCreateLinkOutOfBounds(t *testing.T) { + tmpDir := t.TempDir() + hostRoot := filepath.Join(tmpDir, "/host-root/") + containerRoot := filepath.Join(tmpDir, "/container-root") + + require.NoError(t, makeFs(hostRoot)) + require.NoError(t, + makeFs(containerRoot, + dirOrLink{path: "/lib"}, + dirOrLink{path: "/lib/foo", target: hostRoot}, + ), + ) + + path, err := symlinks.Resolve(filepath.Join(containerRoot, "/lib/foo")) + require.NoError(t, err) + require.Equal(t, hostRoot, path) + + // nvidia-cdi-hook create-symlinks --link ../libfoo.so.1::/lib/foo/libfoo.so + _ = getTestCommand().createLink(containerRoot, "../libfoo.so.1", "/lib/foo/libfoo.so") + // TODO: We need to enabled this check once we have updated the implementation. + // require.Error(t, err) + _, err = os.Lstat(filepath.Join(hostRoot, "libfoo.so")) + require.ErrorIs(t, err, os.ErrNotExist) + _, err = os.Lstat(filepath.Join(containerRoot, hostRoot, "libfoo.so")) + require.NoError(t, err) +} + +type dirOrLink struct { + path string + target string +} + +func makeFs(tmpdir string, fs ...dirOrLink) error { + if err := os.MkdirAll(tmpdir, 0o755); err != nil { + return err + } + for _, s := range fs { + s.path = filepath.Join(tmpdir, s.path) + if s.target == "" { + _ = os.MkdirAll(s.path, 0o755) + continue + } + if err := os.MkdirAll(filepath.Dir(s.path), 0o755); err != nil { + return err + } + if err := os.Symlink(s.target, s.path); err != nil && !os.IsExist(err) { + return err + } + } + return nil +} + +// getTestCommand creates a command for running tests against. +func getTestCommand() *command { + logger, _ := testlog.NewNullLogger() + return &command{ + logger: logger, + } +} diff --git a/go.mod b/go.mod index 7b5bceba9..957826520 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/NVIDIA/go-nvlib v0.6.1 github.com/NVIDIA/go-nvml v0.12.4-0 github.com/fsnotify/fsnotify v1.7.0 + github.com/moby/sys/symlink v0.3.0 github.com/opencontainers/runtime-spec v1.2.0 github.com/pelletier/go-toml v1.9.5 github.com/sirupsen/logrus v1.9.3 diff --git a/go.sum b/go.sum index 6c6d76219..737cd321f 100644 --- a/go.sum +++ b/go.sum @@ -28,6 +28,8 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mndrix/tap-go v0.0.0-20171203230836-629fa407e90b/go.mod h1:pzzDgJWZ34fGzaAZGFW22KVZDfyrYW+QABMrWnJBnSs= +github.com/moby/sys/symlink v0.3.0 h1:GZX89mEZ9u53f97npBy4Rc3vJKj7JBDj/PN2I22GrNU= +github.com/moby/sys/symlink v0.3.0/go.mod h1:3eNdhduHmYPcgsJtZXW1W4XUJdZGBIkttZ8xKqPUJq0= github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= github.com/opencontainers/runtime-spec v1.0.3-0.20220825212826-86290f6a00fb/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/runtime-spec v1.2.0 h1:z97+pHb3uELt/yiAWD691HNHQIF07bE7dzrbT927iTk= diff --git a/vendor/github.com/moby/sys/symlink/LICENSE b/vendor/github.com/moby/sys/symlink/LICENSE new file mode 100644 index 000000000..d64569567 --- /dev/null +++ b/vendor/github.com/moby/sys/symlink/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License 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. diff --git a/vendor/github.com/moby/sys/symlink/LICENSE.APACHE b/vendor/github.com/moby/sys/symlink/LICENSE.APACHE new file mode 100644 index 000000000..5d80670bc --- /dev/null +++ b/vendor/github.com/moby/sys/symlink/LICENSE.APACHE @@ -0,0 +1,191 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2014-2018 Docker, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License 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. diff --git a/vendor/github.com/moby/sys/symlink/LICENSE.BSD b/vendor/github.com/moby/sys/symlink/LICENSE.BSD new file mode 100644 index 000000000..2ee8768d3 --- /dev/null +++ b/vendor/github.com/moby/sys/symlink/LICENSE.BSD @@ -0,0 +1,27 @@ +Copyright (c) 2014-2018 The Docker & Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/moby/sys/symlink/doc.go b/vendor/github.com/moby/sys/symlink/doc.go new file mode 100644 index 000000000..7c22ca275 --- /dev/null +++ b/vendor/github.com/moby/sys/symlink/doc.go @@ -0,0 +1,11 @@ +// Package symlink implements [FollowSymlinkInScope] which is an extension +// of [path/filepath.EvalSymlinks], as well as a Windows long-path aware +// version of [path/filepath.EvalSymlinks] from the Go standard library. +// +// The code from [path/filepath.EvalSymlinks] has been adapted in fs.go. +// Read the [LICENSE.BSD] file that governs fs.go and [LICENSE.APACHE] for +// fs_unix_test.go. +// +// [LICENSE.APACHE]: https://github.com/moby/sys/blob/symlink/v0.2.0/symlink/LICENSE.APACHE +// [LICENSE.BSD]: https://github.com/moby/sys/blob/symlink/v0.2.0/symlink/LICENSE.APACHE +package symlink diff --git a/vendor/github.com/moby/sys/symlink/fs.go b/vendor/github.com/moby/sys/symlink/fs.go new file mode 100644 index 000000000..6b8266138 --- /dev/null +++ b/vendor/github.com/moby/sys/symlink/fs.go @@ -0,0 +1,164 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.BSD file. + +// This code is a modified version of path/filepath/symlink.go from the Go +// standard library in [docker@fa3ec89], which was based on [go1.3.3], +// with Windows implementatinos being added in [docker@9b648df]. +// +// [docker@fa3ec89]: https://github.com/moby/moby/commit/fa3ec89515431ce425f924c8a9a804d5cb18382f +// [go1.3.3]: https://github.com/golang/go/blob/go1.3.3/src/pkg/path/filepath/symlink.go +// [docker@9b648df]: https://github.com/moby/moby/commit/9b648dfac6453de5944ee4bb749115d85a253a05 + +package symlink + +import ( + "bytes" + "errors" + "os" + "path/filepath" + "strings" +) + +// FollowSymlinkInScope evaluates symbolic links in "path" within a scope "root" +// and returns a result guaranteed to be contained within the scope "root" at +// the time of the call. It returns an error of either "path" or "root" cannot +// be converted to an absolute path. +// +// Symbolic links in "root" are not evaluated and left as-is. Errors encountered +// while attempting to evaluate symlinks in path are returned, but non-existing +// paths are valid and do not constitute an error. "path" must contain "root" +// as a prefix, or else an error is returned. Trying to break out from "root" +// does not constitute an error, instead resolves the path within "root". +// +// Example: +// +// // If "/foo/bar" is a symbolic link to "/outside": +// FollowSymlinkInScope("/foo/bar", "/foo") // Returns "/foo/outside" instead of "/outside" +// +// IMPORTANT: It is the caller's responsibility to call FollowSymlinkInScope +// after relevant symbolic links are created to avoid Time-of-check Time-of-use +// (TOCTOU) race conditions ([CWE-367]). No additional symbolic links must be +// created after evaluating, as those could potentially make a previously-safe +// path unsafe. +// +// For example, if "/foo/bar" does not exist, FollowSymlinkInScope("/foo/bar", "/foo") +// evaluates the path to "/foo/bar". If one makes "/foo/bar" a symbolic link to +// "/baz" subsequently, then "/foo/bar" should no longer be considered safely +// contained in "/foo". +// +// [CWE-367]: https://cwe.mitre.org/data/definitions/367.html +func FollowSymlinkInScope(path, root string) (string, error) { + path, err := filepath.Abs(filepath.FromSlash(path)) + if err != nil { + return "", err + } + root, err = filepath.Abs(filepath.FromSlash(root)) + if err != nil { + return "", err + } + return evalSymlinksInScope(path, root) +} + +// evalSymlinksInScope evaluates symbolic links in "path" within a scope "root" +// and returns a result guaranteed to be contained within the scope "root" at +// the time of the call. Refer to [FollowSymlinkInScope] for details. +func evalSymlinksInScope(path, root string) (string, error) { + root = filepath.Clean(root) + if path == root { + return path, nil + } + if !strings.HasPrefix(path, root) { + return "", errors.New("evalSymlinksInScope: " + path + " is not in " + root) + } + const maxIter = 255 + originalPath := path + // given root of "/a" and path of "/a/b/../../c" we want path to be "/b/../../c" + path = path[len(root):] + if root == string(filepath.Separator) { + path = string(filepath.Separator) + path + } + if !strings.HasPrefix(path, string(filepath.Separator)) { + return "", errors.New("evalSymlinksInScope: " + path + " is not in " + root) + } + path = filepath.Clean(path) + // consume path by taking each frontmost path element, + // expanding it if it's a symlink, and appending it to b + var b bytes.Buffer + // b here will always be considered to be the "current absolute path inside + // root" when we append paths to it, we also append a slash and use + // filepath.Clean after the loop to trim the trailing slash + for n := 0; path != ""; n++ { + if n > maxIter { + return "", errors.New("evalSymlinksInScope: too many links in " + originalPath) + } + + // find next path component, p + i := strings.IndexRune(path, filepath.Separator) + var p string + if i == -1 { + p, path = path, "" + } else { + p, path = path[:i], path[i+1:] + } + + if p == "" { + continue + } + + // this takes a b.String() like "b/../" and a p like "c" and turns it + // into "/b/../c" which then gets filepath.Cleaned into "/c" and then + // root gets prepended and we Clean again (to remove any trailing slash + // if the first Clean gave us just "/") + cleanP := filepath.Clean(string(filepath.Separator) + b.String() + p) + if isDriveOrRoot(cleanP) { + // never Lstat "/" itself, or drive letters on Windows + b.Reset() + continue + } + fullP := filepath.Clean(root + cleanP) + + fi, err := os.Lstat(fullP) + if os.IsNotExist(err) { + // if p does not exist, accept it + b.WriteString(p) + b.WriteRune(filepath.Separator) + continue + } + if err != nil { + return "", err + } + if fi.Mode()&os.ModeSymlink == 0 { + b.WriteString(p) + b.WriteRune(filepath.Separator) + continue + } + + // it's a symlink, put it at the front of path + dest, err := os.Readlink(fullP) + if err != nil { + return "", err + } + if isAbs(dest) { + b.Reset() + } + path = dest + string(filepath.Separator) + path + } + + // see note above on "fullP := ..." for why this is double-cleaned and + // what's happening here + return filepath.Clean(root + filepath.Clean(string(filepath.Separator)+b.String())), nil +} + +// EvalSymlinks is a modified version of [path/filepath.EvalSymlinks] from +// the Go standard library with support for Windows long paths (paths prepended +// with "\\?\"). On non-Windows platforms, it's an alias for [path/filepath.EvalSymlinks]. +// +// EvalSymlinks returns the path name after the evaluation of any symbolic +// links. If path is relative, the result will be relative to the current +// directory, unless one of the components is an absolute symbolic link. +// +// EvalSymlinks calls [path/filepath.Clean] on the result. +func EvalSymlinks(path string) (string, error) { + return evalSymlinks(path) +} diff --git a/vendor/github.com/moby/sys/symlink/fs_unix.go b/vendor/github.com/moby/sys/symlink/fs_unix.go new file mode 100644 index 000000000..13ce82511 --- /dev/null +++ b/vendor/github.com/moby/sys/symlink/fs_unix.go @@ -0,0 +1,18 @@ +//go:build !windows +// +build !windows + +package symlink + +import ( + "path/filepath" +) + +func evalSymlinks(path string) (string, error) { + return filepath.EvalSymlinks(path) +} + +func isDriveOrRoot(p string) bool { + return p == string(filepath.Separator) +} + +var isAbs = filepath.IsAbs diff --git a/vendor/github.com/moby/sys/symlink/fs_windows.go b/vendor/github.com/moby/sys/symlink/fs_windows.go new file mode 100644 index 000000000..819b72894 --- /dev/null +++ b/vendor/github.com/moby/sys/symlink/fs_windows.go @@ -0,0 +1,197 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.BSD file. + +// This code is a modified version of [path/filepath/symlink_windows.go] +// and [path/filepath/symlink.go] from the Go 1.4.2 standard library, and +// added in [docker@9b648df]. +// +// [path/filepath/symlink_windows.go]: https://github.com/golang/go/blob/go1.4.2/src/path/filepath/symlink_windows.go +// [path/filepath/symlink.go]: https://github.com/golang/go/blob/go1.4.2/src/path/filepath/symlink.go +// [docker@9b648df]: https://github.com/moby/moby/commit/9b648dfac6453de5944ee4bb749115d85a253a05 + +package symlink + +import ( + "bytes" + "errors" + "os" + "path/filepath" + "strings" + + "golang.org/x/sys/windows" +) + +func toShort(path string) (string, error) { + p, err := windows.UTF16FromString(path) + if err != nil { + return "", err + } + b := p // GetShortPathName says we can reuse buffer + n, err := windows.GetShortPathName(&p[0], &b[0], uint32(len(b))) + if err != nil { + return "", err + } + if n > uint32(len(b)) { + b = make([]uint16, n) + if _, err = windows.GetShortPathName(&p[0], &b[0], uint32(len(b))); err != nil { + return "", err + } + } + return windows.UTF16ToString(b), nil +} + +func toLong(path string) (string, error) { + p, err := windows.UTF16FromString(path) + if err != nil { + return "", err + } + b := p // GetLongPathName says we can reuse buffer + n, err := windows.GetLongPathName(&p[0], &b[0], uint32(len(b))) + if err != nil { + return "", err + } + if n > uint32(len(b)) { + b = make([]uint16, n) + n, err = windows.GetLongPathName(&p[0], &b[0], uint32(len(b))) + if err != nil { + return "", err + } + } + b = b[:n] + return windows.UTF16ToString(b), nil +} + +func evalSymlinks(path string) (string, error) { + path, err := walkSymlinks(path) + if err != nil { + return "", err + } + + p, err := toShort(path) + if err != nil { + return "", err + } + p, err = toLong(p) + if err != nil { + return "", err + } + // windows.GetLongPathName does not change the case of the drive letter, + // but the result of EvalSymlinks must be unique, so we have + // EvalSymlinks(`c:\a`) == EvalSymlinks(`C:\a`). + // Make drive letter upper case. + if len(p) >= 2 && p[1] == ':' && 'a' <= p[0] && p[0] <= 'z' { + p = string(p[0]+'A'-'a') + p[1:] + } else if len(p) >= 6 && p[5] == ':' && 'a' <= p[4] && p[4] <= 'z' { + p = p[:3] + string(p[4]+'A'-'a') + p[5:] + } + return filepath.Clean(p), nil +} + +const ( + utf8RuneSelf = 0x80 + longPathPrefix = `\\?\` +) + +func walkSymlinks(path string) (string, error) { + const maxIter = 255 + originalPath := path + // consume path by taking each frontmost path element, + // expanding it if it's a symlink, and appending it to b + var b bytes.Buffer + for n := 0; path != ""; n++ { + if n > maxIter { + return "", errors.New("too many links in " + originalPath) + } + + // A path beginning with `\\?\` represents the root, so automatically + // skip that part and begin processing the next segment. + if strings.HasPrefix(path, longPathPrefix) { + b.WriteString(longPathPrefix) + path = path[4:] + continue + } + + // find next path component, p + i := -1 + for j, c := range path { + if c < utf8RuneSelf && os.IsPathSeparator(uint8(c)) { + i = j + break + } + } + var p string + if i == -1 { + p, path = path, "" + } else { + p, path = path[:i], path[i+1:] + } + + if p == "" { + if b.Len() == 0 { + // must be absolute path + b.WriteRune(filepath.Separator) + } + continue + } + + // If this is the first segment after the long path prefix, accept the + // current segment as a volume root or UNC share and move on to the next. + if b.String() == longPathPrefix { + b.WriteString(p) + b.WriteRune(filepath.Separator) + continue + } + + fi, err := os.Lstat(b.String() + p) + if err != nil { + return "", err + } + if fi.Mode()&os.ModeSymlink == 0 { + b.WriteString(p) + if path != "" || (b.Len() == 2 && len(p) == 2 && p[1] == ':') { + b.WriteRune(filepath.Separator) + } + continue + } + + // it's a symlink, put it at the front of path + dest, err := os.Readlink(b.String() + p) + if err != nil { + return "", err + } + if isAbs(dest) { + b.Reset() + } + path = dest + string(filepath.Separator) + path + } + return filepath.Clean(b.String()), nil +} + +func isDriveOrRoot(p string) bool { + if p == string(filepath.Separator) { + return true + } + + length := len(p) + if length >= 2 { + if p[length-1] == ':' && (('a' <= p[length-2] && p[length-2] <= 'z') || ('A' <= p[length-2] && p[length-2] <= 'Z')) { + return true + } + } + return false +} + +// isAbs is a platform-specific wrapper for filepath.IsAbs. On Windows, +// golang filepath.IsAbs does not consider a path \windows\system32 as absolute +// as it doesn't start with a drive-letter/colon combination. However, in +// docker we need to verify things such as WORKDIR /windows/system32 in +// a Dockerfile (which gets translated to \windows\system32 when being processed +// by the daemon. This SHOULD be treated as absolute from a docker processing +// perspective. +func isAbs(path string) bool { + if filepath.IsAbs(path) || strings.HasPrefix(path, string(os.PathSeparator)) { + return true + } + return false +} diff --git a/vendor/modules.txt b/vendor/modules.txt index bc67f37d0..097fd17bb 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -27,6 +27,9 @@ github.com/google/uuid ## explicit # github.com/kr/pretty v0.3.1 ## explicit; go 1.12 +# github.com/moby/sys/symlink v0.3.0 +## explicit; go 1.17 +github.com/moby/sys/symlink # github.com/opencontainers/runtime-spec v1.2.0 ## explicit github.com/opencontainers/runtime-spec/specs-go