How NFS Root Squash Silently Breaks Your File Timestamps
NFS root_squash causes chmod to fail silently during migration. That chmod failure cascades: timestamps never get set, but the file is counted as transferred successfully.
2,014 files transferred successfully. Zero errors in the log. Checksums pass. Data is intact.
Then you run verification, and every single timestamp is wrong. Not a few. All of them. The modification dates on the destination are the time of the transfer, not the original dates from the source.
No error. No warning. The tool said it was done. What happened?
Meet root_squash
If you run NFS, you’re almost certainly running root_squash. It’s the default on every major NFS server distribution. Most admins never change it because most admins never need to think about it.
Here’s what it does: when a client connects as root (UID 0), the NFS server remaps that user to nobody (UID 65534). Root on the client becomes nobody on the server. This is a security feature. Without it, a root user on any client machine has root access to every file on your NFS export.
# /etc/exports (typical NFS server config)
/srv/data 192.168.1.0/24(rw,sync,root_squash)
That root_squash is the default. You don’t even have to write it. If your exports line doesn’t say no_root_squash, squashing is active.
root_squash is the silent default
If your /etc/exports just says (rw,sync), root_squash is still active. It’s only off when you explicitly add no_root_squash. This catches a lot of people because there’s nothing in the config that tells you it’s on.
The chain reaction
Your migration tool runs as root. That’s normal. It needs root to read files owned by any user on the source, and to set ownership on the destination. Here’s the sequence when it writes a file to an NFS destination with root_squash enabled:
- File copy succeeds. Writing file data works fine. The file is created as
nobody:nogroup, but the bytes are correct. - chown succeeds (sometimes). If the tool sets ownership, this may or may not work depending on the server config, but let’s focus on permissions.
- chmod fails. The tool tries to set the file’s permission bits. But
nobodydoesn’t own the file in the way the server expects, and the SETATTR call returnsEPERM. Operation not permitted. - The tool logs the error and moves on. Most tools treat chmod failure as non-fatal. The data transferred. Permissions are secondary.
- chtimes never runs. Here’s the chain reaction. The chmod and chtimes calls are in the same metadata-preservation block. When chmod returns an error, the function returns early. The
chtimescall that sets the file’s modification timestamp sits after the chmod call. It never executes. - The file is counted as a successful transfer.
The data is there. The checksum is correct. The tool marks it done. But the timestamp is wrong because the one function that sets it was skipped by an early return from a completely unrelated operation.
The silent cascade
chmod failure is the trigger, but timestamp loss is the consequence. These two operations have no logical dependency. Permissions and timestamps are independent metadata. But in most implementations, they share an error path, so one failure kills both.
Why it’s invisible
This failure mode has three layers of camouflage:
The data is correct. File sizes match. Checksums pass. The most important thing, the actual content, is fine. Any verification that only checks data integrity will give you a green light.
The chmod error is buried. It shows up in debug logs, maybe. It’s not a transfer failure. Most tools classify it as a warning at best. In a migration of 100,000 files, 2,000 chmod warnings scrolling by at full speed don’t register.
The timestamp loss leaves no trace. There’s no error for chtimes because chtimes never ran. You can’t grep for a failure that didn’t happen. The only evidence is the absence of correct timestamps on the destination, and you’d have to compare them with the source to notice.
How to detect it
Check your NFS exports:
# On the NFS server
cat /etc/exports
If you see root_squash or don’t see no_root_squash, squashing is active.
Check for EPERM in your tool’s logs:
grep -i "EPERM\|operation not permitted\|permission denied" /var/log/migration*.log
If you see chmod failures on the NFS destination, timestamps are almost certainly wrong too.
Compare timestamps between source and destination:
# Quick spot check
stat -c '%y %n' /source/path/to/file
stat -c '%y %n' /dest/path/to/file
If the destination shows the date of your migration run, not the original date, you’ve been hit.
Bulk comparison:
find /source -type f -exec stat -c '%Y %n' {} \; | sort > /tmp/src-times.txt
find /dest -type f -exec stat -c '%Y %n' {} \; | sort > /tmp/dst-times.txt
diff /tmp/src-times.txt /tmp/dst-times.txt | head -20
If every timestamp on the right side clusters around the same date (the day you ran the migration), the chain reaction happened.
Fix options
Option 1: Disable root_squash (security trade-off)
# /etc/exports
/srv/data 192.168.1.0/24(rw,sync,no_root_squash)
Then exportfs -ra to reload. This gives client root full access to the export. chmod works, chtimes works, everything works. But you’ve removed a security boundary. Only do this on a trusted network with trusted clients, and ideally re-enable squashing after the migration completes.
Option 2: Run as a non-root user with matching UIDs
If the user running the migration tool matches the file owner on the NFS server, chmod works without root. This means:
- The migration user’s UID on the client must match the file owner’s UID on the server
- This only works if all files share one owner, which is rare
Not practical for most migrations, but worth knowing.
Option 3: Handle failures independently
This is the real fix. chmod and chtimes are independent operations. There’s no reason a failure in one should prevent the other from running. The code should:
try chmod → log error if it fails → continue
try chtimes → log error if it fails → continue
report which metadata operations succeeded and which didn't
This way, even if root_squash blocks the chmod, your timestamps still get set. And you get a clear log showing exactly which operations failed and which succeeded, instead of a silent cascade.
syncopio advantage
syncopio treats chmod and chtimes as independent operations. If chmod fails (common with root_squash), it logs the failure and still sets the timestamp. If chtimes fails, it logs that separately. Neither operation can prevent the other from running. Your verification report shows exactly which metadata was preserved and which wasn’t.
Post-migration verification checklist
After any NFS migration, especially when running as root:
1. Check for root_squash on the destination
# On the NFS server
grep -v "^#" /etc/exports | grep -v "no_root_squash"
If results show up, squashing is active on those exports.
2. Compare permissions
stat -c '%a %n' /source/somefile
stat -c '%a %n' /dest/somefile
3. Compare timestamps
stat -c '%y %n' /source/somefile
stat -c '%y %n' /dest/somefile
4. Check for EPERM in logs
Any EPERM on the destination mount during metadata operations means the chain reaction may have fired.
5. Count mismatches at scale
# Files where dest mtime differs from source
paste <(find /source -type f -exec stat -c '%Y %n' {} \; | sort -k2) \
<(find /dest -type f -exec stat -c '%Y %n' {} \; | sort -k2) \
| awk '$1 != $3 { count++ } END { print count " timestamp mismatches" }'
If that number matches your total file count, every file was affected. That’s the signature of a full cascade.
Check before you migrate, not after
Run a test transfer of 10 files to the NFS destination and immediately compare timestamps. If root_squash is going to cause problems, you’ll see it on file #1. Don’t find out after transferring two million files.
Further reading: