sudo can't write to temp file

I'm trying to understand why sudo doesn't have permission here

touch fred.txt
/tmp$ ls -la fred.txt
-rw-rw-r-- 1 me me 0 Dec 12 15:40 fred.txt
/tmp$ sudo -i
~# cd /tmp
/tmp# echo hi >> fred.txt
-bash: fred.txt: Permission denied
/tmp# chmod 666 fred.txt
/tmp# ls -la fred.txt
-rw-rw-rw- 1 me me 0 Dec 12 15:40 fred.txt
/tmp# echo hi >> fred.txt
-bash: fred.txt: Permission denied
id
uid=0(root) gid=0(root) groups=0(root)

As I understand it the permission 666 should give owner, group and other permission to r/w to the file. Surly sudo which is acting as root as evidenced by the id command has access to the file under the 'other' permission.

What am I misunderstanding here?


It has something to do with this commit in the Linux kernel. Often /tmp is "world-writable" ("world" here means "others") and is "sticky":

$ stat /tmp/
...
Access: (1777/drwxrwxrwt)  Uid: (    0/    root)   Gid: (    0/    root)
...

chmod(2):

...
S_ISVTX  (01000)  sticky bit (restricted deletion flag, as described in unlink(2))
...
S_IWOTH  (00002)  write by others
...

As mentioned in the commit message, it doesn't exactly prevent you from writing to an affected file, but from opening such a file with O_CREAT, which is a flag for open() that is typically set to trigger file creation if the target does not exist yet. Therefore, you can still write to an affected file with the flag unset (which can be done with dd):

# ls -l /tmp/meh 
-rwxrwxrwx 1 tom tom 0 Dec 12 17:33 /tmp/meh
# id
uid=0(root) gid=0(root) groups=0(root)
# echo test | dd of=/tmp/meh oflag=append conv=notrunc
dd: failed to open '/tmp/meh': Permission denied
# echo test | dd of=/tmp/meh oflag=append conv=notrunc,nocreat
0+1 records in
0+1 records out
5 bytes copied, 9.4981e-05 s, 52.6 kB/s
# 

Note that it's purely about whether the flag is set, but not whether a file needs to be created, since in the case of concern, the file always exists.

Also note that, as mentioned in the commit message, the file could not be opened with O_CREAT not just because it was not owned by user that attempted to open it (root), but also because its owner (tom) is different from the owner (root) of the directory (/tmp). If /tmp/ were owned by tom as /tmp/meh was, root would have been able to open it with O_CREAT.

Also see this commit in systemd which enables the "protection" on distros that adopted it:

# sysctl fs.protected_regular
fs.protected_regular = 1

Note that the related sysctls can also be set 2 (or 0).