File Ownership and the chown Model
Every object in a Linux filesystem is an inode, and every inode carries an owning user ID (
i_uid) and owning group ID (i_gid) — the two numbers that, together with the nine permission bits, drive every discretionary access-control decision the kernel makes about that file. Thechown(2)family of system calls changes those two numbers. But ownership is not freely reassignable: changing the owner of a file requires theCAP_CHOWNcapability (effectively, root), because the ability to give a file away — or claim someone else’s — would shred any quota or accountability model; whereas changing only the group is permitted to the file’s owner, restricted to groups the owner is actually a member of. A subtle but security-critical side effect rides along: a successfulchown(and any unprivileged write) strips the set-user-ID and set-group-ID bits, because letting ownership change while a setuid bit persists would be a trivial privilege-escalation primitive. This note traces the model end to end against the Linux v6.12 source —chown_common()infs/open.c,setattr_prepare()/chown_ok()/chgrp_ok()infs/attr.c, andinode_init_owner()infs/inode.c(all GPL-2.0, torvalds/linux v6.12).
Mental Model
Think of an inode’s (uid, gid) pair as a two-part name tag the kernel reads on every access. The user-ID half answers “is the accessing process the owner?” and selects the owner permission triad; the group-ID half answers “is the accessing process in the file’s group?” and selects the group triad; everyone else falls to the “other” triad. (The triad-selection logic itself lives in the sibling note File Permissions and the Mode Bits.) chown rewrites that name tag — and because the name tag is the identity the access checks key off, the kernel is conservative about who may rewrite it.
The asymmetry between owner-change and group-change is the crux. Giving away ownership is privileged; reassigning the group is delegated to the owner, but only among groups the owner already belongs to. This mirrors the real-world intuition: you may file your own document under any folder you have access to, but you may not stamp someone else’s name on it, nor file it under a department you’re not in.
flowchart TD CALL["chown(path, newuid, newgid)<br/>userspace"] --> CC["chown_common() fs/open.c<br/>build struct iattr newattrs"] CC --> VALID["ia_valid = ATTR_CTIME<br/>+ ATTR_UID if uid != -1<br/>+ ATTR_GID if gid != -1"] VALID --> KILL{"is it a directory?"} KILL -->|"no (regular file etc.)"| ADDKILL["OR in ATTR_KILL_SUID<br/>+ ATTR_KILL_PRIV<br/>+ setattr_should_drop_sgid()"] KILL -->|"yes"| SKIP["leave special bits intact"] ADDKILL --> NC["notify_change()"] SKIP --> NC NC --> SP["setattr_prepare() fs/attr.c"] SP --> CHK{"permission gate"} CHK -->|"ATTR_UID set"| CHOWNOK["chown_ok():<br/>fsuid == owner AND no change,<br/>OR CAP_CHOWN"] CHK -->|"ATTR_GID set"| CHGRPOK["chgrp_ok():<br/>owner AND target group<br/>in caller's groups,<br/>OR CAP_CHOWN"] CHOWNOK -->|"fail"| EPERM["return -EPERM"] CHGRPOK -->|"fail"| EPERM CHOWNOK -->|"pass"| APPLY["fs setattr: write i_uid/i_gid,<br/>strip suid/sgid"] CHGRPOK -->|"pass"| APPLY
The control flow of a chown() call through the v6.12 VFS. What it shows: the syscall lands in chown_common(), which assembles a struct iattr describing the change and unconditionally tacks on the suid/sgid/priv-killing flags for non-directories before handing off to notify_change() → setattr_prepare(), where the per-attribute permission gates (chown_ok for the UID change, chgrp_ok for the GID change) decide pass or -EPERM. The insight to take: the privilege check and the suid-stripping are two independent mechanisms stapled to the same call — the stripping happens regardless of which gate let the call through, which is exactly why it cannot be bypassed.
Mechanical Walk-through
Where ownership lives
The owning IDs are fields on the in-memory inode: inode->i_uid (a kuid_t) and inode->i_gid (a kgid_t). These are kernel-internal ID types (kuid_t/kgid_t) that are namespace-relative — the same on-disk number maps to different kuid_t values inside different user namespaces, which is what makes unprivileged user namespaces and idmapped mounts work. For this note’s purposes, treat them as “the owner UID” and “the owner GID”; the namespace translation (make_kuid, i_uid_into_vfsuid) is plumbing layered on top of the same model. The on-disk inode persists these numbers; the in-memory inode is what every access check reads.
The syscall surface
chown(2), lchown(2), fchown(2), and fchownat(2) are four entry points onto one implementation. In v6.12 fs/open.c, chown and lchown are thin wrappers over do_fchownat() with AT_FDCWD and (for lchown) the AT_SYMLINK_NOFOLLOW flag — lchown operates on the symlink itself rather than its target. fchown resolves a file descriptor via ksys_fchown() → vfs_fchown(). All of them converge on chown_common() (fs/open.c v6.12):
int chown_common(const struct path *path, uid_t user, gid_t group)
{
struct inode *inode = path->dentry->d_inode;
struct iattr newattrs;
kuid_t uid;
kgid_t gid;
uid = make_kuid(current_user_ns(), user);
gid = make_kgid(current_user_ns(), group);
/* ... */
newattrs.ia_vfsuid = INVALID_VFSUID;
newattrs.ia_vfsgid = INVALID_VFSGID;
newattrs.ia_valid = ATTR_CTIME;
if ((user != (uid_t)-1) && !setattr_vfsuid(&newattrs, uid))
return -EINVAL;
if ((group != (gid_t)-1) && !setattr_vfsgid(&newattrs, gid))
return -EINVAL;
inode_lock(inode);
if (!S_ISDIR(inode->i_mode))
newattrs.ia_valid |= ATTR_KILL_SUID | ATTR_KILL_PRIV |
setattr_should_drop_sgid(idmap, inode);
/* ... security_path_chown ... */
error = notify_change(idmap, path->dentry, &newattrs, &delegated_inode);
inode_unlock(inode);
/* ... */
}Reading it line by line: make_kuid/make_kgid translate the userspace-supplied numbers into namespace-relative kernel IDs. The sentinel value -1 (cast from (uid_t)-1) means “don’t change this field” — passing -1 for the UID and a real number for the GID is how chgrp is implemented (it’s chown with the UID left alone). ia_valid starts as ATTR_CTIME (any change bumps the inode’s change time), and ATTR_UID/ATTR_GID get added by setattr_vfsuid/setattr_vfsgid only when a real value was supplied. Then the suid-stripping flags are OR-ed in — discussed below. Finally notify_change() carries the assembled struct iattr into the permission gate and the filesystem.
The permission gate: setattr_prepare, chown_ok, chgrp_ok
notify_change() calls setattr_prepare() (fs/attr.c v6.12), which checks each requested attribute change against the caller’s credentials. For ownership the two relevant gates are:
static bool chown_ok(struct mnt_idmap *idmap,
const struct inode *inode, vfsuid_t ia_vfsuid)
{
vfsuid_t vfsuid = i_uid_into_vfsuid(idmap, inode);
if (vfsuid_eq_kuid(vfsuid, current_fsuid()) &&
vfsuid_eq(ia_vfsuid, vfsuid))
return true;
if (capable_wrt_inode_uidgid(idmap, inode, CAP_CHOWN))
return true;
/* ... */
return false;
}The first branch is the surprising one: an unprivileged caller passes chown_ok only if they already own the file and the “new” UID equals the current UID — i.e. a no-op. In other words, an unprivileged process can never change the owner of a file at all; it can at most reassert the existing owner. Any real owner-change demands the second branch, CAP_CHOWN. This is exactly what the man page states: “Only a privileged process (Linux: one with the CAP_CHOWN capability) may change the owner of a file” (chown(2)). The capable_wrt_inode_uidgid() wrapper is the namespace-aware capability check — it confirms the caller holds CAP_CHOWN and that the inode’s owner maps into the caller’s user namespace, which is what stops a user-namespace root from chowning files it doesn’t conceptually own.
The group gate is more permissive:
static bool chgrp_ok(struct mnt_idmap *idmap,
const struct inode *inode, vfsgid_t ia_vfsgid)
{
vfsgid_t vfsgid = i_gid_into_vfsgid(idmap, inode);
vfsuid_t vfsuid = i_uid_into_vfsuid(idmap, inode);
if (vfsuid_eq_kuid(vfsuid, current_fsuid())) {
if (vfsgid_eq(ia_vfsgid, vfsgid))
return true;
if (vfsgid_in_group_p(ia_vfsgid))
return true;
}
if (capable_wrt_inode_uidgid(idmap, inode, CAP_CHOWN))
return true;
/* ... */
return false;
}Here the first branch is genuinely useful for unprivileged callers: if you are the file’s owner (vfsuid == current_fsuid()), you may set the group either to its current value (a no-op) or to any group of which you are a member (vfsgid_in_group_p, which scans the caller’s primary and supplementary groups). This is the man page rule verbatim: “The owner of a file may change the group of the file to any group of which that owner is a member” (chown(2)). CAP_CHOWN (the second branch) lets a privileged process set the group to anything, member or not. If neither gate returns true, setattr_prepare returns -EPERM, which propagates back as the syscall’s error.
The full setattr_prepare dispatch shows the structure cleanly:
if ((ia_valid & ATTR_UID) &&
!chown_ok(idmap, inode, attr->ia_vfsuid))
return -EPERM;
if ((ia_valid & ATTR_GID) &&
!chgrp_ok(idmap, inode, attr->ia_vfsgid))
return -EPERM;Note the gates run independently and only when their bit is set. A chgrp (UID -1, so ATTR_UID is clear) never invokes chown_ok, which is why a non-owner-but-group-member operation has exactly the semantics described and nothing more.
The setuid/setgid-clearing side effect
The most security-relevant behavior of chown is what it does to the set-user-ID (S_ISUID, octal 04000) and set-group-ID (S_ISGID, 02000) mode bits. Recall (from setuid setgid and the Sticky Bit) that a setuid executable runs with the file owner’s identity. Now imagine chown did not clear setuid: an attacker creates a shell script — no, a compiled binary — sets it world-executable, and then somehow gets root to chown root:root it while the setuid bit is on. Instantly it is a setuid-root shell. Even without root, allowing an owner to keep a setuid bit through ownership transfer breaks the invariant that “a setuid binary runs as whoever set it up.” So the kernel clears the bits on every ownership change of a non-directory.
The mechanism is the ATTR_KILL_SUID | ATTR_KILL_PRIV | setattr_should_drop_sgid() OR-in shown earlier, guarded by if (!S_ISDIR(inode->i_mode)). Directories are exempt because on a directory S_ISGID means something entirely different (group inheritance, below) and S_ISUID is meaningless. The helpers in fs/attr.c encode the exact rule:
int setattr_should_drop_sgid(struct mnt_idmap *idmap,
const struct inode *inode)
{
umode_t mode = inode->i_mode;
if (!(mode & S_ISGID))
return 0;
if (mode & S_IXGRP)
return ATTR_KILL_SGID;
if (!in_group_or_capable(idmap, inode, i_gid_into_vfsgid(idmap, inode)))
return ATTR_KILL_SGID;
return 0;
}Walking this: if the setgid bit isn’t set, nothing to drop. If it is set and the group-execute bit (S_IXGRP) is also set — meaning this is a genuine setgid executable — then ATTR_KILL_SGID is returned and the bit will be stripped. But if S_ISGID is set without S_IXGRP, the bit is not an executable-privilege marker; on Linux it historically denotes mandatory file locking, and the kernel preserves it (unless the caller is neither in the file’s group nor holds the relevant capability, a narrow case). This is precisely the carve-out the man page calls out: “In case of a non-group-executable file (i.e., one for which the S_IXGRP bit is not set) the S_ISGID bit indicates mandatory locking, and is not cleared by a chown()” (chown(2)).
The companion setattr_should_drop_suidgid() (used for the write path rather than chown, but the same idea) shows that setuid is unconditional:
int setattr_should_drop_suidgid(struct mnt_idmap *idmap,
struct inode *inode)
{
umode_t mode = inode->i_mode;
int kill = 0;
/* suid always must be killed */
if (unlikely(mode & S_ISUID))
kill = ATTR_KILL_SUID;
kill |= setattr_should_drop_sgid(idmap, inode);
if (unlikely(kill && !capable(CAP_FSETID) && S_ISREG(mode)))
return kill;
return 0;
}Two things to read here. First, the comment suid always must be killed — there is no group-execute subtlety for setuid; if S_ISUID is set, it dies. Second, the final guard: the stripping is suppressed if the caller holds CAP_FSETID. That capability is defined precisely as the right to “Don’t clear set-user-ID and set-group-ID mode bits when a file is modified” (capabilities(7)). It exists so that backup/restore tooling and package managers, which legitimately need to write files while preserving their setuid bits, can do so. The stripping also only applies to regular files (S_ISREG).
The same stripping fires on an unprivileged write to a setuid file — file_remove_privs() (which calls dentry_needs_remove_privs() → setattr_should_drop_suidgid()) runs on the write path. So if a non-root user (lacking CAP_FSETID) writes a single byte into a setuid binary they happen to own, the setuid bit vanishes. This is the same defense from a different angle: you cannot smuggle code into a setuid binary and keep it setuid.
Group inheritance on new files: the setgid directory
The other half of the ownership story is what (uid, gid) a newly created file gets. By default a new inode is owned by the creating process’s filesystem UID and GID. But there is one important override: if the parent directory has its setgid bit (S_ISGID) set, new files created in it inherit the directory’s group rather than the creator’s primary group — and new subdirectories additionally inherit the setgid bit itself, so the behavior propagates down the tree. This is the standard mechanism for shared project directories where everyone’s files should belong to a common group regardless of each user’s default group. The logic is inode_init_owner() (fs/inode.c v6.12):
void inode_init_owner(struct mnt_idmap *idmap, struct inode *inode,
const struct inode *dir, umode_t mode)
{
inode_fsuid_set(inode, idmap);
if (dir && dir->i_mode & S_ISGID) {
inode->i_gid = dir->i_gid;
/* Directories are special, and always inherit S_ISGID */
if (S_ISDIR(mode))
mode |= S_ISGID;
} else
inode_fsgid_set(inode, idmap);
inode->i_mode = mode;
}Line by line: the new inode’s UID is always the creating process’s fsuid (inode_fsuid_set) — there is no directory-based UID inheritance, only group inheritance. Then, if the parent directory dir carries S_ISGID, the new inode’s group (i_gid) is copied from the directory (dir->i_gid) instead of from the process’s fsgid; and if the new object is itself a directory, S_ISGID is forced on so the policy cascades. Otherwise (else branch) the group comes from the process (inode_fsgid_set). This is exactly the inode(7) description: “files created there inherit their group ID from the directory, not from the effective group ID of the creating process, and directories created there will also get the S_ISGID bit set” (inode(7)). The deeper mechanics of why you’d set a setgid directory and how it composes with umask belong to setuid setgid and the Sticky Bit and The umask and Default Permissions; here the point is simply that the owning group of a new inode is not always the creator’s.
Failure Modes and Common Misunderstandings
“chmod after chown” gotcha. A frequent shell-scripting bug: chmod u+s file; chown root file. Because chown strips setuid, the binary ends up not setuid. The correct order is chown root file; chmod u+s file — set ownership first, then the special bit. This is a direct consequence of chown_common’s unconditional ATTR_KILL_SUID and bites people who assume the two operations are commutative.
“Why can’t I chgrp my own file to group X?” Because you’re not a member of X. chgrp_ok’s vfsgid_in_group_p check scans your current credential’s group set; if X isn’t in it, you get -EPERM even though you own the file. The fix is to be added to the group (and re-login or newgrp to pick it up in your credentials), or to have a CAP_CHOWN-holding process do it.
-1 confusion. Passing chown(path, -1, gid) is the idiomatic “change group only” call (the kernel skips the UID). But in shell, chown :group file or chgrp group file is the portable way; raw -1 is a C-API detail.
Ownership ≠ access. Owning a file does not automatically let you read or write it — the mode bits still apply, and an owner can chmod 000 their own file and lock themselves out (then chmod it back, since chmod permission keys off ownership via inode_owner_or_capable, not the current mode). Ownership grants the right to change permissions and group, not blanket access.
Quota and accountability. The whole reason owner-change is privileged is disk quota and accountability: if any user could chown a 10 GB file onto another user, they could exhaust that user’s quota or evade their own. CAP_CHOWN gating is the enforcement point.
Uncertain
Verify: that on the write path
file_remove_privs()/setattr_should_drop_suidgid()is the exact function chain in v6.12 and thatCAP_FSETIDsuppresses stripping on write identically to chown. Reason: the write path was confirmed againstfs/inode.c(dentry_needs_remove_privs,file_remove_privs) andfs/attr.c(setattr_should_drop_suidgid), but the full call sequence fromvfs_write→file_remove_privswas not traced line by line in this task. To resolve: readfile_remove_privs_flags()and its caller inmm/filemap.c/fs/read_write.cfor v6.12. uncertain
Alternatives and Adjacent Mechanisms
chown operates on the discretionary owner/group identity. Where you need finer-grained “this specific user may write, that one may only read” without a shared group, Linux offers POSIX Access Control Lists (see Access Control Lists in Linux) layered on top of the same inode — ACLs do not change ownership but add per-principal entries. Where the policy must be mandatory (not at the owner’s discretion), the SELinux/AppArmor LSMs label files independently of uid/gid. And the namespace-relative side of ownership — making a file owned by UID 1000 inside a container map to UID 100000 on the host — is idmapped mounts and user namespaces, which reinterpret the same i_uid/i_gid rather than changing them; see Process Credentials and struct cred for how the credential side composes.
Production Notes
The “chown strips setuid” behavior is the single most common real-world source of “my setuid binary stopped working after deployment” tickets — CI pipelines and container image builds that chown files into place after chmod silently drop the bit. The robust fix in Dockerfiles and provisioning scripts is to apply mode bits last, or to use install -m 4755 -o root which sets owner and mode in one atomic operation that ends with the bit set. Configuration-management tools (Ansible’s file/copy modules with both owner/group and mode) sidestep the ordering by computing the desired final state and applying it, but hand-rolled scripts routinely get the order wrong.
The setgid-directory pattern (chmod g+s /srv/shared; chgrp devs /srv/shared) is the canonical way to make a shared workspace where all created files land in the devs group — but it is a frequent surprise that it does not fix the permission bits, only the group; you still need a permissive umask (e.g. 002) or default ACLs for group members to actually write each other’s files. The two mechanisms (group inheritance vs. permission defaults) are independent and both required.
See Also
- File Permissions and the Mode Bits — the nine permission bits and how the owner/group/other triads are selected from the same
i_uid/i_gidthis note governs - setuid setgid and the Sticky Bit — sibling; the special bits whose stripping on chown is described here, and the setgid-directory inheritance in depth
- Discretionary Access Control — parent concept; owner-decides-policy, of which
chownis the policy-setting verb - The umask and Default Permissions — what mode a new file gets, complementing what group it gets here
- Process Credentials and struct cred — the
current_fsuid()/group-set the gates check against - Access Control Lists in Linux — finer-grained per-principal alternative to group-based sharing
- POSIX Capabilities / File Capabilities and Ambient Capabilities —
CAP_CHOWNandCAP_FSETIDcome from here - Linux Security MOC — parent map (section A, the DAC foundation)