fsck: BAD_FILEMODE diagnostic broken / never triggers
- From
- Xavier Morel <xavier.morel@masklinn.net>
- Date
- Aug 6, 2022, 13:43 UTC
- Message-ID
- <B3488A12-BCE3-48C8-915C-E2AC4E71ECD2@masklinn.net>
While looking at git's handling of invalid objects, I stumbled upon git apparently not caring much about file mode values, except for the following requirements:
- must be non-empty (badTree) - must be octal (badTree) - should not be zero-padded (zeroPaddedFilemode)
Looking at tree-walk this is obvious from decode_tree_entry: it calls canon_mode, which
- checks the S_IFMT exactly, and falls back on 160000 (S_IFGITLINK) for cases other than S_ISREG, S_ISLNK, or S_ISDIR - returns the corresponding S_IF (with the access modes unset) for all cases other than S_ISREG, for which it returns 755 if S_IXUSR is set, otherwise 644
However looking at fsck_tree, it does have a fair amount of code to validate entry modes and a dedicated message id for this (BAD_FILEMODE / badFilemode), it even has a code path for legacy entries with S_IWGRP set (extensively documented under `git fsck --strict`).
I guess, over time mode canonicalisation has slowly creeped earlier the tree-parsing code, and (seemingly for several years) it has been occurring before "git fsck" gets tree entry information, so git fsck simply can not see invalid entry modes?
Reproduction:
- create an empty git repository - create tree for (or with) any possible entry type value - run `git fsck --strict --no-dangling`
Expected:
all the trees except the 5 with valid filemodes should be flagged
Observed:
> git fsck --strict --no-dangling
Checking object directories: 100% (256/256), done. notice: HEAD points to an unborn branch (master) notice: No default references
Script to generate one tree for each filemode between 1 and 777777 (inclusive), can be run from a (non-bare) repository root, beware that the resulting repository takes about 1GB.
from hashlib import sha1 from pathlib import Path from zlib import compress
def hash_object(type, content):
obj = b"%s %d\0%s" % (type.encode(), len(content), content)
oid = sha1(obj).hexdigest() e_dir = GIT_ROOT / 'objects' / oid[:2]
e_dir.mkdir(exist_ok=True, parents=True) e = e_dir / oid[2:]
if not e.exists():
e.write_bytes(compress(obj))return oid
# technically S_IFMT caps out at 200_000 - 1, but get_mode doesn't
# actually have a limit
MAX_ENTRY = 0o777_777
if __name__ == '__main__':
GIT_ROOT = Path.cwd() / '.git'
assert GIT_ROOT.is_dir()
blob_hex = hash_object('blob', b"")
blob_oid = bytes.fromhex(blob_hex)
print("empty blob", blob_hex) tree_hex = hash_object("tree", b"100644 empty_blob\0" + blob_oid)
tree_oid = bytes.fromhex(tree_hex)
print("base tree", tree_hex) for mode in range(1, MAX_ENTRY+1):
print(f"\r{mode:06o}/{MAX_ENTRY:o}", end=' ')
tree = b"%o x\0%s" % (
mode,
tree_oid if (mode & 0o170_000) == 0o040_000 else blob_oid,
)
h = hash_object("tree", tree)
print("=", h, end='', flush=True)