2018-02-05 21:05:59 +00:00
package container // import "github.com/docker/docker/integration/container"
2017-12-18 21:02:23 +00:00
import (
"context"
"fmt"
2022-10-09 15:55:42 +00:00
"os"
2018-01-18 21:55:27 +00:00
"path/filepath"
2022-10-09 15:55:42 +00:00
"syscall"
2017-12-18 21:02:23 +00:00
"testing"
2018-10-10 10:20:13 +00:00
"time"
2017-12-18 21:02:23 +00:00
"github.com/docker/docker/api/types"
2018-10-10 10:20:13 +00:00
containertypes "github.com/docker/docker/api/types/container"
mounttypes "github.com/docker/docker/api/types/mount"
2018-02-04 17:38:04 +00:00
"github.com/docker/docker/api/types/network"
2018-10-10 10:20:13 +00:00
"github.com/docker/docker/api/types/versions"
2018-02-04 17:38:04 +00:00
"github.com/docker/docker/client"
2018-10-10 10:20:13 +00:00
"github.com/docker/docker/integration/internal/container"
2023-04-05 11:32:03 +00:00
"github.com/docker/docker/pkg/parsers/kernel"
2020-03-13 23:38:24 +00:00
"github.com/moby/sys/mount"
2020-11-07 07:06:44 +00:00
"github.com/moby/sys/mountinfo"
2020-02-07 13:39:24 +00:00
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/fs"
"gotest.tools/v3/poll"
"gotest.tools/v3/skip"
2017-12-18 21:02:23 +00:00
)
2018-02-04 17:38:04 +00:00
func TestContainerNetworkMountsNoChown ( t * testing . T ) {
// chown only applies to Linux bind mounted volumes; must be same host to verify
2019-01-03 11:32:05 +00:00
skip . If ( t , testEnv . IsRemoteDaemon )
2018-02-04 17:38:04 +00:00
defer setupTest ( t ) ( )
ctx := context . Background ( )
tmpDir := fs . NewDir ( t , "network-file-mounts" , fs . WithMode ( 0755 ) , fs . WithFile ( "nwfile" , "network file bind mount" , fs . WithMode ( 0644 ) ) )
defer tmpDir . Remove ( )
tmpNWFileMount := tmpDir . Join ( "nwfile" )
2018-10-10 10:20:13 +00:00
config := containertypes . Config {
2018-02-04 17:38:04 +00:00
Image : "busybox" ,
}
2018-10-10 10:20:13 +00:00
hostConfig := containertypes . HostConfig {
Mounts : [ ] mounttypes . Mount {
2018-02-04 17:38:04 +00:00
{
Type : "bind" ,
Source : tmpNWFileMount ,
Target : "/etc/resolv.conf" ,
} ,
{
Type : "bind" ,
Source : tmpNWFileMount ,
Target : "/etc/hostname" ,
} ,
{
Type : "bind" ,
Source : tmpNWFileMount ,
Target : "/etc/hosts" ,
} ,
} ,
}
2019-01-03 21:49:00 +00:00
cli , err := client . NewClientWithOpts ( client . FromEnv )
2018-03-13 19:28:34 +00:00
assert . NilError ( t , err )
2018-02-04 17:38:04 +00:00
defer cli . Close ( )
2020-03-19 20:54:48 +00:00
ctrCreate , err := cli . ContainerCreate ( ctx , & config , & hostConfig , & network . NetworkingConfig { } , nil , "" )
2018-03-13 19:28:34 +00:00
assert . NilError ( t , err )
2018-02-04 17:38:04 +00:00
// container will exit immediately because of no tty, but we only need the start sequence to test the condition
err = cli . ContainerStart ( ctx , ctrCreate . ID , types . ContainerStartOptions { } )
2018-03-13 19:28:34 +00:00
assert . NilError ( t , err )
2018-02-04 17:38:04 +00:00
2018-02-07 00:19:38 +00:00
// Check that host-located bind mount network file did not change ownership when the container was started
// Note: If the user specifies a mountpath from the host, we should not be
// attempting to chown files outside the daemon's metadata directory
// (represented by `daemon.repository` at init time).
// This forces users who want to use user namespaces to handle the
// ownership needs of any external files mounted as network files
// (/etc/resolv.conf, /etc/hosts, /etc/hostname) separately from the
// daemon. In all other volume/bind mount situations we have taken this
// same line--we don't chown host file content.
// See GitHub PR 34224 for details.
2022-10-09 15:55:42 +00:00
info , err := os . Stat ( tmpNWFileMount )
2018-03-13 19:28:34 +00:00
assert . NilError ( t , err )
2022-10-09 15:55:42 +00:00
fi := info . Sys ( ) . ( * syscall . Stat_t )
assert . Check ( t , is . Equal ( fi . Uid , uint32 ( 0 ) ) , "bind mounted network file should not change ownership from root" )
2018-02-04 17:38:04 +00:00
}
2018-01-18 21:55:27 +00:00
func TestMountDaemonRoot ( t * testing . T ) {
2019-01-03 11:32:05 +00:00
skip . If ( t , testEnv . IsRemoteDaemon )
2018-01-18 21:55:27 +00:00
2019-01-02 13:16:25 +00:00
defer setupTest ( t ) ( )
client := testEnv . APIClient ( )
2018-01-18 21:55:27 +00:00
ctx := context . Background ( )
info , err := client . Info ( ctx )
if err != nil {
t . Fatal ( err )
}
for _ , test := range [ ] struct {
desc string
2018-10-10 10:20:13 +00:00
propagation mounttypes . Propagation
expected mounttypes . Propagation
2018-01-18 21:55:27 +00:00
} {
{
desc : "default" ,
propagation : "" ,
2018-10-10 10:20:13 +00:00
expected : mounttypes . PropagationRSlave ,
2018-01-18 21:55:27 +00:00
} ,
{
desc : "private" ,
2018-10-10 10:20:13 +00:00
propagation : mounttypes . PropagationPrivate ,
2018-01-18 21:55:27 +00:00
} ,
{
desc : "rprivate" ,
2018-10-10 10:20:13 +00:00
propagation : mounttypes . PropagationRPrivate ,
2018-01-18 21:55:27 +00:00
} ,
{
desc : "slave" ,
2018-10-10 10:20:13 +00:00
propagation : mounttypes . PropagationSlave ,
2018-01-18 21:55:27 +00:00
} ,
{
desc : "rslave" ,
2018-10-10 10:20:13 +00:00
propagation : mounttypes . PropagationRSlave ,
expected : mounttypes . PropagationRSlave ,
2018-01-18 21:55:27 +00:00
} ,
{
desc : "shared" ,
2018-10-10 10:20:13 +00:00
propagation : mounttypes . PropagationShared ,
2018-01-18 21:55:27 +00:00
} ,
{
desc : "rshared" ,
2018-10-10 10:20:13 +00:00
propagation : mounttypes . PropagationRShared ,
expected : mounttypes . PropagationRShared ,
2018-01-18 21:55:27 +00:00
} ,
} {
t . Run ( test . desc , func ( t * testing . T ) {
test := test
t . Parallel ( )
propagationSpec := fmt . Sprintf ( ":%s" , test . propagation )
if test . propagation == "" {
propagationSpec = ""
}
bindSpecRoot := info . DockerRootDir + ":" + "/foo" + propagationSpec
bindSpecSub := filepath . Join ( info . DockerRootDir , "containers" ) + ":/foo" + propagationSpec
2018-10-10 10:20:13 +00:00
for name , hc := range map [ string ] * containertypes . HostConfig {
2018-01-18 21:55:27 +00:00
"bind root" : { Binds : [ ] string { bindSpecRoot } } ,
"bind subpath" : { Binds : [ ] string { bindSpecSub } } ,
"mount root" : {
2018-10-10 10:20:13 +00:00
Mounts : [ ] mounttypes . Mount {
2018-01-18 21:55:27 +00:00
{
2018-10-10 10:20:13 +00:00
Type : mounttypes . TypeBind ,
2018-01-18 21:55:27 +00:00
Source : info . DockerRootDir ,
Target : "/foo" ,
2018-10-10 10:20:13 +00:00
BindOptions : & mounttypes . BindOptions { Propagation : test . propagation } ,
2018-01-18 21:55:27 +00:00
} ,
} ,
} ,
"mount subpath" : {
2018-10-10 10:20:13 +00:00
Mounts : [ ] mounttypes . Mount {
2018-01-18 21:55:27 +00:00
{
2018-10-10 10:20:13 +00:00
Type : mounttypes . TypeBind ,
2018-01-18 21:55:27 +00:00
Source : filepath . Join ( info . DockerRootDir , "containers" ) ,
Target : "/foo" ,
2018-10-10 10:20:13 +00:00
BindOptions : & mounttypes . BindOptions { Propagation : test . propagation } ,
2018-01-18 21:55:27 +00:00
} ,
} ,
} ,
} {
t . Run ( name , func ( t * testing . T ) {
hc := hc
t . Parallel ( )
2018-10-10 10:20:13 +00:00
c , err := client . ContainerCreate ( ctx , & containertypes . Config {
2018-01-18 21:55:27 +00:00
Image : "busybox" ,
Cmd : [ ] string { "true" } ,
2020-03-19 20:54:48 +00:00
} , hc , nil , nil , "" )
2018-01-18 21:55:27 +00:00
if err != nil {
if test . expected != "" {
t . Fatal ( err )
}
// expected an error, so this is ok and should not continue
return
}
if test . expected == "" {
t . Fatal ( "expected create to fail" )
}
defer func ( ) {
if err := client . ContainerRemove ( ctx , c . ID , types . ContainerRemoveOptions { Force : true } ) ; err != nil {
panic ( err )
}
} ( )
inspect , err := client . ContainerInspect ( ctx , c . ID )
if err != nil {
t . Fatal ( err )
}
if len ( inspect . Mounts ) != 1 {
t . Fatalf ( "unexpected number of mounts: %+v" , inspect . Mounts )
}
m := inspect . Mounts [ 0 ]
if m . Propagation != test . expected {
t . Fatalf ( "got unexpected propagation mode, expected %q, got: %v" , test . expected , m . Propagation )
}
} )
}
} )
}
}
2018-10-10 10:20:13 +00:00
func TestContainerBindMountNonRecursive ( t * testing . T ) {
2019-01-03 11:32:05 +00:00
skip . If ( t , testEnv . IsRemoteDaemon )
2018-10-10 10:20:13 +00:00
skip . If ( t , versions . LessThan ( testEnv . DaemonAPIVersion ( ) , "1.40" ) , "BindOptions.NonRecursive requires API v1.40" )
2020-02-18 09:43:56 +00:00
skip . If ( t , testEnv . IsRootless , "cannot be tested because RootlessKit executes the daemon in private mount namespace (https://github.com/rootless-containers/rootlesskit/issues/97)" )
2018-10-10 10:20:13 +00:00
defer setupTest ( t ) ( )
tmpDir1 := fs . NewDir ( t , "tmpdir1" , fs . WithMode ( 0755 ) ,
fs . WithDir ( "mnt" , fs . WithMode ( 0755 ) ) )
defer tmpDir1 . Remove ( )
tmpDir1Mnt := filepath . Join ( tmpDir1 . Path ( ) , "mnt" )
tmpDir2 := fs . NewDir ( t , "tmpdir2" , fs . WithMode ( 0755 ) ,
fs . WithFile ( "file" , "should not be visible when NonRecursive" , fs . WithMode ( 0644 ) ) )
defer tmpDir2 . Remove ( )
err := mount . Mount ( tmpDir2 . Path ( ) , tmpDir1Mnt , "none" , "bind,ro" )
if err != nil {
t . Fatal ( err )
}
defer func ( ) {
if err := mount . Unmount ( tmpDir1Mnt ) ; err != nil {
t . Fatal ( err )
}
} ( )
// implicit is recursive (NonRecursive: false)
implicit := mounttypes . Mount {
Type : "bind" ,
Source : tmpDir1 . Path ( ) ,
Target : "/foo" ,
ReadOnly : true ,
}
recursive := implicit
recursive . BindOptions = & mounttypes . BindOptions {
NonRecursive : false ,
}
recursiveVerifier := [ ] string { "test" , "-f" , "/foo/mnt/file" }
nonRecursive := implicit
nonRecursive . BindOptions = & mounttypes . BindOptions {
NonRecursive : true ,
}
nonRecursiveVerifier := [ ] string { "test" , "!" , "-f" , "/foo/mnt/file" }
ctx := context . Background ( )
2019-01-02 13:16:25 +00:00
client := testEnv . APIClient ( )
2018-10-10 10:20:13 +00:00
containers := [ ] string {
2019-06-06 11:15:31 +00:00
container . Run ( ctx , t , client , container . WithMount ( implicit ) , container . WithCmd ( recursiveVerifier ... ) ) ,
container . Run ( ctx , t , client , container . WithMount ( recursive ) , container . WithCmd ( recursiveVerifier ... ) ) ,
container . Run ( ctx , t , client , container . WithMount ( nonRecursive ) , container . WithCmd ( nonRecursiveVerifier ... ) ) ,
2018-10-10 10:20:13 +00:00
}
for _ , c := range containers {
poll . WaitOn ( t , container . IsSuccessful ( ctx , client , c ) , poll . WithDelay ( 100 * time . Millisecond ) )
}
}
2020-11-07 07:06:44 +00:00
func TestContainerVolumesMountedAsShared ( t * testing . T ) {
// Volume propagation is linux only. Also it creates directories for
// bind mounting, so needs to be same host.
skip . If ( t , testEnv . IsRemoteDaemon )
skip . If ( t , testEnv . IsUserNamespace )
skip . If ( t , testEnv . IsRootless , "cannot be tested because RootlessKit executes the daemon in private mount namespace (https://github.com/rootless-containers/rootlesskit/issues/97)" )
defer setupTest ( t ) ( )
// Prepare a source directory to bind mount
tmpDir1 := fs . NewDir ( t , "volume-source" , fs . WithMode ( 0755 ) ,
fs . WithDir ( "mnt1" , fs . WithMode ( 0755 ) ) )
defer tmpDir1 . Remove ( )
tmpDir1Mnt := filepath . Join ( tmpDir1 . Path ( ) , "mnt1" )
// Convert this directory into a shared mount point so that we do
// not rely on propagation properties of parent mount.
2020-11-10 09:00:48 +00:00
if err := mount . MakePrivate ( tmpDir1 . Path ( ) ) ; err != nil {
2020-11-07 07:06:44 +00:00
t . Fatal ( err )
}
defer func ( ) {
if err := mount . Unmount ( tmpDir1 . Path ( ) ) ; err != nil {
t . Fatal ( err )
}
} ( )
2020-11-10 09:00:48 +00:00
if err := mount . MakeShared ( tmpDir1 . Path ( ) ) ; err != nil {
2020-11-07 07:06:44 +00:00
t . Fatal ( err )
}
sharedMount := mounttypes . Mount {
Type : mounttypes . TypeBind ,
Source : tmpDir1 . Path ( ) ,
Target : "/volume-dest" ,
BindOptions : & mounttypes . BindOptions {
Propagation : mounttypes . PropagationShared ,
} ,
}
bindMountCmd := [ ] string { "mount" , "--bind" , "/volume-dest/mnt1" , "/volume-dest/mnt1" }
ctx := context . Background ( )
client := testEnv . APIClient ( )
containerID := container . Run ( ctx , t , client , container . WithPrivileged ( true ) , container . WithMount ( sharedMount ) , container . WithCmd ( bindMountCmd ... ) )
poll . WaitOn ( t , container . IsSuccessful ( ctx , client , containerID ) , poll . WithDelay ( 100 * time . Millisecond ) )
// Make sure a bind mount under a shared volume propagated to host.
if mounted , _ := mountinfo . Mounted ( tmpDir1Mnt ) ; ! mounted {
t . Fatalf ( "Bind mount under shared volume did not propagate to host" )
}
mount . Unmount ( tmpDir1Mnt )
}
func TestContainerVolumesMountedAsSlave ( t * testing . T ) {
// Volume propagation is linux only. Also it creates directories for
// bind mounting, so needs to be same host.
skip . If ( t , testEnv . IsRemoteDaemon )
skip . If ( t , testEnv . IsUserNamespace )
skip . If ( t , testEnv . IsRootless , "cannot be tested because RootlessKit executes the daemon in private mount namespace (https://github.com/rootless-containers/rootlesskit/issues/97)" )
// Prepare a source directory to bind mount
tmpDir1 := fs . NewDir ( t , "volume-source" , fs . WithMode ( 0755 ) ,
fs . WithDir ( "mnt1" , fs . WithMode ( 0755 ) ) )
defer tmpDir1 . Remove ( )
tmpDir1Mnt := filepath . Join ( tmpDir1 . Path ( ) , "mnt1" )
// Prepare a source directory with file in it. We will bind mount this
// directory and see if file shows up.
tmpDir2 := fs . NewDir ( t , "volume-source2" , fs . WithMode ( 0755 ) ,
fs . WithFile ( "slave-testfile" , "Test" , fs . WithMode ( 0644 ) ) )
defer tmpDir2 . Remove ( )
// Convert this directory into a shared mount point so that we do
// not rely on propagation properties of parent mount.
2020-11-10 09:00:48 +00:00
if err := mount . MakePrivate ( tmpDir1 . Path ( ) ) ; err != nil {
2020-11-07 07:06:44 +00:00
t . Fatal ( err )
}
defer func ( ) {
if err := mount . Unmount ( tmpDir1 . Path ( ) ) ; err != nil {
t . Fatal ( err )
}
} ( )
2020-11-10 09:00:48 +00:00
if err := mount . MakeShared ( tmpDir1 . Path ( ) ) ; err != nil {
2020-11-07 07:06:44 +00:00
t . Fatal ( err )
}
slaveMount := mounttypes . Mount {
Type : mounttypes . TypeBind ,
Source : tmpDir1 . Path ( ) ,
Target : "/volume-dest" ,
BindOptions : & mounttypes . BindOptions {
Propagation : mounttypes . PropagationSlave ,
} ,
}
topCmd := [ ] string { "top" }
ctx := context . Background ( )
client := testEnv . APIClient ( )
containerID := container . Run ( ctx , t , client , container . WithTty ( true ) , container . WithMount ( slaveMount ) , container . WithCmd ( topCmd ... ) )
// Bind mount tmpDir2/ onto tmpDir1/mnt1. If mount propagates inside
// container then contents of tmpDir2/slave-testfile should become
// visible at "/volume-dest/mnt1/slave-testfile"
if err := mount . Mount ( tmpDir2 . Path ( ) , tmpDir1Mnt , "none" , "bind" ) ; err != nil {
t . Fatal ( err )
}
defer func ( ) {
if err := mount . Unmount ( tmpDir1Mnt ) ; err != nil {
t . Fatal ( err )
}
} ( )
mountCmd := [ ] string { "cat" , "/volume-dest/mnt1/slave-testfile" }
if result , err := container . Exec ( ctx , client , containerID , mountCmd ) ; err == nil {
if result . Stdout ( ) != "Test" {
t . Fatalf ( "Bind mount under slave volume did not propagate to container" )
}
} else {
t . Fatal ( err )
}
}
2022-09-21 23:03:04 +00:00
// Regression test for #38995 and #43390.
func TestContainerCopyLeaksMounts ( t * testing . T ) {
defer setupTest ( t ) ( )
bindMount := mounttypes . Mount {
Type : mounttypes . TypeBind ,
Source : "/var" ,
Target : "/hostvar" ,
BindOptions : & mounttypes . BindOptions {
Propagation : mounttypes . PropagationRSlave ,
} ,
}
ctx := context . Background ( )
client := testEnv . APIClient ( )
cid := container . Run ( ctx , t , client , container . WithMount ( bindMount ) , container . WithCmd ( "sleep" , "120s" ) )
getMounts := func ( ) string {
t . Helper ( )
res , err := container . Exec ( ctx , client , cid , [ ] string { "cat" , "/proc/self/mountinfo" } )
assert . NilError ( t , err )
assert . Equal ( t , res . ExitCode , 0 )
return res . Stdout ( )
}
mountsBefore := getMounts ( )
_ , _ , err := client . CopyFromContainer ( ctx , cid , "/etc/passwd" )
assert . NilError ( t , err )
mountsAfter := getMounts ( )
assert . Equal ( t , mountsBefore , mountsAfter )
}
2023-04-05 11:32:03 +00:00
func TestContainerBindMountRecursivelyReadOnly ( t * testing . T ) {
skip . If ( t , testEnv . IsRemoteDaemon )
skip . If ( t , versions . LessThan ( testEnv . DaemonAPIVersion ( ) , "1.44" ) , "requires API v1.44" )
defer setupTest ( t ) ( )
// 0o777 for allowing rootless containers to write to this directory
tmpDir1 := fs . NewDir ( t , "tmpdir1" , fs . WithMode ( 0 o777 ) ,
fs . WithDir ( "mnt" , fs . WithMode ( 0 o777 ) ) )
defer tmpDir1 . Remove ( )
tmpDir1Mnt := filepath . Join ( tmpDir1 . Path ( ) , "mnt" )
tmpDir2 := fs . NewDir ( t , "tmpdir2" , fs . WithMode ( 0 o777 ) ,
fs . WithFile ( "file" , "should not be writable when recursively read only" , fs . WithMode ( 0 o666 ) ) )
defer tmpDir2 . Remove ( )
if err := mount . Mount ( tmpDir2 . Path ( ) , tmpDir1Mnt , "none" , "bind" ) ; err != nil {
t . Fatal ( err )
}
defer func ( ) {
if err := mount . Unmount ( tmpDir1Mnt ) ; err != nil {
t . Fatal ( err )
}
} ( )
rroSupported := kernel . CheckKernelVersion ( 5 , 12 , 0 )
nonRecursiveVerifier := [ ] string { ` /bin/sh ` , ` -xc ` , ` touch /foo/mnt/file; [ $? = 0 ] ` }
forceRecursiveVerifier := [ ] string { ` /bin/sh ` , ` -xc ` , ` touch /foo/mnt/file; [ $? != 0 ] ` }
// ro (recursive if kernel >= 5.12)
ro := mounttypes . Mount {
Type : mounttypes . TypeBind ,
Source : tmpDir1 . Path ( ) ,
Target : "/foo" ,
ReadOnly : true ,
BindOptions : & mounttypes . BindOptions {
Propagation : mounttypes . PropagationRPrivate ,
} ,
}
roAsStr := ro . Source + ":" + ro . Target + ":ro,rprivate"
roVerifier := nonRecursiveVerifier
if rroSupported {
roVerifier = forceRecursiveVerifier
}
// Non-recursive
nonRecursive := ro
nonRecursive . BindOptions = & mounttypes . BindOptions {
ReadOnlyNonRecursive : true ,
Propagation : mounttypes . PropagationRPrivate ,
}
nonRecursiveAsStr := nonRecursive . Source + ":" + nonRecursive . Target + ":ro-non-recursive,rprivate"
// Force recursive
forceRecursive := ro
forceRecursive . BindOptions = & mounttypes . BindOptions {
ReadOnlyForceRecursive : true ,
Propagation : mounttypes . PropagationRPrivate ,
}
forceRecursiveAsStr := forceRecursive . Source + ":" + forceRecursive . Target + ":ro-force-recursive,rprivate"
ctx := context . Background ( )
client := testEnv . APIClient ( )
containers := [ ] string {
container . Run ( ctx , t , client , container . WithMount ( ro ) , container . WithCmd ( roVerifier ... ) ) ,
container . Run ( ctx , t , client , container . WithBindRaw ( roAsStr ) , container . WithCmd ( roVerifier ... ) ) ,
container . Run ( ctx , t , client , container . WithMount ( nonRecursive ) , container . WithCmd ( nonRecursiveVerifier ... ) ) ,
container . Run ( ctx , t , client , container . WithBindRaw ( nonRecursiveAsStr ) , container . WithCmd ( nonRecursiveVerifier ... ) ) ,
}
if rroSupported {
containers = append ( containers ,
container . Run ( ctx , t , client , container . WithMount ( forceRecursive ) , container . WithCmd ( forceRecursiveVerifier ... ) ) ,
container . Run ( ctx , t , client , container . WithBindRaw ( forceRecursiveAsStr ) , container . WithCmd ( forceRecursiveVerifier ... ) ) ,
)
}
for _ , c := range containers {
poll . WaitOn ( t , container . IsSuccessful ( ctx , client , c ) , poll . WithDelay ( 100 * time . Millisecond ) )
}
}