#480·boltons

FilePerms cannot revoke permission bits: setting a field to a subset of its value is a no-op on int(fp)

Author: ArtJackCreated Sep 7, 2026Updated Sep 7, 2026

FilePerms only ever ORs bits into its integer. Setting a field to something narrower than it was — fp.group = '' after group='r' — updates the attribute (and repr) but leaves the previous bits in int(fp). A caller tightening a file's mode through os.chmod(path, int(fp)) therefore leaves the file exactly as permissive as before, while repr(fp) reports the tightened value.

Reproduction (fresh clone at 967864f, Python 3.13 and 3.14, tempdir):

python
import os, tempfile
from boltons.fileutils import FilePerms

with tempfile.TemporaryDirectory() as d:
    p = os.path.join(d, "s.txt")
    open(p, "w").write("secret")
    os.chmod(p, 0o644)
    fp = FilePerms.from_path(p)
    fp.other = ""
    fp.group = ""
    print(repr(fp))                          # FilePerms(user='rw', group='', other='')
    print(oct(int(fp)))                      # 0o644  — expected 0o600
    os.chmod(p, int(fp))
    print(oct(os.stat(p).st_mode & 0o777))   # 0o644  — the file is still group- and world-readable

Mechanismboltons/fileutils.py, FilePerms._FilePermProperty._update_integer (lines 147-153):

python
        def _update_integer(self, fp_obj, value):
            mode = 0
            key = 'xwr'
            for symbol in value:
                bit = 2 ** key.index(symbol)
                mode |= (bit << (self.offset * 3))
            fp_obj._integer |= mode        # bits are only ever added; the field's previous bits are never cleared

Clearing the field's three bits before OR-ing (fp_obj._integer &= ~(0o7 << (self.offset * 3))) makes the integer follow the attributes. It matters most in the one direction people use this class for — revoking access — where the failure is silent and repr says it succeeded.

The existing tests only build up permissions from FilePerms() (int(FilePerms()) == 0, repr after setting fields), so nothing exercises a revoke; a regression test would set a field to a subset and assert on int(fp). The class has behaved this way since it was introduced.

Found by an automated QA pass over the repository (Verdict); the reproduction above was re-run by hand on a fresh clone before filing.