Repository navigation
Prevent symlink-following chown in vcap-writable directories #234
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -208,6 +208,35 @@ var _ = Describe("RuncAdapter", func() { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Context("when an additional volume provided is a symlink", func() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sentinelFile string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| symlinkPath string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| BeforeEach(func() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sentinelFile = filepath.Join(systemRoot, "sentinel.target") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Expect(os.WriteFile(sentinelFile, []byte("protected"), 0600)).To(Succeed()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| symlinkPath = filepath.Join(systemRoot, "symlink-vol") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Expect(os.Symlink(sentinelFile, symlinkPath)).To(Succeed()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| procCfg.AdditionalVolumes = append(procCfg.AdditionalVolumes, config.Volume{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Path: symlinkPath, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| It("refuses to configure the volume and does not modify the target", func() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _, _, err := runcAdapter.CreateJobPrerequisites(bpmCfg, procCfg, user) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Expect(err).To(HaveOccurred()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Expect(err.Error()).To(ContainSubstring("refusing to configure additional volume at symlink")) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| contents, readErr := os.ReadFile(sentinelFile) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Expect(readErr).NotTo(HaveOccurred()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Expect(string(contents)).To(Equal("protected")) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Context("when a volume should be mounted only", func() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| BeforeEach(func() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| procCfg.AdditionalVolumes = append(procCfg.AdditionalVolumes, config.Volume{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -363,6 +392,131 @@ var _ = Describe("RuncAdapter", func() { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Context("when a job directory is a symlink to an existing directory", func() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var sentinelDir string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| BeforeEach(func() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sentinelDir = filepath.Join(systemRoot, "sentinel-dir") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Expect(os.MkdirAll(sentinelDir, 0755)).To(Succeed()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Plant a symlink at the temp directory pointing to sentinel | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tempDirParent := filepath.Dir(bpmCfg.TempDir().External()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Expect(os.MkdirAll(tempDirParent, 0700)).To(Succeed()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Expect(os.Symlink(sentinelDir, bpmCfg.TempDir().External())).To(Succeed()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| It("refuses to follow the symlink and does not alter the target", func() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _, _, err := runcAdapter.CreateJobPrerequisites(bpmCfg, procCfg, user) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Expect(err).To(HaveOccurred()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Expect(err.Error()).To(ContainSubstring("refusing to create directory through symlink")) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| info, statErr := os.Lstat(sentinelDir) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Expect(statErr).NotTo(HaveOccurred()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Expect(info.Mode() & os.ModePerm).To(Equal(os.FileMode(0755))) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // TNZGOV-18900 (CWE-59): CreateJobPrerequisites runs as root on the host. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Several of the paths it chowns live in directories that are bind-mounted | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // writable into the container and owned by vcap, so a compromised container | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // can pre-plant a symlink and redirect root's chown onto an arbitrary host | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // file — a container escape. These tests reproduce the escape against the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // real entry point and assert the host target's ownership is never changed. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // The escape is only observable when the chown can actually change an owner, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // which requires root; the whole suite already runs as root in CI (other | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // cases assert Uid == 200). We skip explicitly when not root so a rootless | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // run reports "skipped" rather than a misleading pass. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Describe("SECURITY: symlink-following chown (TNZGOV-18900)", func() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var hostTarget, hostSentinel string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ownerUID := func(path string) uint32 { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| info, err := os.Stat(path) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Expect(err).NotTo(HaveOccurred()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return info.Sys().(*syscall.Stat_t).Uid | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| BeforeEach(func() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if os.Getuid() != 0 { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Skip("must run as root to observe the ownership change that constitutes the escape") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Stand-in for a sensitive host location (e.g. /etc) that lives | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // OUTSIDE the job tree and is owned by root. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| hostTarget = filepath.Join(systemRoot, "host-sensitive") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Expect(os.MkdirAll(hostTarget, 0700)).To(Succeed()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| hostSentinel = filepath.Join(hostTarget, "shadow") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Expect(os.WriteFile(hostSentinel, []byte("root:!locked:"), 0600)).To(Succeed()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Expect(ownerUID(hostTarget)).To(Equal(uint32(0))) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Context("when the attacker plants a symlink at a created job directory (leaf)", func() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| BeforeEach(func() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // vcap owns the job data dir inside the container, so it can | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // replace TempDir with a symlink to the host target. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tempDirParent := filepath.Dir(bpmCfg.TempDir().External()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Expect(os.MkdirAll(tempDirParent, 0700)).To(Succeed()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Expect(os.Symlink(hostTarget, bpmCfg.TempDir().External())).To(Succeed()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| It("refuses to follow it and does not chown the host target to vcap", func() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _, _, err := runcAdapter.CreateJobPrerequisites(bpmCfg, procCfg, user) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Expect(ownerUID(hostTarget)).To(Equal(uint32(0)), "host target was chowned to vcap — CONTAINER ESCAPE") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Expect(err).To(HaveOccurred()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Expect(err.Error()).To(ContainSubstring("refusing to create directory through symlink")) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| contents, readErr := os.ReadFile(hostSentinel) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Expect(readErr).NotTo(HaveOccurred()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Expect(string(contents)).To(Equal("root:!locked:")) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Context("when the attacker plants a symlink at an additional volume (leaf)", func() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| BeforeEach(func() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| volParent := filepath.Join(systemRoot, "vcap-writable") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Expect(os.MkdirAll(volParent, 0770)).To(Succeed()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| volPath := filepath.Join(volParent, "vol") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Expect(os.Symlink(hostTarget, volPath)).To(Succeed()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| procCfg.AdditionalVolumes = append(procCfg.AdditionalVolumes, config.Volume{Path: volPath}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| It("refuses to configure it and does not chown the host target to vcap", func() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _, _, err := runcAdapter.CreateJobPrerequisites(bpmCfg, procCfg, user) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Expect(ownerUID(hostTarget)).To(Equal(uint32(0)), "host target was chowned to vcap — CONTAINER ESCAPE") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Expect(err).To(HaveOccurred()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Expect(err.Error()).To(ContainSubstring("refusing to configure additional volume at symlink")) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Context("when the attacker plants a symlink at an intermediate component of an additional volume", func() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| BeforeEach(func() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // A nested volume path below a vcap-writable directory. The | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // attacker controls an intermediate component, not the leaf — | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // the case a leaf-only guard misses. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| volParent := filepath.Join(systemRoot, "vcap-writable") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Expect(os.MkdirAll(volParent, 0770)).To(Succeed()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Expect(os.Symlink(hostTarget, filepath.Join(volParent, "sub"))).To(Succeed()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| procCfg.AdditionalVolumes = append(procCfg.AdditionalVolumes, config.Volume{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Path: filepath.Join(volParent, "sub", "leaf"), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| It("refuses to follow the intermediate symlink and does not create or chown under the target", func() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _, _, err := runcAdapter.CreateJobPrerequisites(bpmCfg, procCfg, user) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Expect(err).To(HaveOccurred()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Expect(err.Error()).To(ContainSubstring("refusing to create directory through symlink")) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Expect(ownerUID(hostTarget)).To(Equal(uint32(0)), "host target was chowned to vcap — CONTAINER ESCAPE") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _, statErr := os.Lstat(filepath.Join(hostTarget, "leaf")) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Expect(os.IsNotExist(statErr)).To(BeTrue(), "leaf was created under the host target through the intermediate symlink") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+494
to
+517
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🛡️ Detected with Advanced Tier | 🔵 Trivial | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: set -eu
printf '%s\n' '--- relevant test block ---'
nl -ba src/bpm/runc/adapter/adapter_test.go | sed -n '430,535p'
printf '%s\n' '--- implementation references ---'
rg -n -F -- 'MkdirAllChown' src/bpm/runc/adapter
rg -n -F -- 'refusing to create directory through symlink' src
printf '%s\n' '--- implementation outline ---'
ast-grep outline src/bpm/runc/adapter/adapter.go --view expanded || true
printf '%s\n' '--- implementation excerpt ---'
rg -n -F -- 'func MkdirAllChown' src || true
git diff --no-ext-diff --unified=40 25aa898e1a8b672c259d23f6acac43ef91c53781 939c37d4db29d4d29ecaad986033cb6f47ed1ab7 -- src/bpm/runc/adapter/adapter.go src/bpm/runc/adapter/adapter_test.goRepository: cloudfoundry/bpm-release Length of output: 26013 🏁 Script executed: set -eu
printf '%s\n' '--- relevant test block ---'
nl -ba src/bpm/runc/adapter/adapter_test.go | sed -n '430,535p'
printf '%s\n' '--- implementation references ---'
rg -n -F -- 'MkdirAllChown' src/bpm/runc/adapter
rg -n -F -- 'refusing to create directory through symlink' src
printf '%s\n' '--- implementation excerpt ---'
rg -n -F -- 'func MkdirAllChown' src || true
printf '%s\n' '--- PR diff ---'
git diff --no-ext-diff --unified=40 25aa898e1a8b672c259d23f6acac43ef91c53781 939c37d4db29d4d29ecaad986033cb6f47ed1ab7 -- src/bpm/runc/adapter/adapter.go src/bpm/runc/adapter/adapter_test.goRepository: cloudfoundry/bpm-release Length of output: 23978 🏁 Script executed: set -eu
nl -ba src/bpm/runc/adapter/adapter_test.go | sed -n '430,535p'
rg -n -F -- 'MkdirAllChown' src/bpm/runc/adapter
rg -n -F -- 'refusing to create directory through symlink' src
git diff --no-ext-diff --unified=40 25aa898e1a8b672c259d23f6acac43ef91c53781 939c37d4db29d4d29ecaad986033cb6f47ed1ab7 -- src/bpm/runc/adapter/adapter.go src/bpm/runc/adapter/adapter_test.goRepository: cloudfoundry/bpm-release Length of output: 23787 🏁 Script executed: set -eu
printf '%s\n' '--- safeio helper ---'
nl -ba src/bpm/safeio/safeio.go | sed -n '70,125p'
printf '%s\n' '--- focused safeio tests ---'
nl -ba src/bpm/safeio/safeio_test.go | sed -n '185,290p'Repository: cloudfoundry/bpm-release Length of output: 7005 🏁 Script executed: set -eu
nl -ba src/bpm/safeio/safeio.go | sed -n '70,125p'
nl -ba src/bpm/safeio/safeio_test.go | sed -n '185,290p'Repository: cloudfoundry/bpm-release Length of output: 6954 🏁 Script executed: set -eu
nl -ba src/bpm/safeio/safeio.go | sed -n '125,155p'Repository: cloudfoundry/bpm-release Length of output: 731 Path Traversal Reachability: Unreachable Add coverage for an existing leaf behind the intermediate symlink. When Extend the intermediate-symlink setup and assertions BeforeEach(func() {
// A nested volume path below a vcap-writable directory. The
// attacker controls an intermediate component, not the leaf —
// the case a leaf-only guard misses.
volParent := filepath.Join(systemRoot, "vcap-writable")
Expect(os.MkdirAll(volParent, 0770)).To(Succeed())
Expect(os.Symlink(hostTarget, filepath.Join(volParent, "sub"))).To(Succeed())
+ Expect(os.Mkdir(filepath.Join(hostTarget, "leaf"), 0755)).To(Succeed())
procCfg.AdditionalVolumes = append(procCfg.AdditionalVolumes, config.Volume{
Path: filepath.Join(volParent, "sub", "leaf"),
})
})
@@
Expect(ownerUID(hostTarget)).To(Equal(uint32(0)), "host target was chowned to vcap — CONTAINER ESCAPE")
- _, statErr := os.Lstat(filepath.Join(hostTarget, "leaf"))
- Expect(os.IsNotExist(statErr)).To(BeTrue(), "leaf was created under the host target through the intermediate symlink")
+ leafInfo, statErr := os.Lstat(filepath.Join(hostTarget, "leaf"))
+ Expect(statErr).NotTo(HaveOccurred())
+ Expect(leafInfo.Mode() & os.ModePerm).To(Equal(os.FileMode(0755)))
+ Expect(leafInfo.Sys().(*syscall.Stat_t).Uid).To(Equal(uint32(0)))
})📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Context("when the user requests an ephemeral disk", func() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| BeforeEach(func() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| procCfg.EphemeralDisk = true | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,15 +15,19 @@ | |
|
|
||
| // Package safeio provides helpers for performing privileged file operations | ||
| // against paths that live in directories writable by less-privileged users | ||
| // (e.g. job log directories). The helpers use O_NOFOLLOW + fchown so that a | ||
| // pre-planted symlink at the leaf path cannot redirect the operation onto an | ||
| // arbitrary host file. | ||
| // (e.g. job log directories). The helpers use O_NOFOLLOW, Lstat, and lchown | ||
| // so that a pre-planted symlink cannot redirect the operation onto an | ||
| // arbitrary host file. MkdirAllChown checks every component of the path; | ||
| // OpenAppendChown and Lchown operate on an existing parent and so guard the | ||
| // leaf only. | ||
| package safeio | ||
|
|
||
| import ( | ||
| "errors" | ||
| "fmt" | ||
| "os" | ||
| "path/filepath" | ||
| "strings" | ||
|
|
||
| "golang.org/x/sys/unix" | ||
| ) | ||
|
|
@@ -54,3 +58,80 @@ func OpenAppendChown(path string, uid, gid int, perm os.FileMode) (*os.File, err | |
|
|
||
| return f, nil | ||
| } | ||
|
|
||
| // MkdirAllChown creates a directory (and any parent directories) with 0700 | ||
| // permissions and chowns the leaf directory to the specified uid and gid. | ||
| // | ||
| // The path is walked one component at a time from the filesystem root, and | ||
| // every component is opened with O_NOFOLLOW. A symlink anywhere in the path — | ||
| // not just at the leaf — is refused. Checking only the leaf is not sufficient: | ||
| // an attacker who can plant a symlink at an intermediate component redirects | ||
| // the whole subtree, so MkdirAll would create the leaf under the symlink's | ||
| // target and the subsequent chown would hand that target to uid/gid. | ||
| // | ||
| // The final chown is issued against the open directory descriptor (fchown(2)), | ||
| // so the path cannot be swapped between the walk and the ownership change. | ||
| // | ||
| // Because every component is checked, a legitimate symlink in the path prefix | ||
| // is rejected too. Callers must pass paths whose ancestors are real | ||
| // directories. | ||
| func MkdirAllChown(path string, uid, gid int) error { | ||
| if !filepath.IsAbs(path) { | ||
| return fmt.Errorf("refusing to create directory at relative path %s", path) | ||
| } | ||
|
|
||
| // O_PATH would be enough to walk, but it makes O_NOFOLLOW return a | ||
| // descriptor to the symlink itself rather than ELOOP, so use a real | ||
| // directory open instead. | ||
| parent, err := unix.Open("/", unix.O_RDONLY|unix.O_DIRECTORY|unix.O_CLOEXEC, 0) | ||
| if err != nil { | ||
| return fmt.Errorf("open /: %w", err) | ||
| } | ||
| defer func() { _ = unix.Close(parent) }() //nolint:errcheck | ||
|
|
||
| for _, component := range strings.Split(strings.Trim(filepath.Clean(path), "/"), "/") { | ||
| if err := unix.Mkdirat(parent, component, 0700); err != nil && !errors.Is(err, unix.EEXIST) { | ||
| return fmt.Errorf("mkdir %s: %w", path, err) | ||
| } | ||
|
|
||
| // Classify the component before opening it. Errno alone is not | ||
| // portable here: with O_DIRECTORY set, a symlink reports ENOTDIR on | ||
| // some platforms and ELOOP on others. | ||
| var st unix.Stat_t | ||
| if err := unix.Fstatat(parent, component, &st, unix.AT_SYMLINK_NOFOLLOW); err != nil { | ||
| return fmt.Errorf("stat %s: %w", path, err) | ||
| } | ||
| switch st.Mode & unix.S_IFMT { | ||
| case unix.S_IFLNK: | ||
| return fmt.Errorf("refusing to create directory through symlink at %s", path) | ||
| case unix.S_IFDIR: | ||
| default: | ||
| return fmt.Errorf("path %s exists and is not a directory", path) | ||
| } | ||
|
Comment on lines
+92
to
+110
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🔴 Critical | 🏗️ Heavy lift 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
rg -n -C3 'sys/log|ln -s|symlink' --glob '!**/*_test.go' . | head -80
rg -n -C3 'func \(.*\) (LogDir|SocketDir|TempDir|DataDir|StoreDir)\(' src/bpm/configRepository: cloudfoundry/bpm-release Length of output: 6188 🏁 Script executed: set -o pipefail
printf '%s\n' '--- diff against merge base ---'
git diff --no-ext-diff --unified=40 25aa898e1a8b672c259d23f6acac43ef91c53781 939c37d4db29d4d29ecaad986033cb6f47ed1ab7 -- src/bpm/safeio/safeio.go src/bpm | sed -n '1,360p'
printf '%s\n' '--- safeio declarations and callers ---'
rg -n -F --glob '*.go' -e 'MkdirAllChown' -e 'createDirFor' -e 'setupProcess' -e 'LogDir()' src/bpm
printf '%s\n' '--- safeio complete file ---'
nl -ba src/bpm/safeio/safeio.go | sed -n '1,240p'
printf '%s\n' '--- config and caller context ---'
nl -ba src/bpm/config/bpm_config.go | sed -n '1,140p'
rg -n -C 8 -F --glob '*.go' -e 'createDirFor' -e 'failed to create system files' -e 'LogDir().External()' -e 'LogDir().String()' src
printf '%s\n' '--- path method definitions ---'
rg -n -F --glob '*.go' -e 'func (.*) LogDir' -e 'func (.*) DataDir' -e 'func (.*) StoreDir' -e 'func (.*) RunDir' -e 'type Bosh' -e 'type Environment' src vendor 2>/dev/null | head -160Repository: cloudfoundry/bpm-release Length of output: 33341 🏁 Script executed: set -o pipefail
printf '%s\n' '--- changed safeio and callers ---'
git diff --no-ext-diff --unified=25 25aa898e1a8b672c259d23f6acac43ef91c53781 939c37d4db29d4d29ecaad986033cb6f47ed1ab7 -- src/bpm/safeio/safeio.go src/bpm | sed -n '1,420p'
printf '%s\n' '--- exact caller references ---'
rg -n -F --glob '*.go' -e 'MkdirAllChown' -e 'createDirFor' -e 'setupProcess' -e 'failed to create system files' -e 'LogDir().External()' src/bpm || test "$?" -eq 1
printf '%s\n' '--- safeio source ---'
nl -ba src/bpm/safeio/safeio.go | sed -n '1,240p'
printf '%s\n' '--- config source ---'
nl -ba src/bpm/config/bpm_config.go | sed -n '70,120p'
printf '%s\n' '--- path definitions ---'
rg -n -F --glob '*.go' -e 'LogDir(' -e 'DataDir(' -e 'StoreDir(' -e 'RunDir(' -e 'type Bosh' -e 'type Environment' src vendor 2>/dev/null | head -240Repository: cloudfoundry/bpm-release Length of output: 29394 🏁 Script executed: set -o pipefail
git diff --no-ext-diff --unified=25 25aa898e1a8b672c259d23f6acac43ef91c53781 939c37d4db29d4d29ecaad986033cb6f47ed1ab7 -- src/bpm/safeio/safeio.go src/bpm | sed -n '1,420p'
printf '%s\n' '--- references ---'
rg -n -F --glob '*.go' -e 'MkdirAllChown' -e 'createDirFor' -e 'setupProcess' -e 'failed to create system files' -e 'LogDir().External()' src/bpm || test "$?" -eq 1
printf '%s\n' '--- safeio ---'
nl -ba src/bpm/safeio/safeio.go | sed -n '1,240p'
printf '%s\n' '--- config ---'
nl -ba src/bpm/config/bpm_config.go | sed -n '70,120p'
printf '%s\n' '--- path definitions ---'
rg -n -F --glob '*.go' -e 'LogDir(' -e 'DataDir(' -e 'StoreDir(' -e 'RunDir(' -e 'type Bosh' -e 'type Environment' src vendor 2>/dev/null | head -240Repository: cloudfoundry/bpm-release Length of output: 29332 🏁 Script executed: printf '%s\n' '--- safeio implementation ---'
nl -ba src/bpm/safeio/safeio.go | sed -n '1,240p'
printf '%s\n' '--- changed call sites ---'
rg -n -C 12 -F --glob '*.go' -e 'MkdirAllChown' -e 'createDirFor' -e 'failed to create system files' src/bpm || test "$?" -eq 1
printf '%s\n' '--- config path methods ---'
nl -ba src/bpm/config/bpm_config.go | sed -n '80,112p'
printf '%s\n' '--- BOSH path implementations ---'
rg -n -C 8 -F --glob '*.go' -e 'func (.*) LogDir(' -e 'func (.*) DataDir(' -e 'func (.*) StoreDir(' -e 'func (.*) RunDir(' vendor src 2>/dev/null | head -240Repository: cloudfoundry/bpm-release Length of output: 26228 🏁 Script executed: set -o pipefail
printf '%s\n' '--- caller ---'
rg -n -C 20 -F --glob '*.go' -e 'MkdirAllChown' -e 'createDirFor' -e 'failed to create system files' src/bpm || test "$?" -eq 1
printf '%s\n' '--- safeio ---'
nl -ba src/bpm/safeio/safeio.go | sed -n '1,180p'
printf '%s\n' '--- config binding ---'
nl -ba src/bpm/config/bpm_config.go | sed -n '80,112p'
printf '%s\n' '--- dependencies defining path methods ---'
rg -n -C 8 -F --glob '*.go' -e 'func (.*) LogDir(' -e 'func (.*) RunDir(' -e 'func (.*) DataDir(' -e 'func (.*) StoreDir(' src vendor 2>/dev/null | head -240Repository: cloudfoundry/bpm-release Length of output: 31696 🏁 Script executed: set -o pipefail
rg -n -C 15 -F --glob '*.go' 'MkdirAllChown' src
rg -n -C 12 -F --glob '*.go' 'createDirFor' src
rg -n -C 8 -F 'boshEnv' src/bpm/config/bpm_config.go
sed -n '1,80p' src/bpm/config/bpm_config.go
rg -n -C 10 --glob '*.go' 'func .*LogDir|func .*RunDir|func .*DataDir|func .*StoreDir' vendor src 2>/dev/null | head -240Repository: cloudfoundry/bpm-release Length of output: 29809 🌐 Web query:
💡 Result: 🌐 Web query:
💡 Result: Review commentAllow the trusted BOSH’s Allow symlinks only in an explicitly trusted, root-owned prefix whose parent is not writable by the container user. Continue no-follow checks for the job-specific suffix. 🤖 Prompt for AI Agents |
||
|
|
||
| // O_NOFOLLOW closes the window between the check above and the open. | ||
| child, err := unix.Openat(parent, component, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0) | ||
| if err != nil { | ||
| return fmt.Errorf("open %s: %w", path, err) | ||
| } | ||
|
|
||
| _ = unix.Close(parent) //nolint:errcheck | ||
| parent = child | ||
| } | ||
|
|
||
| return unix.Fchown(parent, uid, gid) | ||
| } | ||
|
|
||
| // Lchown changes the ownership of the named file or directory without following | ||
| // symbolic links. If the path is a symlink, it refuses to chown and returns an error. | ||
| func Lchown(path string, uid, gid int) error { | ||
| fi, err := os.Lstat(path) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| if fi.Mode()&os.ModeSymlink != 0 { | ||
| return fmt.Errorf("refusing to chown symlink at %s", path) | ||
| } | ||
|
|
||
| return os.Lchown(path, uid, gid) | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🛡️ Detected with Advanced Tier | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
Repository: cloudfoundry/bpm-release
Length of output: 27983
🏁 Script executed:
Repository: cloudfoundry/bpm-release
Length of output: 42134
Path Traversal
Reachability: External
Exploitability: Moderate
CWE: CWE-59
View Security blast radius
Walk existing volume paths without following intermediate symlinks.
os.Lstatrejects only a symlink at the final component. The lateros.Chmod(vol.Path, ...)andsafeio.Lchowncalls still resolve parent symlinks. A container process that can modify an intermediate component can redirect privileged chmod or chown operations to another host object.src/bpm/runc/adapter/adapter.go, walk the existing volume path component by component with no-follow operations. Applyfchmodandfchownto the resulting descriptor.src/bpm/safeio/safeio.go, makeLchownreject symlinks at every component, open the leaf withO_PATH|O_NOFOLLOW, and callfchownatwithAT_EMPTY_PATH.📍 Affects 2 files
src/bpm/runc/adapter/adapter.go#L93-L99(this comment)src/bpm/safeio/safeio.go#L127-L137🤖 Prompt for AI Agents