diff --git a/native/fs.h b/native/fs.h index bbc3cd7..c42be7d 100644 --- a/native/fs.h +++ b/native/fs.h @@ -129,6 +129,12 @@ static _Thread_local ProcShared *proc_now=&proc_first; #define prctl_pdeathsig (proc_now->priv.pdeathsig) #define prctl_name (proc_now->priv.name) #define cap_held (proc_now->priv.caps) +/* Root's CAP_DAC_OVERRIDE (fs_dac_begin). denied: the host refused a call for a file's + or directory's mode (fs_error); on: the call is being done again for root; ro: its + walk is in a read-only bind mount, where no mode changes; held: what that pass + changed, to put back (fs_dac_end). */ +#define FS_DAC_HELD 64 +static _Thread_local struct { int on,denied,ro,count;struct { int fd;mode_t mode,set; } held[FS_DAC_HELD]; } fs_dac; /* merged: the listing of a directory both image layers hold (overlay.h). */ struct OvList;static void ov_list_free(struct OvList *list); typedef struct GuestDirectory { DIR *stream;unsigned refs,count;char path[1025];struct OvList *merged;int lower; } GuestDirectory; /* lower: opened there only */ @@ -221,6 +227,7 @@ static uint64_t fs_error(void) { case ENOTEMPTY:return linux_error(39);case EROFS:return linux_error(30); case EFBIG:return linux_error(27);case ENOSPC:return linux_error(28); case EXDEV:return linux_error(18); /* openat2 RESOLVE_BENEATH, rename across mounts */ + case EACCES:fs_dac.denied=1;return linux_error(13); /* root does the call again (FS_DAC_REDO) */ default:return net_error(); } } @@ -504,6 +511,90 @@ static int fs_walk_fast(int base,const char *cur_start,const char *rest,int foll #define FS_ANY_AT 0 #define fs_renameat renameat #endif +/* Root's CAP_DAC_OVERRIDE. The container's files are the Mac user's, so macOS refuses + a directory or file whose mode lacks the owner's bits, to the guest's root as to + anyone, where Linux lets root through: tar as root makes a directory with its final + mode (0555 in Go's module cache) and then its contents, rm -rf removes them, and a + root shell lists a 0700-less directory. A file call the host refused with EACCES is + done once more for root holding the capability (FS_DAC_REDO), with fs_dac.on: that + pass takes no fast path, so the walk opens every directory on the way itself, and + each one of the container's (upper layer, writable bind mounts and volumes; never + the image's, which no container changes, nor a read-only mount's: fs_dac.ro) + lacking owner rwx is given them for the rest of + the call - by descriptor, or, where the mode refuses the open itself, by its name in + its already open parent with no symlink followed, so nothing outside the root is + changed. Callers that open the leaf do the same for it (fs_dac_leaf). fs_dac_end puts + each mode back by descriptor (the entry may be renamed or removed by then) unless it + changed meanwhile (a chmod by another thread; the call's own: fs_dac_chmod). A mode + that cannot be put back is said on stderr, the container's log (fs_dac_stuck). + ponytail: for those microseconds the container's other processes see the wider + mode, and an engine killed then leaves it; CAP_DAC_READ_SEARCH alone is not honoured; + exec, chown, inotify and unix-socket paths are not redone (a root user rarely meets + an unsearchable directory there). */ +static int fs_dac_can(void) { return !guest_uid && (cap_held[0]&2u); } /* capability 1 */ +static int fs_dac_begin(void) { + int denied=fs_dac.denied;fs_dac.denied=0; + if(!denied || fs_dac.on || !fs_dac_can())return 0; + fs_dac.on=1;return 1; +} +/* A mode the pass widened stays so (errno says why): the call itself is done or + refused already, so the container's log is where it can be told. */ +static void fs_dac_stuck(mode_t mode,mode_t set) { + fprintf(stderr,"dockiva: a file or directory root's call opened up keeps mode %04o, not its own %04o: %s\n",(unsigned)set,(unsigned)mode,strerror(errno)); +} +static void fs_dac_end(void) { + int saved=errno;struct stat st; + while(fs_dac.count) { + int i=--fs_dac.count,fd=fs_dac.held[i].fd; + if(fstat(fd,&st) || ((st.st_mode&07777)==fs_dac.held[i].set && fchmod(fd,fs_dac.held[i].mode)))fs_dac_stuck(fs_dac.held[i].mode,fs_dac.held[i].set); + close(fd); + } + fs_dac.on=0;errno=saved; +} +#define FS_DAC_REDO(result,call) do { if((result)==linux_error(13) && fs_dac_begin()) { (result)=(call);fs_dac_end(); } } while(0) +static mode_t fs_dac_need(const struct stat *st) { + mode_t need=S_ISDIR(st->st_mode)?0700:S_ISREG(st->st_mode)?0600:0; + return (st->st_mode&need)==need?0:need; +} +static void fs_dac_hold(int fd,mode_t mode,mode_t need) { + fs_dac.held[fs_dac.count].fd=fd;fs_dac.held[fs_dac.count].mode=mode;fs_dac.held[fs_dac.count++].set=mode|need; +} +/* A directory of the container's the pass opened (fd), or will change entries in. */ +static void fs_dac_fd(int fd) { + struct stat st;mode_t need=0; + if(!fs_dac.on || fs_dac.ro || fd<0 || fs_dac.count==FS_DAC_HELD || fstat(fd,&st) || !(need=fs_dac_need(&st)))return; + int keep=fcntl(fd,F_DUPFD_CLOEXEC,0); + if(keep<0 || fchmod(keep,(st.st_mode&07777)|need)) { if(keep>=0)close(keep);return; } + fs_dac_hold(keep,st.st_mode&07777,need); +} +/* The container's leaf in parent, before the pass opens it or changes its entries. */ +static void fs_dac_leaf(int parent,const char *leaf) { + struct stat st;mode_t need=0; + if(!fs_dac.on || fs_dac.ro || fs_dac.count==FS_DAC_HELD || fstatat(parent,leaf,&st,FS_NOFOLLOW_AT) || !(need=fs_dac_need(&st)))return; + mode_t mode=st.st_mode&07777; + if(fchmodat(parent,leaf,mode|need,FS_NOFOLLOW_AT))return; + int fd=openat(parent,leaf,O_RDONLY|FS_NOFOLLOW_OPEN|O_CLOEXEC|O_NONBLOCK);struct stat now; + if(fd>=0 && !fstat(fd,&now) && now.st_ino==st.st_ino && now.st_dev==st.st_dev) { fs_dac_hold(fd,mode,need);return; } + /* Not opened (no descriptor to spare), or the name is another file's by now: put + back by name, where it still names the file widened and nothing changed its mode + since. Renamed or removed meanwhile, that file is out of reach. */ + int saved=errno; + if(fd>=0)close(fd); + if(fstatat(parent,leaf,&now,FS_NOFOLLOW_AT))fs_dac_stuck(mode,mode|need); + else if(now.st_ino!=st.st_ino || now.st_dev!=st.st_dev) { errno=ESTALE;fs_dac_stuck(mode,mode|need); } + else if((now.st_mode&07777)==(mode|need) && fchmodat(parent,leaf,mode,FS_NOFOLLOW_AT))fs_dac_stuck(mode,mode|need); + errno=saved; +} +/* The pass's own chmod of st's file to mode: that is what stays. */ +static void fs_dac_chmod(const struct stat *st,mode_t mode) { + struct stat held; + for(int i=0;ist_ino && held.st_dev==st->st_dev)fs_dac.held[i].mode=fs_dac.held[i].set=mode; +} +static int fs_subdir(int dir,const char *name) { + if(fs_dac.on)fs_dac_leaf(dir,name); + return openat(dir,name,O_RDONLY|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC); +} static int fs_walk(int base,const char *base_path,const char *input,int follow,char leaf[1025],char canon[1025]); static int ov_walk(int base,const char *base_path,const char *input,int follow,char leaf[1025],char canon[1025]); /* overlay.h */ /* A guest directory by canonical path, as a descriptor (a mount's own, if it is one). */ @@ -539,7 +630,7 @@ static int fs_walk(int base,const char *base_path,const char *input,int follow,c strcpy(cur,rest[0]=='/'?(fs_scope.root>=0?fs_scope.path:"/"):base_path); if(mem_mount_count && *cur && mem_mount_inside(cur))return mem_redirect_set(cur,rest); #ifdef O_NOFOLLOW_ANY - { /* The fast path stays in base's host directory tree: not across a mount. */ + if(!fs_dac.on) { /* The fast path stays in base's host directory tree: not across a mount. */ char full[1025];int fast=fs_walk_fast(base,cur,rest,follow,direct,leaf,full); if(fast!=-2) { if(fast>=0) { fs_where=fs_mount_of(full);if(canon)strcpy(canon,full); } @@ -548,6 +639,7 @@ static int fs_walk(int base,const char *base_path,const char *input,int follow,c } #endif int fd=dup(base);if(fd<0)return -1; + if(fs_dac.on) { int m=fs_mount_count?fs_mount_of(cur):0;fs_dac.ro=m>0 && fs_mounts[m-1].readonly;fs_dac_fd(fd); } for(;;) { while(rest[pos]=='/')pos++; if(!rest[pos]) { strcpy(leaf,".");if(canon)strcpy(canon,cur);fs_where=fs_mount_of(cur);return fd; } @@ -563,13 +655,14 @@ static int fs_walk(int base,const char *base_path,const char *input,int follow,c if(!*cur)continue; /* position unknown: stay, as at "/" */ fs_canon_pop(cur); int parent=fs_open_dir(cur);int saved=errno;close(fd);fd=parent;errno=saved;if(fd<0)return -1; + fs_dac_fd(fd); continue; } if(fs_scope.resolve&8 && fs_is_root(fd)) { close(fd);errno=EXDEV;return -1; } if(!fs_is_root(fd)) { int next=openat(fd,"..",O_RDONLY|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC); int saved=errno;close(fd);fd=next;errno=saved;if(fd<0)return -1; - fs_canon_pop(cur); + fs_dac_fd(fd);fs_canon_pop(cur); } continue; } @@ -584,7 +677,7 @@ static int fs_walk(int base,const char *base_path,const char *input,int follow,c if(m>=0) { if(!fs_mounts[m].directory && !last) { close(fd);errno=ENOTDIR;return -1; } close(fd);fd=dup(fs_mounts[m].fd);if(fd<0)return -1; - strcpy(cur,at); + fs_dac.ro=fs_mounts[m].readonly;fs_dac_fd(fd);strcpy(cur,at); if(last) { strcpy(leaf,fs_mounts[m].directory?".":fs_mounts[m].name); if(canon)strcpy(canon,at); @@ -607,6 +700,7 @@ static int fs_walk(int base,const char *base_path,const char *input,int follow,c if(fs_scope.resolve&8) { close(fd);errno=EXDEV;return -1; } close(fd);fd=dup(FS_WALK_ROOT);if(fd<0)return -1; strcpy(cur,fs_scope.root>=0?fs_scope.path:"/"); + if(fs_dac.on) { int m=fs_mount_count?fs_mount_of(cur):0;fs_dac.ro=m>0 && fs_mounts[m-1].readonly;fs_dac_fd(fd); } } continue; } @@ -615,12 +709,14 @@ static int fs_walk(int base,const char *base_path,const char *input,int follow,c if(canon) { strcpy(canon,cur);if(fs_canon_push(canon,part)<0) { close(fd);return -1; } } fs_where=fs_mount_of(cur);return fd; } - int next=openat(fd,part,O_RDONLY|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC); + int next=fs_subdir(fd,part); int saved=errno;close(fd);fd=next;errno=saved;if(fd<0)return -1; if(fs_canon_push(cur,part)<0) { close(fd);return -1; } } } #include "overlay.h" +/* Whether the last walk's leaf is the container's own (fs_dac): not an image file. */ +static int ov_leaf_upper(void) { return !(fs_lower>=0 && ov_leaf.canon[0] && ov_leaf.layer==1); } /* Minimal /proc: /proc/self/exe and /proc//exe name each process's executable (canonical guest paths, indexed by pid; PROCESS_LIMIT+1 processes). Path lookups resolve them to that file; readlink returns the path. */ @@ -700,15 +796,17 @@ static uint64_t fs_fchdir(void) { /* Working directories remain descriptor anchored and record the canonical (link-free) path. A renamed cwd is usable for relative I/O, but getcwd fails instead of returning an unverified stale pathname. */ -static uint64_t fs_chdir(void) { +static uint64_t fs_chdir_once(void) { char leaf[1025],canon[1025]; int parent=fs_lookup((uint64_t)-100,cpu->gpr[0],leaf,1,canon);if(parent<0)return fs_error(); + if(fs_dac.on && ov_leaf_upper())fs_dac_leaf(parent,leaf); int fd=openat(parent,leaf,O_RDONLY|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC); int saved=errno;close(parent);errno=saved;if(fd<0)return fs_error(); GuestCwd *next=calloc(1,sizeof(*next));if(!next) { close(fd);return linux_error(12); } next->fd=fd;next->refs=1;strcpy(next->path,canon); fs_cwd_release();fs_cwd=next;return 0; } +static uint64_t fs_chdir(void) { uint64_t result=fs_chdir_once();FS_DAC_REDO(result,fs_chdir_once());return result; } /* Initial working directory from --cwd (an image WorkingDir), resolved inside the root. */ static int mem_start_cwd(const char *path); static int fs_start_cwd(const char *path) { @@ -1547,6 +1645,7 @@ static uint64_t fs_open(void) { refuses one before the leaf). */ int link=opath && exists && S_ISLNK(before.st_mode); if(link)options=O_RDONLY|O_SYMLINK|FS_NOFOLLOW_OPEN|O_CLOEXEC|O_NONBLOCK; + if(fs_dac.on && exists && !(fs_lower>=0 && layers.canon[0] && layers.layer==1))fs_dac_leaf(parent,leaf); /* root opens whatever its mode */ int fd=openat(parent,leaf,options|(fresh?O_EXCL:0),mode); if(fd<0 && fresh && errno==EEXIST && !(flags&128)) { fresh=0;fd=openat(parent,leaf,options,mode); } if(fd<0) { uint64_t e=fs_error();close(parent);return e; } @@ -1756,7 +1855,9 @@ static uint64_t fs_flock(int fd) { return (operation&4) || !schedule_active?linux_error(11):IO_BLOCK; return fs_lock_set(st.st_dev,st.st_ino,owner,0,INT64_MAX,type); } -static uint64_t fs_call(uint64_t call) { +static uint64_t fs_call_once(uint64_t call); +static uint64_t fs_call(uint64_t call) { uint64_t result=fs_call_once(call);FS_DAC_REDO(result,fs_call_once(call));return result; } +static uint64_t fs_call_once(uint64_t call) { if(call==166) { unsigned previous=fs_umask;fs_umask=(unsigned)cpu->gpr[0]&0777;return previous; } if(call==52 || call==53 || call==452) { uint64_t mode=cpu->gpr[call==52?1:2]&UINT64_C(07777); /* Linux ignores other bits (libuv passes st_mode); setuid/setgid/sticky allowed */ @@ -1778,6 +1879,7 @@ static uint64_t fs_call(uint64_t call) { struct stat st;int result=fs_leaf_stat(parent,leaf,&st); if(!result && !S_ISREG(st.st_mode) && !S_ISDIR(st.st_mode) && !S_ISSOCK(st.st_mode)) { errno=EACCES;result=-1; } if(!result)result=fchmodat(parent,leaf,(mode_t)mode,FS_NOFOLLOW_AT); + if(!result && fs_dac.on)fs_dac_chmod(&st,(mode_t)mode); int saved=errno;close(parent);errno=saved;return result<0?fs_error():0; } else if(fd<0)return linux_error(9); struct stat st;int result=fstat(fd,&st); @@ -1873,6 +1975,7 @@ static uint64_t fs_call(uint64_t call) { char leaf[1025];int parent=fs_lookup((uint64_t)-100,cpu->gpr[0],leaf,1|FS_DIRECT,NULL);if(parent<0)return fs_error(); if(!fs_path_writable()) { close(parent);return linux_error(30); } { OvLeaf w=ov_leaf;if(ov_prepare(&w,&parent,leaf,OV_MODIFY)) { uint64_t e=fs_error();close(parent);return e; } } + fs_dac_leaf(parent,leaf); int fd=openat(parent,leaf,O_WRONLY|FS_NOFOLLOW_OPEN|O_NONBLOCK|O_CLOEXEC);int saved=errno;close(parent);errno=saved; if(fd<0)return fs_error(); struct stat st;if(fstat(fd,&st)<0 || !S_ISREG(st.st_mode)) { saved=S_ISDIR(st.st_mode)?EISDIR:EINVAL;close(fd);errno=saved;return fs_error(); } @@ -2000,6 +2103,7 @@ static uint64_t fs_call(uint64_t call) { if(!fs_path_writable()) { close(other);close(parent);return linux_error(30); } if(!strcmp(target,".")) { close(other);close(parent);return linux_error(fs_where>0?16:13); } OvLeaf to=ov_leaf;struct stat moving; + if(fs_dac.on && !(layers && w.layer==1))fs_dac_leaf(parent,leaf); /* a directory moving to another one is written (its "..") */ int directory=upm_fs && layers && to.canon[0] && !fstatat(parent,leaf,&moving,FS_NOFOLLOW_AT) && S_ISDIR(moving.st_mode); if(directory && upm_has_files(to.canon)) { result=-1;errno=ENOTEMPTY; } /* its pending files (upm.h) are entries */ else if(layers || (fs_lower>=0 && to.canon[0]))result=layers && to.canon[0]?ov_rename(&w,parent,leaf,&to,other,target):(errno=EXDEV,-1); @@ -2042,7 +2146,10 @@ static uint64_t fs_call(uint64_t call) { int layers=fs_lower>=0 && dir->path[0] && !(fs_mount_count && fs_mount_of(dir->path)); if(!dir->stream && !dir->merged) { struct stat below; /* only a directory both layers hold merges: a lower-only one (most of an image's) streams as it is */ - if(layers && !dir->lower && ((!fstatat(fs_lower,dir->path[1]?dir->path+1:".",&below,FS_NOFOLLOW_AT) && S_ISDIR(below.st_mode)) || upm_has_files(dir->path)))dir->merged=ov_list(dir->path); + if(layers && !dir->lower && ((!fstatat(fs_lower,dir->path[1]?dir->path+1:".",&below,FS_NOFOLLOW_AT) && S_ISDIR(below.st_mode)) || upm_has_files(dir->path))) { + errno=0;dir->merged=ov_list(dir->path); + if(!dir->merged && errno==EACCES && fs_dac_can())return fs_error(); /* its mode refuses the walk there: root's redo lists it whole */ + } if(!dir->merged) { int copy=dup(fd);if(copy<0)return fs_error(); dir->stream=fdopendir(copy);if(!dir->stream) { int saved=errno;close(copy);errno=saved;return fs_error(); } diff --git a/native/memfs.h b/native/memfs.h index 24818f1..ec823a1 100644 --- a/native/memfs.h +++ b/native/memfs.h @@ -64,10 +64,19 @@ static int mem_is(const MemNode *n,uint32_t type) { return (n->mode&0170000)==ty #define MEM_DIR 0040000u #define MEM_REG 0100000u #define MEM_LNK 0120000u -/* rwx (4, 2, 1) for the caller: root passes, except execute on a file with no x bit. */ +/* rwx (4, 2, 1) for the caller: root passes, except execute on a file with no x bit. + An upper directory's mirror (upm.h) is 0777 whatever the directory's mode: changing + its entries is the host directory's to allow, as for the files on disk beside them + (a user made files in a 0555 directory, and renamed ones not yet written back). */ +static void mem_dir_path(const MemNode *n,char out[1025]); static int mem_may(const MemNode *n,unsigned want) { - if(!guest_uid)return !(want&1) || mem_is(n,MEM_DIR) || (n->mode&0111); - unsigned bits=guest_uid==n->uid || n->fs==upm_fs?(n->mode>>6)&7:guest_gid==n->gid?(n->mode>>3)&7:n->mode&7; /* upm.h: owned as host files are */ + int layer=n->fs==upm_fs; + if(!guest_uid && !(layer && !fs_dac_can()))return !(want&1) || mem_is(n,MEM_DIR) || (n->mode&0111); /* (the layer's files: root holding CAP_DAC_OVERRIDE, fs.h) */ + if(layer && (want&2) && mem_is(n,MEM_DIR)) { + char path[1025];struct stat st;mem_dir_path(n,path); + if(path[0] && !fstatat(fs_root,path[1]?path+1:".",&st,FS_NOFOLLOW_AT) && (st.st_mode&0300)!=0300)return 0; + } + unsigned bits=guest_uid==n->uid || layer?(n->mode>>6)&7:guest_gid==n->gid?(n->mode>>3)&7:n->mode&7; /* upm.h: owned as host files are */ return (bits&want)==want; } /* Removing or renaming node out of dir: the sticky bit keeps others' files. */ diff --git a/native/overlay.h b/native/overlay.h index 94256ac..71ad5aa 100644 --- a/native/overlay.h +++ b/native/overlay.h @@ -169,6 +169,7 @@ static int ov_mark(int dir,const char *name) { /* make ".wh.N static void ov_unmark(int dir,const char *name) { char wh[300];if(snprintf(wh,sizeof wh,OV_WH "%s",name)<=255)unlinkat(dir,wh,0); } static void ov_close2(int a,int b) { int saved=errno;if(a>=0)close(a);if(b>=0)close(b);errno=saved; } static int ov_subdir(int dir,const char *name) { return openat(dir,name,O_RDONLY|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC); } +#define ov_updir fs_subdir /* an upper or mounted directory: root's redo opens it whatever its mode (fs_dac, fs.h) */ /* A directory stream from its start (a dup shares the descriptor's position). */ static DIR *ov_opendir(int dir) { int copy=dup(dir);DIR *s=copy>=0 && lseek(copy,0,SEEK_SET)==0?fdopendir(copy):NULL; @@ -190,19 +191,22 @@ static int ov_pair(const char *canon,int *upper,int *lower,int *mount) { if(m>0) { if(!fs_mounts[m-1].directory) { errno=ENOTDIR;return -1; } *mount=m-1;p+=strlen(fs_mounts[m-1].path);*upper=dup(fs_mounts[m-1].fd);if(*upper<0)return -1; + fs_dac.ro=fs_mounts[m-1].readonly;fs_dac_fd(*upper); } else { + fs_dac.ro=0; /* Most directories are in one layer only: a path the lower layer lacks is the upper one's, found in one call (and the reverse needs the walk). */ struct stat st; - if(canon[1] && fstatat(fs_lower,canon+1,&st,FS_NOFOLLOW_AT) && (errno==ENOENT || errno==ENOTDIR)) { + if(!fs_dac.on && canon[1] && fstatat(fs_lower,canon+1,&st,FS_NOFOLLOW_AT) && (errno==ENOENT || errno==ENOTDIR)) { *upper=openat(fs_root,canon+1,O_RDONLY|O_DIRECTORY|FS_NOFOLLOW_OPEN|O_CLOEXEC); if(*upper>=0 || errno!=ELOOP)return *upper<0?-1:0; } *upper=dup(fs_root);*lower=dup(fs_lower); if(*upper<0 || *lower<0) { ov_close2(*upper,*lower);return -1; } + fs_dac_fd(*upper); } while(ov_part(&p,part)) { - int u=*upper>=0?ov_subdir(*upper,part):-1; + int u=*upper>=0?ov_updir(*upper,part):-1; if(u<0 && *upper>=0 && errno!=ENOENT) { ov_close2(*upper,*lower);return -1; } /* a file or link hides what is below */ int l=-1; if(*lower>=0 && !(u>=0?ov_marker(u,NULL):*upper>=0 && ov_marker(*upper,part)))l=ov_subdir(*lower,part); @@ -233,11 +237,13 @@ static int ov_chmod(int dir,const char *name,mode_t mode) { /* A canonical guest directory of the root's layers in the upper layer: it and those above it are made from the lower ones where missing. Returns its descriptor. */ static int ov_upper(const char *canon) { - if(canon[1]) { int d=openat(fs_root,canon+1,O_RDONLY|O_DIRECTORY|FS_NOFOLLOW_OPEN|O_CLOEXEC);if(d>=0)return d; } /* there already */ + fs_dac.ro=0; + if(canon[1]) { int d=openat(fs_root,canon+1,O_RDONLY|O_DIRECTORY|FS_NOFOLLOW_OPEN|O_CLOEXEC);if(d>=0) { fs_dac_fd(d);return d; } } /* there already */ int u=dup(fs_root),l=dup(fs_lower);const char *p=canon;char part[256]; if(u<0 || l<0) { ov_close2(u,l);return -1; } + fs_dac_fd(u); while(ov_part(&p,part)) { - int nu=ov_subdir(u,part),nl=l>=0?ov_subdir(l,part):-1; + int nu=ov_updir(u,part),nl=l>=0?ov_subdir(l,part):-1; if(nu<0 && errno==ENOENT) { struct stat st; if(nl<0 || fstat(nl,&st) || ov_marker(u,part)) { ov_close2(u,l);ov_close2(nl,-1);errno=ENOENT;return -1; } @@ -248,7 +254,7 @@ static int ov_upper(const char *canon) { (void)ov_chmod(u,part,st.st_mode&07777);(void)utimensat(u,part,times,AT_SYMLINK_NOFOLLOW); ov_dirty=1; /* a lower directory now shows from above (fchmod of its descriptor): ov_changed */ } - nu=ov_subdir(u,part); + nu=ov_updir(u,part); } if(nu<0) { ov_close2(u,l);ov_close2(nl,-1);return -1; } if(nl>=0 && ov_marker(nu,NULL)) { close(nl);nl=-1; } /* an opaque directory: nothing lower is seen */ @@ -615,7 +621,7 @@ static int ov_walk(int base,const char *base_path,const char *input,int follow,c memcpy(rest,input,length+1); if(mem_mount_count && mem_mount_inside(cur))return mem_redirect_set(cur,rest); OvLinkWalk passed={0};char from[1025]; - if(fs_scope.root<0) { + if(fs_scope.root<0 && !fs_dac.on) { char to[1025];int fast; if(ov_link_get(cur,rest,follow,to) && (fast=ov_walk_fast("/",to,follow,direct,leaf,canon))!=-2)return fast; if((fast=ov_walk_fast(cur,rest,follow,direct,leaf,canon))!=-2)return fast; @@ -635,6 +641,7 @@ static int ov_walk(int base,const char *base_path,const char *input,int follow,c if(fd>=0) { strcpy(leaf,".");if(canon)strcpy(canon,cur); fs_where=0;ov_leaf.layer=layer;strcpy(ov_leaf.canon,cur); + if(!layer) { fs_dac.ro=0;fs_dac_fd(fd); } return fd; } } @@ -669,7 +676,7 @@ static int ov_walk(int base,const char *base_path,const char *input,int follow,c if(i>=0) { if(!fs_mounts[i].directory && !last) { ov_close2(u,l);errno=ENOTDIR;return -1; } ov_close2(u,l);u=dup(fs_mounts[i].fd);l=-1;mount=i;if(u<0)return -1; - strcpy(cur,at); + fs_dac.ro=fs_mounts[i].readonly;fs_dac_fd(u);strcpy(cur,at); if(last) { strcpy(leaf,fs_mounts[i].directory?".":fs_mounts[i].name);if(canon)strcpy(canon,at);fs_where=i+1;return u; } continue; } @@ -714,7 +721,7 @@ static int ov_walk(int base,const char *base_path,const char *input,int follow,c if(!S_ISDIR(st.st_mode)) { ov_close2(u,l);errno=ENOTDIR;return -1; } int nu=-1,nl=-1; if(layer==0) { - nu=ov_subdir(u,part); + nu=ov_updir(u,part); if(nu<0) { ov_close2(u,l);return -1; } if(mount<0 && l>=0 && !ov_marker(nu,NULL))nl=ov_subdir(l,part); } else if((nl=ov_subdir(l,part))<0) { ov_close2(u,l);return -1; } diff --git a/native/upm.h b/native/upm.h index 11c0af0..9105bb3 100644 --- a/native/upm.h +++ b/native/upm.h @@ -209,7 +209,18 @@ static int upm_write_back(MemNode *n,const char *canon,int force) { /* All from the directory, opened refusing a symlink anywhere on the way (another run, an exec, may have swapped one in): nothing lands outside the root. */ int parent=dir[1]?openat(fs_root,dir+1,O_RDONLY|O_DIRECTORY|FS_NOFOLLOW_OPEN|O_CLOEXEC):dup(fs_root); - int fd=parent>=0?openat(parent,temp,O_WRONLY|O_CREAT|O_EXCL|O_CLOEXEC|O_NOFOLLOW,0600):-1,ok=fd>=0; + int fd=parent>=0?openat(parent,temp,O_WRONLY|O_CREAT|O_EXCL|O_CLOEXEC|O_NOFOLLOW,0600):-1,forced=0; + if(fd<0 && errno==EACCES) { + /* The directory, or one above it, lost the owner's bits after the file was made + (Go fills a module's directory, then makes it 0555): the file is the guest's + already, so it is written whoever made it, as root's calls are (fs_dac, fs.h). + It stayed in memory and went with the container's end. */ + forced=!fs_dac.on;fs_dac.on=1; + if(parent>=0)close(parent); + parent=ov_upper(dir); + fd=parent>=0?openat(parent,temp,O_WRONLY|O_CREAT|O_EXCL|O_CLOEXEC|O_NOFOLLOW,0600):-1; + } + int ok=fd>=0; for(uint64_t at=0;ok && at=0)(void)unlinkat(parent,temp,0); if(parent>=0)close(parent); + if(forced)fs_dac_end(); mem_node_put(n); /* the pin (frees it once forgotten) */ return done; } @@ -262,6 +274,7 @@ static int64_t upm_move(MemNode *n,MemEntry *e,const char *from,const char *to,c } MemNode *into=upm_dir(dir,1);MemEntry *clash=into?mem_find(into,name):NULL; if(!into || (clash && clash->node))return 1; + if(!mem_may(into,3))return -13; upm_count(to,1); /* before it exists: walks look it up */ MemEntry *now=mem_link(into,name,n);int64_t r=now?0:-12; if(!r && disk) { @@ -285,6 +298,7 @@ static int64_t upm_cross(uint64_t call,MemPath *w,const int *res) { if(res[0]==1) { /* from a pending file */ MemNode *n=w[0].node;char from[1025];snprintf(from,sizeof from,"%s",w[0].canon); if(!n || !from[0]) { pthread_mutex_unlock(&mem_lock);return -2; } + if(call!=37 && w[0].dir && !mem_may(w[0].dir,3)) { pthread_mutex_unlock(&mem_lock);return -13; } /* its directory's mode, as mem_rename */ if(call!=37 && !(flags&2)) { /* a rename: in memory where it can be (upm_move) */ n->refs++;pthread_mutex_unlock(&mem_lock); char leaf[1025],to[1025];int parent=fs_lookup(x[2],x[3],leaf,FS_DIRECT,to); diff --git a/scripts/verify_overlay.py b/scripts/verify_overlay.py index 8fb191f..a4c16c4 100644 --- a/scripts/verify_overlay.py +++ b/scripts/verify_overlay.py @@ -12,7 +12,11 @@ mode, time and inode unchanged, and no copy-up leftovers may remain. Last, names that differ only by case (tests/native/case_guest.c) with both layers on a case-sensitive volume, as dockivad keeps them, with and without the memory layer, and statfs there -(tests/native/statfs_guest.c): the size of the volume --statfs-from names.""" +(tests/native/statfs_guest.c): the size of the volume --statfs-from names. And root's +CAP_DAC_OVERRIDE (tests/native/rootdac_guest.c): root changing 0555 and 000 directories +of the upper layer, a bind mount and the image, with and without the memory layer, +other users refused, the modes and the image left as they were, and a tar archive of +0555 directories and 0444 files extracted by busybox tar as root.""" import os from pathlib import Path import subprocess @@ -115,6 +119,68 @@ def upm_runs(t): return failed +def dac_runs(t): + """rootdac_guest.c on every built runtime, with and without the memory layer, a + bind mount at /vol; then what it left: every file on disk, each mode as the guest set it.""" + failed = 0 + lower = t/'dac-lower' + for name in ('low/ro/x', 'low/ro/mv', 'low/ro/keep', 'low/ro2/x'): + (lower/name).parent.mkdir(parents=True, exist_ok=True) + (lower/name).write_text(name.rsplit('/', 1)[1]) + (lower/'low/ro/sub').mkdir() + (lower/'low/ro').chmod(0o555) + (lower/'low/ro2').chmod(0o555) + build(ROOT/'tests/native/rootdac_guest.c', t/'rootdac') + before = snapshot(lower) + + def left(base, hard=True): + """What is wrong under base (the host's /w or /vol) after the run (hard: it made a hard link there).""" + a = base/'a' + wrong = [] if a.stat().st_mode & 0o7777 == 0o555 else [f'a is {oct(a.stat().st_mode)}'] + a.chmod(0o755) # to look inside + want = {'d1b': 'd', 'd3': 'd+', 'd4': 'd', 'd5': 'd', 'y1b': 'y', 'y3': 'y', 'keep': 'kept', 'z': 'z', 'cwd': 'c'} + if (a/'d3').stat().st_mode & 0o7777 != 0: + wrong.append('d3 mode') + (a/'d3').chmod(0o600) + wrong += [k for k, v in want.items() if not (a/k).is_file() or (a/k).read_text() != v] + wrong += [k for k in ('d/in', 'pre2', 'last') if not (a/k).is_dir()] + [k for k in ('pre', 'd1', 'd2', 'y1', 'y2', 'f', 'g', 'mvd') if os.path.lexists(a/k)] + if not (a/'l').is_symlink() or not (a/'p').is_fifo() or (base/'out').stat().st_mode & 0o7777 != 0o555: + wrong.append('link, fifo or moved directory') + if hard and (not (a/'h').is_file() or (a/'h').stat().st_ino != (a/'d4').stat().st_ino): + wrong.append('hard link') + return wrong + [str(p) for p in base.rglob('.wh.*')] + + for binary, runtime, extra in runtimes(): + for memory in (False, True): + tag = f'{binary}-{int(memory)}' + upper, state, vol, ro = t/f'dac-up-{tag}', t/f'dac-state-{tag}', t/f'dac-vol-{tag}', t/f'dac-ro-{tag}' + for d in (upper, state, vol, ro/'locked'): + d.mkdir(parents=True) + (ro/'locked/f').write_text('f') + (ro/'locked').chmod(0) + more = ['--upper-memory', str(state/'upper-sync'), '--layers-gen', str(state/'gen')] if memory else [] + r = subprocess.run([str(runtime), 'run', str(t/'rootdac'), '--root', str(upper), '--lower', str(lower), '--writable', '--bind', str(vol), '/vol', '--bind-ro', str(ro), '/ro', + '--storage-limit-mib', '0', '--max-steps', '2000000000', '--process-tree', '--scheduled-network', '--dynamic', *more, *extra], + capture_output=True, timeout=300) + ok = r.returncode == 0 and r.stdout == b'rootdac PASS\n' and b'ERROR: AddressSanitizer' not in r.stderr + wrong = [] + if ok: + low = upper/'low/ro' + wrong = left(upper/'w') + left(vol, False) + ([] if low.stat().st_mode & 0o7777 == 0o555 else ['low/ro mode']) + low.chmod(0o755) + wrong += [k for k in ('.wh.x', '.wh.sub', '.wh.mv', 'f', 'mv2') if not (low/k).is_file()] + ([] if (low/'new').is_dir() else ['low/ro/new']) + wrong += [] if (ro/'locked').stat().st_mode & 0o7777 == 0 else ['the read-only mount changed'] + wrong += ['low/ro2 copied up'] if (upper/'low/ro2').exists() and (upper/'low/ro2').stat().st_mode & 0o7777 != 0o555 else [] + ok = ok and not wrong + failed += not ok + print(f'{binary} rootdac_guest{" with the memory layer" if memory else ""}:', 'PASS' if ok else + f'FAIL exit={r.returncode} {r.stdout!r} wrong={wrong} {r.stderr.decode()[-1500:]}') + same = snapshot(lower) == before + failed += not same + print('rootdac lower layer unchanged:', 'PASS' if same else 'FAIL') + return failed + + ATTACK = r''' for f in /bin/busybox /lib/ld-musl-aarch64.so.1; do # what this shell runs on: changed, not broken echo pwn >> $f; chmod 777 $f; touch -d 2001-01-01 $f; ln $f $f.hard; mv $f.hard $f.moved; rm $f.moved @@ -156,6 +222,7 @@ def main(): failed += not same print('lower layer unchanged:', 'PASS' if same else f'FAIL {[k for k in set(before)|set(after) if before.get(k) != after.get(k)]}') failed += upm_runs(t) + failed += dac_runs(t) # busybox on alpine's files as the lower layer lower = t/'alpine' @@ -182,6 +249,14 @@ def busybox(upper, script, *more, background=False): ok = r.returncode == 0 and r.stdout.endswith(b'e2e PASS\n') and not leftovers(t/'e2e') failed += not ok print('busybox e2e on alpine layers:', 'PASS' if ok else f'FAIL exit={r.returncode} {r.stdout[-800:]!r} {r.stderr.decode()[-1500:]}') + # tar as root: 0555 directories made with their final mode, then filled; rm -rf after + script = ('set -e; mkdir -p /src/m/a/b /src/m/c; echo 1 > /src/m/a/f; echo 2 > /src/m/a/b/g; echo 3 > /src/m/c/h; chmod 444 /src/m/a/f /src/m/a/b/g /src/m/c/h; ' + 'chmod 555 /src/m/a/b /src/m/a /src/m/c /src/m; tar cf /t.tar -C /src m; mkdir /dst; tar xf /t.tar -C /dst; test "$(find /dst | wc -l)" = 8; ' + 'test "$(stat -c %a /dst/m/a)" = 555; test "$(stat -c %a /dst/m/a/b/g)" = 444; test "$(cat /dst/m/a/b/g)" = 2; rm -rf /dst /src; test ! -e /dst; test ! -e /src; echo tar PASS') + r = busybox(t/'tar', script, '--upper-memory', str(t/'tar-sync')) + ok = r.returncode == 0 and r.stdout.endswith(b'tar PASS\n') and not leftovers(t/'tar') + failed += not ok + print('busybox tar and rm -rf of 0555 directories as root:', 'PASS' if ok else f'FAIL exit={r.returncode} {r.stdout[-800:]!r} {r.stderr.decode()[-1500:]}') r = busybox(t/'attack', ATTACK) out = r.stdout.decode(errors='replace') ok = '==attack done==' in out and 'pwn' in out # the container sees its own changes diff --git a/tests/native/rootdac_guest.c b/tests/native/rootdac_guest.c new file mode 100644 index 0000000..4c34830 --- /dev/null +++ b/tests/native/rootdac_guest.c @@ -0,0 +1,172 @@ +/* Root's CAP_DAC_OVERRIDE (native/fs.h: fs_dac): root changes the entries of a + directory whose mode lacks write permission (0555, as tar leaves Go's module cache + before filling it) or every permission (000), and opens files whatever their mode; + the modes stay as set. Another user, and root without the capability, get EACCES for + each of them. In a directory made in the container (/w), a bind mount (/vol, when + mounted) and one the image has as 0555 (/low/ro), for files on disk and, with + --upper-memory, files still in memory (native/upm.h). Run by scripts/verify_overlay.py, + which then checks what the run left on the host. Hard links are made in /w only: + linkat of a file in a bind mount answers ENOENT beside image layers, whatever the modes. + A read-only bind mount (/ro) keeps its modes, so its 000 directory stays shut to root too. */ +typedef unsigned long U; +static long call(U n,U a,U b,U c,U d,U e,U f) { + register U x8 __asm__("x8")=n,x0 __asm__("x0")=a,x1 __asm__("x1")=b; + register U x2 __asm__("x2")=c,x3 __asm__("x3")=d,x4 __asm__("x4")=e,x5 __asm__("x5")=f; + __asm__ volatile("svc #0":"+r"(x0):"r"(x8),"r"(x1),"r"(x2),"r"(x3),"r"(x4),"r"(x5):"memory","cc");return (long)x0; +} +#define C1(n,a) call(n,(U)(a),0,0,0,0,0) +#define C2(n,a,b) call(n,(U)(a),(U)(b),0,0,0,0) +#define C3(n,a,b,c) call(n,(U)(a),(U)(b),(U)(c),0,0,0) +#define C4(n,a,b,c,d) call(n,(U)(a),(U)(b),(U)(c),(U)(d),0,0) +#define C5(n,a,b,c,d,e) call(n,(U)(a),(U)(b),(U)(c),(U)(d),(U)(e),0) +#define AT (-100) +#define EACCES (-13) +static unsigned slen(const char *s) { unsigned n=0;while(s[n])n++;return n; } +static const char *where=""; +static void fail_at(int line) { + char text[96]="rootdac FAIL line ";unsigned n=18;char digits[8];unsigned k=0; + while(line && k<8) { digits[k++]=(char)('0'+line%10);line/=10; } + while(k)text[n++]=digits[--k]; + text[n++]=' ';for(const char *p=where;*p && n<90;p++)text[n++]=*p; + text[n++]='\n';C3(64,1,text,n);C1(94,1);for(;;){} +} +#define CHECK(x) do { if(!(x))fail_at(__LINE__); } while(0) +static int same(const void *a,const void *b,unsigned n) { const unsigned char *p=a,*q=b;for(unsigned i=0;i=0;i--)v=v<<8|p[i];return v; } +static unsigned char st[128]; +static long stat_of(const char *path) { return C4(79,AT,path,st,256); } /* AT_SYMLINK_NOFOLLOW */ +#define MEMORY (1UL<<62) /* upm.h's inode numbers */ +static int pending(const char *path) { return stat_of(path)==0 && u64(st+8)>=MEMORY; } +static long mode(const char *path) { return stat_of(path)?-1:(long)((st[16]|st[17]<<8)&07777); } +static long open3(const char *path,U flags,U mode) { return C4(56,AT,path,flags,mode); } +static long put(const char *path,const char *text) { + long fd=open3(path,01101,0644);if(fd<0)return fd; /* O_WRONLY|O_CREAT|O_TRUNC */ + long n=C3(64,fd,text,slen(text));C1(57,fd);return n; +} +static long append(const char *path,const char *text) { + long fd=open3(path,02001,0);if(fd<0)return fd; /* O_WRONLY|O_APPEND */ + long n=C3(64,fd,text,slen(text));C1(57,fd);return n; +} +static int has(const char *path,const char *text) { + char buffer[64];long fd=open3(path,0,0);if(fd<0)return 0; + long n=C3(63,fd,buffer,sizeof buffer);C1(57,fd); + return n==(long)slen(text) && same(buffer,text,(unsigned)n); +} +static long mkdir(const char *path) { return C3(34,AT,path,0755); } +static long rmdir(const char *path) { return C3(35,AT,path,0x200); } +static long unlink(const char *path) { return C3(35,AT,path,0); } +static long symlink(const char *path) { return C3(36,"x",AT,path); } +static long mkfifo(const char *path) { return C4(33,AT,path,0010644,0); } +static long link(const char *from,const char *to) { return C5(37,AT,from,AT,to,0); } +static long rename(const char *from,const char *to) { return C4(38,AT,from,AT,to); } +static long chmod(const char *path,U mode) { return C3(53,AT,path,mode); } +static long chdir(const char *path) { return C1(49,path); } +/* Names in a directory (getdents64) besides . and .., or the open's error. */ +static long names(const char *dir) { + unsigned char buffer[1024];long fd=open3(dir,040000,0);if(fd<0)return fd; + long count=0; + for(;;) { + long n=C3(61,fd,buffer,sizeof buffer);if(n<=0)break; + for(long at=0;at0 && call(260,(U)pid,(U)&status,0,0,0,0)==pid && status==0); } + +/* base/a, 0755 still: directories and files that reach the disk before its mode changes. */ +static void setup(const char *b) { + where=b; + CHECK(mkdir(P(b,"/a"))==0 && mkdir(P(b,"/a/pre"))==0 && mkdir(P(b,"/a/pre2"))==0 && mkdir(P(b,"/a/mvd"))==0); + CHECK(put(P(b,"/a/d1"),"d")==1 && put(P(b,"/a/d2"),"d")==1 && put(P(b,"/a/d3"),"d")==1 && put(P(b,"/a/d4"),"d")==1 && put(P(b,"/a/d5"),"d")==1); +} +/* Root, in base/a once it is 0555, then 000. */ +static void root(const char *b) { + where=b; + int layer=b[1]=='w'; + int memory=pending(P(b,"/a/d1")); /* the memory layer holds this run's new files */ + if(memory) { nap();CHECK(!pending(P(b,"/a/d1")) && !pending(P(b,"/a/d5"))); } + CHECK(put(P(b,"/a/y1"),"y")==1 && put(P(b,"/a/y2"),"y")==1 && put(P(b,"/a/y3"),"y")==1 && pending(P(b,"/a/y1"))==memory); + CHECK(chmod(P(b,"/a/mvd"),0555)==0 && chmod(P(b,"/a"),0555)==0); + CHECK(mkdir(P(b,"/a/d"))==0); + CHECK(symlink(P(b,"/a/l"))==0); + CHECK(mkfifo(P(b,"/a/p"))==0); + CHECK(rmdir(P(b,"/a/pre"))==0 && stat_of(P(b,"/a/pre"))==-2); + CHECK(put(P(b,"/a/f"),"new")==3); /* a new file, renamed and removed */ + CHECK(rename(P(b,"/a/f"),P(b,"/a/g"))==0 && has(P(b,"/a/g"),"new") && unlink(P(b,"/a/g"))==0 && stat_of(P(b,"/a/g"))==-2); + CHECK(rename(P(b,"/a/d1"),P(b,"/a/d1b"))==0 && has(P(b,"/a/d1b"),"d")); /* files on disk */ + CHECK(unlink(P(b,"/a/d2"))==0 && stat_of(P(b,"/a/d2"))==-2); + CHECK(rename(P(b,"/a/y1"),P(b,"/a/y1b"))==0 && has(P(b,"/a/y1b"),"y")); /* files made just before the chmod */ + CHECK(unlink(P(b,"/a/y2"))==0 && stat_of(P(b,"/a/y2"))==-2); + CHECK(!layer || (link(P(b,"/a/d4"),P(b,"/a/h"))==0 && has(P(b,"/a/h"),"d"))); + CHECK(rename(P(b,"/a/mvd"),P(b,"/out"))==0 && mode(P(b,"/out"))==0555); /* a 0555 directory to another parent: its ".." is written */ + CHECK(put(P(b,"/a/keep"),"kept")==4); + CHECK(mode(P(b,"/a"))==0555); + /* files open whatever their mode */ + CHECK(chmod(P(b,"/a/d3"),0444)==0 && append(P(b,"/a/d3"),"+")==1 && mode(P(b,"/a/d3"))==0444); + CHECK(chmod(P(b,"/a/d3"),0)==0 && has(P(b,"/a/d3"),"d+") && append(P(b,"/a/d3"),"+")==1 && mode(P(b,"/a/d3"))==0); + CHECK(C2(45,P(b,"/a/d3"),2)==0 && has(P(b,"/a/d3"),"d+") && mode(P(b,"/a/d3"))==0); /* truncate */ + /* no permission at all: listed, searched, entered, changed */ + CHECK(chmod(P(b,"/a"),0)==0); + CHECK(names(P(b,"/a"))==11+layer); /* d l p pre2 d1b d3 d4 d5 y1b y3 keep, and h */ + CHECK(put(P(b,"/a/z"),"z")==1 && has(P(b,"/a/z"),"z") && mkdir(P(b,"/a/d/in"))==0 && mode(P(b,"/a"))==0); + CHECK(chdir(P(b,"/a"))==0 && stat_of("z")==0 && put("cwd","c")==1 && names(".")==13+layer && chdir("/")==0 && mode(P(b,"/a"))==0); + CHECK(chmod(P(b,"/a/."),0700)==0 && mode(P(b,"/a"))==0700); /* the call's own chmod is what stays */ + CHECK(chmod(P(b,"/a"),0555)==0); +} +/* Anyone the mode binds (another user; root without CAP_DAC_OVERRIDE): refused, in base/a at 0555 and 000. */ +static void bound(const char *b,int user) { + where=b; + CHECK(mkdir(P(b,"/a/e"))==EACCES && symlink(P(b,"/a/m"))==EACCES && mkfifo(P(b,"/a/q"))==EACCES); + CHECK(rmdir(P(b,"/a/pre2"))==EACCES && put(P(b,"/a/new"),"n")==EACCES); + CHECK(rename(P(b,"/a/d5"),P(b,"/a/d5x"))==EACCES && unlink(P(b,"/a/d4"))==EACCES); + CHECK(rename(P(b,"/a/y3"),P(b,"/a/y3x"))==EACCES && unlink(P(b,"/a/y3"))==EACCES); + CHECK((b[1]!='w' || link(P(b,"/a/d5"),P(b,"/a/h2"))==EACCES) && rename(P(b,"/out"),P(b,"/a/in"))==EACCES); + CHECK(append(P(b,"/a/d5"),"")==0); /* the file's own mode allows it */ + if(!user)return; /* (the test's root child stops here: a chmod is its to undo) */ + CHECK(chmod(P(b,"/a"),0)==0); + CHECK(names(P(b,"/a"))==EACCES && put(P(b,"/a/zz"),"z")==EACCES && stat_of(P(b,"/a/d5"))==EACCES && chdir(P(b,"/a"))==EACCES); + CHECK(chmod(P(b,"/a"),0555)==0 && chmod(P(b,"/a/d5"),0)==0 && open3(P(b,"/a/d5"),0,0)==EACCES && chmod(P(b,"/a/d5"),0644)==0); +} +void _start(void) { + int vol=mode("/vol")>=0; + CHECK(mkdir("/w")==0); + setup("/w");if(vol)setup("/vol"); + root("/w");if(vol)root("/vol"); + /* a 0555 directory of the image: a whiteout, a new directory and a new file in its upper copy */ + where="/low/ro"; + CHECK(mode("/low/ro")==0555 && unlink("/low/ro/x")==0 && stat_of("/low/ro/x")==-2 && rmdir("/low/ro/sub")==0 && mkdir("/low/ro/new")==0); + CHECK(put("/low/ro/f","f")==1 && rename("/low/ro/mv","/low/ro/mv2")==0 && has("/low/ro/mv2","mv") && mode("/low/ro")==0555 && names("/low/ro")==4); /* keep new f mv2 */ + where="/";CHECK(chmod("/",0555)==0 && mkdir("/top")==0 && rmdir("/top")==0 && mode("/")==0555 && chmod("/",0755)==0); + where="/ro";CHECK(mode("/ro")<0 || (names("/ro/locked")==EACCES && stat_of("/ro/locked/f")==EACCES && mkdir("/ro/new")==-30)); + long pid=fork(); + if(!pid) { /* another user */ + CHECK(C2(159,0,0)==0 && C1(144,1000)==0 && C1(146,1000)==0); + bound("/w",1);if(vol)bound("/vol",1); + where="/low";CHECK(unlink("/low/ro/keep")==EACCES && mkdir("/low/ro/e")==EACCES && mkdir("/low/ro2/e")==EACCES && unlink("/low/ro2/x")==EACCES); + C1(94,0); + } + wait_ok(pid); + pid=fork(); + if(!pid) { /* root having dropped CAP_DAC_OVERRIDE (capability 1) */ + unsigned header[2]={0x20080522,0},data[6]={~2u,~2u,0,0x1ff,0x1ff,0}; + CHECK(C2(91,header,data)==0); + bound("/w",0);if(vol)bound("/vol",0); + C1(94,0); + } + wait_ok(pid); + where="";CHECK(mode("/w/a")==0555 && mkdir("/w/a/last")==0 && (!vol || mkdir("/vol/a/last")==0)); /* the parent still holds it */ + C3(64,1,"rootdac PASS\n",13);C1(94,0);for(;;){} +}