Summary
On a macOS host, files on the virtiofs bind mount of the host workspace are reported as entirely sparse: st_blocks is 0, lseek(SEEK_DATA) returns EOF, and lseek(SEEK_HOLE) returns 0. read() returns the correct bytes, so most tools are fine — but any tool that trusts hole detection silently produces a file of the correct length containing only NUL bytes.
cp(1) is such a tool. cp workspace-file /tmp/backup is 100% silent data loss: the destination has the right size, the right mtime, and no content. I lost a source file this way — the copy was zeros, and later restoring "the backup" over the original destroyed it too.
This is a data-integrity bug, not a performance one, and it is silent: cp exits 0.
Reproduction
100% reproducible, 400/400 attempts. From inside the sandbox, with a host workspace bind-mounted at /Users/<user>/workspace/<repo>:
$ cp /Users/<user>/workspace/<repo>/some-file.go /tmp/copy.go
$ cmp /Users/<user>/workspace/<repo>/some-file.go /tmp/copy.go
/tmp/copy.go differs: byte 1, line 1
$ python3 -c "d=open('/tmp/copy.go','rb').read(); print(len(d), d.count(bytes(1)))"
22166 22166 # correct length, every byte NUL
The source is never damaged; only the copy.
Root cause
The virtiofs mount misreports sparseness. Probing the same file content on the bind mount vs. on the sandbox's overlay root:
virtiofs (/Users/<user>/workspace/<repo>/service.go):
size=22166
lseek(0, SEEK_DATA) = 22166 <-- EOF: "no data anywhere in this file"
lseek(0, SEEK_HOLE) = 0 <-- "a hole begins at offset 0"
st_blocks = 0
overlayfs (/tmp/ref.go, byte-identical content):
size=22166
lseek(0, SEEK_DATA) = 0
lseek(0, SEEK_HOLE) = 22166
st_blocks = 48
cp reads that as "the entire file is a hole" and writes a hole of the same length. Anything using plain read() is unaffected, which is why this hid for so long.
What is and isn't affected
| Method |
Result |
cp SRC DST |
all NUL |
cp --sparse=never |
all NUL |
cp --reflink=never |
all NUL |
cp --sparse=always |
all NUL |
cat SRC > DST |
correct |
dd if=SRC of=DST |
correct |
python3 shutil.copyfile |
correct |
cp overlay → overlay |
correct |
Direction matters: the source must be on the virtiofs mount. copy_file_range(2) is not involved — the kernel refuses it here with EXDEV, so coreutils already falls back.
Note that all four --sparse/--reflink variants fail, so there is no cp flag to work around this with.
Impact
Anything that copies out of the workspace: cp in build scripts, backup-before-edit patterns, Makefile install steps, packaging, and coding agents (which routinely snapshot a file before rewriting it). All get zeros, silently. rsync, install, and tar may share the same assumption — I did not test them.
Environment
- Host: macOS (Apple silicon)
- Sandbox:
Linux 7.0.12 #1 SMP PREEMPT Wed Aug 12 16:05:38 UTC 2026 aarch64
- Workspace mode: direct (host tree bind-mounted, not
--clone)
- Mount:
TARGET=/Users/<user>/workspace/<repo> SOURCE=host[/Users/<user>/workspace/<repo>] FSTYPE=virtiofs OPTIONS=rw,nosuid,nodev,relatime
cp (GNU coreutils) 9.7
Suggested fix
Have the virtiofs server report real allocation for host files — st_blocks consistent with the data, and SEEK_DATA/SEEK_HOLE reflecting actual extents. If extent information cannot be obtained from the host filesystem, the safe fallback is to report the file as fully allocated (SEEK_DATA = offset, SEEK_HOLE = EOF, non-zero st_blocks) rather than fully sparse. Over-reporting allocation costs a little I/O; under-reporting it loses data.
Summary
On a macOS host, files on the virtiofs bind mount of the host workspace are reported as entirely sparse:
st_blocksis0,lseek(SEEK_DATA)returns EOF, andlseek(SEEK_HOLE)returns0.read()returns the correct bytes, so most tools are fine — but any tool that trusts hole detection silently produces a file of the correct length containing only NUL bytes.cp(1)is such a tool.cp workspace-file /tmp/backupis 100% silent data loss: the destination has the right size, the right mtime, and no content. I lost a source file this way — the copy was zeros, and later restoring "the backup" over the original destroyed it too.This is a data-integrity bug, not a performance one, and it is silent:
cpexits 0.Reproduction
100% reproducible, 400/400 attempts. From inside the sandbox, with a host workspace bind-mounted at
/Users/<user>/workspace/<repo>:The source is never damaged; only the copy.
Root cause
The virtiofs mount misreports sparseness. Probing the same file content on the bind mount vs. on the sandbox's overlay root:
cpreads that as "the entire file is a hole" and writes a hole of the same length. Anything using plainread()is unaffected, which is why this hid for so long.What is and isn't affected
cp SRC DSTcp --sparse=nevercp --reflink=nevercp --sparse=alwayscat SRC > DSTdd if=SRC of=DSTpython3 shutil.copyfilecpoverlay → overlayDirection matters: the source must be on the virtiofs mount.
copy_file_range(2)is not involved — the kernel refuses it here withEXDEV, so coreutils already falls back.Note that all four
--sparse/--reflinkvariants fail, so there is nocpflag to work around this with.Impact
Anything that copies out of the workspace:
cpin build scripts, backup-before-edit patterns,Makefileinstall steps, packaging, and coding agents (which routinely snapshot a file before rewriting it). All get zeros, silently.rsync,install, andtarmay share the same assumption — I did not test them.Environment
Linux 7.0.12 #1 SMP PREEMPT Wed Aug 12 16:05:38 UTC 2026 aarch64--clone)TARGET=/Users/<user>/workspace/<repo> SOURCE=host[/Users/<user>/workspace/<repo>] FSTYPE=virtiofs OPTIONS=rw,nosuid,nodev,relatimecp (GNU coreutils) 9.7Suggested fix
Have the virtiofs server report real allocation for host files —
st_blocksconsistent with the data, andSEEK_DATA/SEEK_HOLEreflecting actual extents. If extent information cannot be obtained from the host filesystem, the safe fallback is to report the file as fully allocated (SEEK_DATA= offset,SEEK_HOLE= EOF, non-zerost_blocks) rather than fully sparse. Over-reporting allocation costs a little I/O; under-reporting it loses data.