Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion native/fs.h
Original file line number Diff line number Diff line change
Expand Up @@ -1935,7 +1935,7 @@ static uint64_t fs_call_once(uint64_t call) {
if(!fs_path_writable())error=linux_error(30);
else if(where!=fs_where || where<0)error=linux_error(18); /* EXDEV: never across a mount boundary */
else if(!strcmp(from,".") || !strcmp(leaf,"."))error=linux_error(!strcmp(leaf,".")?17:1);
else if(old.layer==2)error=linux_error(2);
else if(old.canon[0] && old.layer==2)error=linux_error(2); /* in the layers; in a bind mount (canon "") the host's linkat reports it */
else if(ov_prepare(&w,&parent,leaf,OV_CREATE) || ov_prepare(&old,&source,from,OV_MODIFY))error=fs_error(); /* image layers: a lower file is copied up */
else if(linkat(source,from,parent,leaf,0)<0)error=fs_error();
else ov_created(&w,parent,leaf,0);
Expand Down
37 changes: 36 additions & 1 deletion scripts/verify_overlay.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
over one shared lower tree, the second run proving the first left the shared files
alone); new files kept in memory (--upper-memory, native/upm.h) renamed while open
over lower and upper files (tests/native/upm_guest.c), and what that run wrote
back: each file whole, and no whiteout beside it; then, with alpine as the lower
back: each file whole, and no whiteout beside it; hard links inside a bind mount
(tests/native/mountlink_guest.c), never across its boundary; then, with alpine as the lower
layer, busybox copying, archiving, renaming
and removing image files, and a container trying to change the image's files every
way it can (writes, chmod, rename, links, truncate, MAP_SHARED writes, sockets,
Expand Down Expand Up @@ -79,6 +80,39 @@ def runtimes():
print(f'{binary}: skipped (not built)')


def mount_link_runs(t):
"""mountlink_guest.c on every built runtime, with and without the memory layer, bind
mounts at /vol and /vol2; then what it left: one file under its three names in the
first mount, nothing in the second, no whiteout in either."""
failed = 0
lower = t/'link-lower'
(lower/'low').mkdir(parents=True)
(lower/'low/f').write_text('f')
build(ROOT/'tests/native/mountlink_guest.c', t/'mountlink')
before = snapshot(lower)
for binary, runtime, extra in runtimes():
for memory in (False, True):
tag = f'{binary}-{int(memory)}'
upper, state, vol, vol2 = t/f'link-up-{tag}', t/f'link-state-{tag}', t/f'link-vol-{tag}', t/f'link-vol2-{tag}'
for d in (upper, state, vol, vol2):
d.mkdir()
more = ['--upper-memory', str(state/'upper-sync'), '--layers-gen', str(state/'gen')] if memory else []
r = subprocess.run([str(runtime), 'run', str(t/'mountlink'), '--root', str(upper), '--lower', str(lower), '--writable', '--bind', str(vol), '/vol', '--bind', str(vol2), '/vol2',
'--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'mountlink PASS\n' and b'ERROR: AddressSanitizer' not in r.stderr
names = sorted(str(p.relative_to(vol)) for p in vol.rglob('*'))
ok = ok and names == ['a', 'b', 'd', 'd/c', 's'] and len({(vol/n).stat().st_ino for n in ('a', 'b', 'd/c')}) == 1 and (vol/'a').stat().st_nlink == 3
ok = ok and not os.listdir(vol2) and not os.path.lexists(upper/'x') and (upper/'low/g').is_file()
failed += not ok
print(f'{binary} mountlink_guest{" with the memory layer" if memory else ""}:', 'PASS' if ok else
f'FAIL exit={r.returncode} {r.stdout!r} vol={names} vol2={os.listdir(vol2)} {r.stderr.decode()[-1500:]}')
same = snapshot(lower) == before
failed += not same
print('mountlink lower layer unchanged:', 'PASS' if same else 'FAIL')
return failed


# upm_guest.c's lower tree, and what its run leaves in the upper layer: files written
# back, and the whiteouts of the lower files it removed (and only those).
UPM_LOWER = {'u/lower': 'old lower', 'u/lower2': 'old lower2', 'u/up': 'old up', 'u/app.log.1': 'old log', 'u/k': 'k', 'u/nr': 'nr',
Expand Down Expand Up @@ -203,6 +237,7 @@ def main():
lower.mkdir()
lower_tree(lower)
build(ROOT/'tests/native/overlay_guest.c', t/'overlay')
failed += mount_link_runs(t)
before = snapshot(lower)
for binary, runtime, extra in runtimes():
for run in (1, 2): # the second run sees the lower layer as the first did, with the lookup cache
Expand Down
65 changes: 65 additions & 0 deletions tests/native/mountlink_guest.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/* Hard links inside a bind mount or named volume beside image layers (native/fs.h:
linkat): both names are one file (one inode, two links), by absolute path, by a
descriptor of the mount's directory, and through a symlink with AT_SYMLINK_FOLLOW; a
missing source is ENOENT, a taken name EEXIST, and a link that would leave the mount
(to another mount, to or from the container's layers) is EXDEV. /vol and /vol2 are
bind mounts, /low/f a file of the image. Run by scripts/verify_overlay.py, with and
without the memory layer, which then checks what the run left on the host. */
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 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 ENOENT (-2)
#define EEXIST (-17)
#define EXDEV (-18)
static void fail_at(int line) {
char text[40]="mountlink FAIL line ";unsigned n=20;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++]='\n';C3(64,1,text,n);C1(94,1);for(;;){}
}
#define CHECK(x) do { if(!(x))fail_at(__LINE__); } while(0)
static unsigned long u64(const unsigned char *p) { unsigned long v=0;for(int i=7;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 */
static unsigned long ino(const char *path) { return stat_of(path)?0:u64(st+8); }
static long links(const char *path) { return stat_of(path)?-1:(long)(st[20]|st[21]<<8); }
static long put(const char *path) {
long fd=C4(56,AT,path,01101,0644);if(fd<0)return fd; /* O_WRONLY|O_CREAT|O_TRUNC */
long n=C3(64,fd,"x",1);C1(57,fd);return n;
}
static int has(const char *path) {
char c[2];long fd=C4(56,AT,path,0,0);if(fd<0)return 0;
long n=C3(63,fd,c,2);C1(57,fd);return n==1 && c[0]=='x';
}
static long link(const char *from,const char *to) { return C5(37,AT,from,AT,to,0); }

void _start(void) {
CHECK(put("/vol/a")==1 && C3(34,AT,"/vol/d",0755)==0);
/* inside the mount: one file under both names */
CHECK(link("/vol/a","/vol/b")==0);
CHECK(ino("/vol/a") && ino("/vol/a")==ino("/vol/b") && links("/vol/a")==2 && has("/vol/b"));
CHECK(link("/vol/b","/vol/d/c")==0 && ino("/vol/d/c")==ino("/vol/a") && links("/vol/a")==3);
long dir=C4(56,AT,"/vol",040000,0);CHECK(dir>=0); /* O_DIRECTORY */
CHECK(C5(37,dir,"a",dir,"d/r",0)==0 && ino("/vol/d/r")==ino("/vol/a"));C1(57,dir);
CHECK(C3(36,"a",AT,"/vol/s")==0 && C5(37,AT,"/vol/s",AT,"/vol/t",0x400)==0 && ino("/vol/t")==ino("/vol/a")); /* AT_SYMLINK_FOLLOW */
CHECK(C3(35,AT,"/vol/t",0)==0 && C3(35,AT,"/vol/d/r",0)==0 && links("/vol/a")==3 && has("/vol/a"));
CHECK(link("/vol/none","/vol/e")==ENOENT && stat_of("/vol/e")==ENOENT);
CHECK(link("/vol/a","/vol/b")==EEXIST && link("/vol/a","/vol/none/e")==ENOENT);
/* never across the mount's boundary */
CHECK(link("/vol/a","/vol2/x")==EXDEV && stat_of("/vol2/x")==ENOENT);
CHECK(link("/vol/a","/x")==EXDEV && link("/vol/a","/low/x")==EXDEV && stat_of("/x")==ENOENT && stat_of("/low/x")==ENOENT);
CHECK(link("/low/f","/vol/x")==EXDEV && put("/up")==1 && link("/up","/vol/x")==EXDEV && stat_of("/vol/x")==ENOENT);
/* the layers as before: an image file is linked (copied up), a missing one is not */
CHECK(link("/low/f","/low/g")==0 && ino("/low/g")==ino("/low/f") && links("/low/f")==2);
CHECK(link("/none","/y")==ENOENT && link("/low/none","/low/y")==ENOENT);
CHECK(links("/vol/a")==3);
C3(64,1,"mountlink PASS\n",15);C1(94,0);for(;;){}
}
Loading