Repository: containers/podman
Pull request #25466: Do not allow mounting to machine dir /tmp
Author: login: baude
Labels: name: lgtm
description: Indicates that a PR is ready to be merged.
name: approved
description: Indicates a PR has been approved by an approver from all required OWNERS files.
name: machine
name: release-note
name: locked - please file new issue/PR
description: Assist humans wanting to comment on an old issue or PR with locked comments.
Created: 2025-03-04T21:27:00Z | Merged: 2025-03-09T05:00:09Z

the destination machine mount overwrote /tmp.  Here I have added a sanity check.  I also moved the volume parsing and check earlier in the init function so that one does not have to endure the decompression and clean up of the machine image for cli parsing.

Fixes: #18230

<!--
Thanks for sending a pull request!

Please make sure you've read our contributing guidelines and how to submit a pull request (https://github.com/containers/podman/blob/main/CONTRIBUTING.md#submitting-pull-requests).

Finally, be sure to sign commits with your real name. Since by opening
a PR you already have commits, you can add signatures if needed with
something like `git commit -s --amend`.
-->

#### Does this PR introduce a user-facing change?

<!--
If no, just write `None` in the release-note block below. If yes, a release note
is required: Enter your extended release note in the block below. If the PR
requires additional action from users switching to the new release, include the
string "action required".

For more information on release notes, please follow the Kubernetes model:
https://git.k8s.io/community/contributors/guide/release-notes.md
-->

```release-note
Fixed bug #18230 where mounting to machine mount to /tmp causes unexpected results.
```



Diff:
diff --git a/docs/source/markdown/podman-machine-init.1.md.in b/docs/source/markdown/podman-machine-init.1.md.in
index 775362a9e93..4dfc3376562 100644
--- a/docs/source/markdown/podman-machine-init.1.md.in
+++ b/docs/source/markdown/podman-machine-init.1.md.in
@@ -149,6 +149,12 @@ options are:
 * **rw**: mount volume read/write (default)
 * **security_model=[model]**: specify 9p security model (see below)
 
+Note: The following destinations are forbidden for volumes: `/bin`, `/boot`, `/dev`, `/etc`,
+`/home`, `/proc`, `/root`, `/run`, `/sbin`, `/sys`, `/tmp`, `/usr`, and `/var`. Subdirectories
+of these destinations are allowed but users should be careful to not mount to important directories
+as unexpected results may occur.
+
+
 The 9p security model [determines] https://wiki.qemu.org/Documentation/9psetup#Starting_the_Guest_directly
 if and how the 9p filesystem translates some filesystem operations before
 actual storage on the host.
diff --git a/pkg/machine/e2e/init_test.go b/pkg/machine/e2e/init_test.go
index 8e3bc72b925..06b1024eb1b 100644
--- a/pkg/machine/e2e/init_test.go
+++ b/pkg/machine/e2e/init_test.go
@@ -79,6 +79,23 @@ var _ = Describe("podman machine init", func() {
 		Expect(badMemSession).To(Exit(125))
 	})
 
+	It("init volume check", func() {
+		skipIfWSL("WSL does not use volume mounts")
+		// Check that mounting to certain target directories like /tmp at the / level is NOT ok
+		tmpVol := initMachine{}
+		targetMount := "/tmp"
+		tmpVolSession, err := mb.setCmd(tmpVol.withVolume(fmt.Sprintf("/whatever:%s", targetMount))).run()
+		Expect(err).ToNot(HaveOccurred())
+		Expect(tmpVolSession).To(Exit(125))
+		Expect(tmpVolSession.errorToString()).To(ContainSubstring(fmt.Sprintf("Error: machine mount destination cannot be %q: consider another location or a subdirectory of an existing location", targetMount)))
+
+		// Mounting to /tmp/foo (subdirectory) is OK
+		tmpSubdir := initMachine{}
+		tmpSubDirSession, err := mb.setCmd(tmpSubdir.withVolume("/whatever:/tmp/foo")).run()
+		Expect(err).ToNot(HaveOccurred())
+		Expect(tmpSubDirSession).To(Exit(0))
+	})
+
 	It("simple init", func() {
 		i := new(initMachine)
 		session, err := mb.setCmd(i.withImage(mb.imagePath)).run()
diff --git a/pkg/machine/shim/host.go b/pkg/machine/shim/host.go
index 82e70be8fa6..1d08fd643dd 100644
--- a/pkg/machine/shim/host.go
+++ b/pkg/machine/shim/host.go
@@ -6,6 +6,7 @@ import (
 	"fmt"
 	"io"
 	"os"
+	"path"
 	"path/filepath"
 	"runtime"
 	"strings"
@@ -118,6 +119,18 @@ func Init(opts machineDefine.InitOptions, mp vmconfigs.VMProvider) error {
 		createOpts.UserModeNetworking = *umn
 	}
 
+	// Mounts
+	if mp.VMType() != machineDefine.WSLVirt {
+		mc.Mounts = CmdLineVolumesToMounts(opts.Volumes, mp.MountType())
+	}
+
+	// Issue #18230 ... do not mount over important directories at the / level (subdirs are fine)
+	for _, mnt := range mc.Mounts {
+		if err := validateDestinationPaths(mnt.Target); err != nil {
+			return err
+		}
+	}
+
 	// Get Image
 	// TODO This needs rework bigtime; my preference is most of below of not living in here.
 	// ideally we could get a func back that pulls the image, and only do so IF everything works because
@@ -251,11 +264,6 @@ func Init(opts machineDefine.InitOptions, mp vmconfigs.VMProvider) error {
 	}
 	ignBuilder.WithUnit(readyUnit)
 
-	// Mounts
-	if mp.VMType() != machineDefine.WSLVirt {
-		mc.Mounts = CmdLineVolumesToMounts(opts.Volumes, mp.MountType())
-	}
-
 	// TODO AddSSHConnectionToPodmanSocket could take an machineconfig instead
 	if err := connection.AddSSHConnectionsToPodmanSocket(mc.HostUser.UID, mc.SSH.Port, mc.SSH.IdentityPath, mc.Name, mc.SSH.RemoteUsername, opts); err != nil {
 		return err
@@ -774,3 +782,27 @@ func Reset(mps []vmconfigs.VMProvider, opts machine.ResetOptions) error {
 	}
 	return resetErrors.ErrorOrNil()
 }
+
+func validateDestinationPaths(dest string) error {
+	// illegalMounts are locations at the / level of the podman machine where we do want users mounting directly over
+	illegalMounts := map[string]struct{}{
+		"/bin":  {},
+		"/boot": {},
+		"/dev":  {},
+		"/etc":  {},
+		"/home": {},
+		"/proc": {},
+		"/root": {},
+		"/run":  {},
+		"/sbin": {},
+		"/sys":  {},
+		"/tmp":  {},
+		"/usr":  {},
+		"/var":  {},
+	}
+	mountTarget := path.Clean(dest)
+	if _, ok := illegalMounts[mountTarget]; ok {
+		return fmt.Errorf("machine mount destination cannot be %q: consider another location or a subdirectory of an existing location", mountTarget)
+	}
+	return nil
+}
diff --git a/pkg/machine/shim/host_test.go b/pkg/machine/shim/host_test.go
new file mode 100644
index 00000000000..3a5473c1560
--- /dev/null
+++ b/pkg/machine/shim/host_test.go
@@ -0,0 +1,61 @@
+package shim
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func Test_validateDestinationPaths(t *testing.T) {
+	tests := []struct {
+		name    string
+		dest    string
+		wantErr bool
+	}{
+		{
+			name:    "Expect fail - /tmp",
+			dest:    "/tmp",
+			wantErr: true,
+		},
+		{
+			name:    "Expect fail trailing /",
+			dest:    "/tmp/",
+			wantErr: true,
+		},
+		{
+			name:    "Expect fail double /",
+			dest:    "//tmp",
+			wantErr: true,
+		},
+		{
+			name:    "/var should fail",
+			dest:    "/var",
+			wantErr: true,
+		},
+		{
+			name:    "/etc should fail",
+			dest:    "/etc",
+			wantErr: true,
+		},
+		{
+			name:    "/tmp subdir OK",
+			dest:    "/tmp/foobar",
+			wantErr: false,
+		},
+		{
+			name:    "/foobar OK",
+			dest:    "/foobar",
+			wantErr: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			err := validateDestinationPaths(tt.dest)
+			if tt.wantErr {
+				assert.ErrorContainsf(t, err, "onsider another location or a subdirectory of an existing location", "illegal mount target")
+			} else {
+				assert.NoError(t, err, "mounts to subdirs or non-critical dirs should succeed")
+			}
+		})
+	}
+}


Reviews:
user: login: Luap99
body: Makes sense, however that raises the question if we restrict `/tmp` why no other paths. Surely mounting over `/etc`, `/var`, `/usr`, `/run` and many more would brick the system in possibly even worse ways.  I even have seen people mounting `/var/lib/containers` (or the home dir path) which cannot work as our virtofs mount seem to be limted to a single uid so it cannot work as universal storage path.

So it would be better to rework that to block a defined list of mounts I think.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T11:16:48Z
user: login: baude
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T13:46:12Z
user: login: jakecorrenti
body: LGTM
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-05T21:52:47Z
user: login: Luap99
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T12:42:56Z
user: login: baude
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T14:56:14Z
user: login: baude
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T15:09:56Z
user: login: Luap99
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T15:11:01Z
user: login: baude
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T15:42:03Z
user: login: Luap99
body: LGTM
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-06T15:48:04Z
user: login: Luap99
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T17:31:37Z
user: login: Luap99
body: /lgtm
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-07T16:05:28Z

Review comments:
diff_hunk: @@ -118,6 +118,19 @@ func Init(opts machineDefine.InitOptions, mp vmconfigs.VMProvider) error {
 		createOpts.UserModeNetworking = *umn
 	}
 
+	// Mounts
+	if mp.VMType() != machineDefine.WSLVirt {
+		mc.Mounts = CmdLineVolumesToMounts(opts.Volumes, mp.MountType())
+	}
+
+	for _, mnt := range mc.Mounts {
+		// Issue #18230 ... cannot mount over /tmp
+		mountTarget, _ := strings.CutSuffix(mnt.Target, "/")
path: pkg/machine/shim/host.go
user: login: Luap99
body: I have not checked how our path mounting handles non sanitized inputs but `//tmp` `/tmp//` etc are all the same path. The way to clean them is using `path.Clean()`. Note *path* not *filepath* as filepath on windows would use `\` but as this is the target path it must be using `/`
created_at: 2025-03-05T11:11:10Z
author_association: MEMBER
diff_hunk: @@ -118,6 +118,19 @@ func Init(opts machineDefine.InitOptions, mp vmconfigs.VMProvider) error {
 		createOpts.UserModeNetworking = *umn
 	}
 
+	// Mounts
+	if mp.VMType() != machineDefine.WSLVirt {
+		mc.Mounts = CmdLineVolumesToMounts(opts.Volumes, mp.MountType())
+	}
+
+	for _, mnt := range mc.Mounts {
+		// Issue #18230 ... cannot mount over /tmp
+		mountTarget, _ := strings.CutSuffix(mnt.Target, "/")
path: pkg/machine/shim/host.go
user: login: baude
body: I like that much better.  Added this and another test.
created_at: 2025-03-05T13:46:12Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,75 @@
+package shim
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func Test_validateDestinationPaths(t *testing.T) {
+	type args struct {
+		dest string
+	}
+	tests := []struct {
+		name    string
+		args    args
+		wantErr bool
+	}{
+		{
+			name:    "Expect fail - /tmp",
+			args:    args{"/tmp"},
+			wantErr: true,
+		},
+		{
+			name: "Expect fail trailing /",
+			args: args{
+				dest: "/tmp/",
+			},
+			wantErr: true,
+		},
+		{
+			name: "Expect fail double /",
+			args: args{
+				dest: "//tmp",
+			},
+			wantErr: true,
+		},
+		{
+			name: "/var should fail",
+			args: args{
+				dest: "/var",
+			},
+			wantErr: true,
+		},
+		{
+			name: "/etc should fail",
+			args: args{
+				dest: "/etc",
+			},
+			wantErr: true,
+		},
+		{
+			name: "/tmp subdir OK",
+			args: args{
+				dest: "/tmp/foobar",
+			},
+			wantErr: false,
+		},
+		{
+			name: "/foobar OK",
+			args: args{
+				dest: "/foobar",
+			},
+			wantErr: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			err := validateDestinationPaths(tt.args.dest)
+			if tt.wantErr {
+				assert.Error(t, err, "illegal mount target should trip error")
+				assert.ErrorContainsf(t, err, "onsider another location or a subdirectory of an existing location", "illegal mount target")
+			}
path: pkg/machine/shim/host_test.go
user: login: Luap99
body: 
This seems to be missing to check for no error on the success case!
assert.NoError()
created_at: 2025-03-06T12:36:54Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,75 @@
+package shim
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func Test_validateDestinationPaths(t *testing.T) {
+	type args struct {
+		dest string
+	}
+	tests := []struct {
+		name    string
+		args    args
+		wantErr bool
+	}{
+		{
+			name:    "Expect fail - /tmp",
+			args:    args{"/tmp"},
+			wantErr: true,
+		},
+		{
+			name: "Expect fail trailing /",
+			args: args{
+				dest: "/tmp/",
+			},
+			wantErr: true,
+		},
+		{
+			name: "Expect fail double /",
+			args: args{
+				dest: "//tmp",
+			},
+			wantErr: true,
+		},
+		{
+			name: "/var should fail",
+			args: args{
+				dest: "/var",
+			},
+			wantErr: true,
+		},
+		{
+			name: "/etc should fail",
+			args: args{
+				dest: "/etc",
+			},
+			wantErr: true,
+		},
+		{
+			name: "/tmp subdir OK",
+			args: args{
+				dest: "/tmp/foobar",
+			},
+			wantErr: false,
+		},
+		{
+			name: "/foobar OK",
+			args: args{
+				dest: "/foobar",
+			},
+			wantErr: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			err := validateDestinationPaths(tt.args.dest)
+			if tt.wantErr {
+				assert.Error(t, err, "illegal mount target should trip error")
+				assert.ErrorContainsf(t, err, "onsider another location or a subdirectory of an existing location", "illegal mount target")
path: pkg/machine/shim/host_test.go
user: login: Luap99
body: I suppose just ErrorContainsf() is enough here as this would already fail if there is no error.
created_at: 2025-03-06T12:37:24Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,75 @@
+package shim
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func Test_validateDestinationPaths(t *testing.T) {
+	type args struct {
+		dest string
+	}
+	tests := []struct {
+		name    string
+		args    args
+		wantErr bool
+	}{
+		{
+			name:    "Expect fail - /tmp",
+			args:    args{"/tmp"},
path: pkg/machine/shim/host_test.go
user: login: Luap99
body: This is inconsistent with the rest, overall I see no need for the args struct, might as well just inline the dest filed into the tests struct.
created_at: 2025-03-06T12:38:56Z
author_association: MEMBER
diff_hunk: @@ -149,6 +149,11 @@ options are:
 * **rw**: mount volume read/write (default)
 * **security_model=[model]**: specify 9p security model (see below)
 
+Note: The following destinations are forbidden for volumes: `/bin`, `/boot`, `/dev`, `/etc`,
+`/home`, `/proc`, `/root`, `/run`, `/sbin`, `/sys`, `/tmp`, `/usr`, and `/var`. Subdirectories
+of these destinations are allowed.
path: docs/source/markdown/podman-machine-init.1.md.in
user: login: Luap99
body: While we do allow subdirectories, it is still not recommend, maybe it should be made clear that mounting over existing important system directories is not supported and can lead to many issues.
created_at: 2025-03-06T12:42:37Z
author_association: MEMBER
diff_hunk: @@ -149,6 +149,11 @@ options are:
 * **rw**: mount volume read/write (default)
 * **security_model=[model]**: specify 9p security model (see below)
 
+Note: The following destinations are forbidden for volumes: `/bin`, `/boot`, `/dev`, `/etc`,
+`/home`, `/proc`, `/root`, `/run`, `/sbin`, `/sys`, `/tmp`, `/usr`, and `/var`. Subdirectories
+of these destinations are allowed.
path: docs/source/markdown/podman-machine-init.1.md.in
user: login: baude
body: with the drift of this PR, it has lost some of its meaning.  In the case of /tmp, subdirs are fine as you point out but in that case I have no problem recommending it.  /var on the other hand, yeah ... not a good idea imho
created_at: 2025-03-06T14:56:13Z
author_association: MEMBER
diff_hunk: @@ -149,6 +149,11 @@ options are:
 * **rw**: mount volume read/write (default)
 * **security_model=[model]**: specify 9p security model (see below)
 
+Note: The following destinations are forbidden for volumes: `/bin`, `/boot`, `/dev`, `/etc`,
+`/home`, `/proc`, `/root`, `/run`, `/sbin`, `/sys`, `/tmp`, `/usr`, and `/var`. Subdirectories
+of these destinations are allowed.
path: docs/source/markdown/podman-machine-init.1.md.in
user: login: baude
body: i added in some text to say users need to be careful.
created_at: 2025-03-06T15:09:56Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,63 @@
+package shim
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func Test_validateDestinationPaths(t *testing.T) {
+	tests := []struct {
+		name    string
+		dest    string
+		wantErr bool
+	}{
+		{
+			name:    "Expect fail - /tmp",
+			dest:    "/tmp",
+			wantErr: true,
+		},
+		{
+			name:    "Expect fail trailing /",
+			dest:    "/tmp/",
+			wantErr: true,
+		},
+		{
+			name:    "Expect fail double /",
+			dest:    "//tmp",
+			wantErr: true,
+		},
+		{
+			name:    "/var should fail",
+			dest:    "/var",
+			wantErr: true,
+		},
+		{
+			name:    "/etc should fail",
+			dest:    "/etc",
+			wantErr: true,
+		},
+		{
+			name:    "/tmp subdir OK",
+			dest:    "/tmp/foobar",
+			wantErr: false,
+		},
+		{
+			name:    "/foobar OK",
+			dest:    "/foobar",
+			wantErr: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			err := validateDestinationPaths(tt.dest)
+			fmt.Println("==> " + tt.dest)
path: pkg/machine/shim/host_test.go
user: login: Luap99
body: debug leftover?
created_at: 2025-03-06T15:11:01Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,63 @@
+package shim
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func Test_validateDestinationPaths(t *testing.T) {
+	tests := []struct {
+		name    string
+		dest    string
+		wantErr bool
+	}{
+		{
+			name:    "Expect fail - /tmp",
+			dest:    "/tmp",
+			wantErr: true,
+		},
+		{
+			name:    "Expect fail trailing /",
+			dest:    "/tmp/",
+			wantErr: true,
+		},
+		{
+			name:    "Expect fail double /",
+			dest:    "//tmp",
+			wantErr: true,
+		},
+		{
+			name:    "/var should fail",
+			dest:    "/var",
+			wantErr: true,
+		},
+		{
+			name:    "/etc should fail",
+			dest:    "/etc",
+			wantErr: true,
+		},
+		{
+			name:    "/tmp subdir OK",
+			dest:    "/tmp/foobar",
+			wantErr: false,
+		},
+		{
+			name:    "/foobar OK",
+			dest:    "/foobar",
+			wantErr: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			err := validateDestinationPaths(tt.dest)
+			fmt.Println("==> " + tt.dest)
path: pkg/machine/shim/host_test.go
user: login: baude
body: yup!
created_at: 2025-03-06T15:42:03Z
author_association: MEMBER
diff_hunk: @@ -77,6 +77,20 @@ var _ = Describe("podman machine init", func() {
 		badMemSession, err := mb.setCmd(badMem.withMemory(uint(total))).run()
 		Expect(err).ToNot(HaveOccurred())
 		Expect(badMemSession).To(Exit(125))
+
+		// Check that mounting to certain target directories like /tmp at the / level is NOT ok
+		tmpVol := initMachine{}
+		targetMount := "/tmp"
+		tmpVolSession, err := mb.setCmd(tmpVol.withVolume(fmt.Sprintf("/whatever:%s", targetMount))).run()
+		Expect(err).ToNot(HaveOccurred())
+		Expect(tmpVolSession).To(Exit(125))
path: pkg/machine/e2e/init_test.go
user: login: Luap99
body: this is failing on WSL because it ignores all volumes so not error is produced.

https://api.cirrus-ci.com/v1/artifact/task/5435362996125696/html/machine-wsl-podman-windows-rootless-host-sqlite.log.html#t--podman-machine-init-bad-init--1
created_at: 2025-03-06T17:31:37Z
author_association: MEMBER

PR comments:
user: login: baude
created_at: 2025-03-05T02:47:21Z
body: i'm thinking i should probably add a test that checks that `/tmp/foo` is a valid mount 
author_association: MEMBER
user: login: packit-as-a-service[bot]
created_at: 2025-03-05T13:44:28Z
body: Ephemeral COPR build failed. @containers/packit-build please check.
author_association: NONE
user: login: baude
created_at: 2025-03-05T14:09:39Z
body: > however that raises the question if we restrict `/tmp` why no other paths.

because we are fixing bugs on specific bug reports.  that said, I am fine with reworking this again.  Are /tmp /etc/ /var the only ones you want restricted? How about /dev , /usr , /proc /sys /bin?
author_association: MEMBER
user: login: Luap99
created_at: 2025-03-05T14:17:13Z
body: How about /bin /boot /dev /etc /home /proc /root /run  /sbin /sys /tmp /usr /var?
That should cover all the important root dirs I think. Of course mounting a sub directory might still mess things up but it is impossible to catch all while allowing generic mounts.
author_association: MEMBER
user: login: openshift-ci[bot]
created_at: 2025-03-07T16:05:34Z
body: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/containers/podman/pull/25466#" title="Author self-approved">baude</a>*, *<a href="https://github.com/containers/podman/pull/25466#pullrequestreview-2662590664" title="Approved">jakecorrenti</a>*, *<a href="https://github.com/containers/podman/pull/25466#pullrequestreview-2667719751" title="LGTM">Luap99</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=containers%2Fpodman).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/containers/podman/blob/main/OWNERS)~~ [Luap99,baude]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
author_association: CONTRIBUTOR