Root Filesystem File Permissions
Free Embedded Linux Course — Chapter 5: Building a Root Filesystem
the files sitting in a staging directory on the host are owned by a regular user, but the
target device expects root to own almost everything, with a handful of files carrying
special permission bits that grant temporary privilege. Get this wrong and you either lock
yourself out of basic device functions, or you leave a security hole wide open. This lecture
of our free embedded Linux course walks through Linux’s permission model
from first principles and shows exactly how to apply it correctly to a staging root
filesystem, which is one of the most practical skills covered in any
free linux device drivers course or systems-level curriculum.
free linux kernel development course
free linux device drivers course
POSIX permissions
SUID SGID sticky bit
What You Will Learn
- How Linux represents users, groups, and the root account internally
- The nine permission bits and how they map onto owner, group, and world
- What the SUID, SGID, and sticky bits actually change at the kernel level
- Why a staging root filesystem needs its ownership rewritten before deployment
- A safe, original walkthrough of setting these bits with
chmodandchown - Common mistakes that turn a permission fix into a security incident
Prerequisites
This lecture assumes you’ve already built a staging root filesystem directory (covered in the
earlier lectures of this free embedded linux course chapter) and are
comfortable with basic shell commands like ls and cd. No prior
security background is required — we build the concept up from scratch.
Users, Groups, and Root: The Kernel’s View
The Linux kernel doesn’t understand usernames at all. Every running process carries a 32-bit
integer called a UID (user ID), and one or more GIDs (group
IDs). The mapping from those numbers to human-readable names — alice,
root, daemon — lives entirely in userspace, in
/etc/passwd and /etc/group. The kernel itself only ever compares
numbers.
UID 0 is special by convention, not by magic: the kernel grants the process with UID 0 (root)
a bypass on almost every permission check. This is why embedded Linux security work is, in
practice, mostly about controlling who and what can become UID 0, even briefly.
The Nine Permission Bits
Every file and directory has exactly one owning UID and one owning GID. Access is controlled
by nine bits, split into three groups of three: what the owner can do, what the owning group
can do, and what everyone else can do. Each group of three covers read, write, and execute.
owner | group | world
4 2 1| 4 2 1| 4 2 1
Because three bits fit perfectly into one octal digit, permissions are almost always written
as three octal digits — for example 754 means owner=rwx (7), group=r-x (5),
world=r– (4).
SUID, SGID, and Sticky: The Fourth Digit
Beyond the standard nine bits, Linux keeps three additional special-purpose bits, usually
written as a fourth leading octal digit:
2000 SGID executable runs with the FILE’s GROUP, not the caller’s
1000 STICKY in a directory: only the file’s own owner may delete it
SUID is the one embedded developers hit most often. Normally a process runs
with the UID of whoever launched it. If the executable file has the SUID bit set, the kernel
instead runs it with the UID of the file’s owner, for the duration of that process
only. This is how ordinary users are able to run privileged tools — the executable briefly
“becomes” root, performs one privileged operation such as opening a raw network socket or
changing the system clock, and the privilege disappears again when the process exits.
SGID works the same way but for group membership, and it has a second use on
directories: any new file created inside an SGID directory automatically inherits the
directory’s group, which is handy for shared project folders.
Sticky only matters on directories with world-writable permissions. Without
it, anyone who can write to a shared directory can delete anyone else’s files in it. With the
sticky bit set, only the file’s own owner (or root) may remove it — this is exactly why
/tmp ships with the sticky bit set on every distribution.
Hands-On: Setting SUID on a Custom Tool
Let’s build a tiny original program, ep_privcheck, that simply prints the real
UID and the effective UID of the process it runs as. Compiling and running it normally, then
with SUID set, makes the effect completely visible.
$ cat > ep_privcheck.c << 'EOF'
#include <stdio.h>
#include <unistd.h>
int main(void)
{
printf("real UID = %d, effective UID = %d\n", getuid(), geteuid());
return 0;
}
EOF
$ gcc -o ep_privcheck ep_privcheck.c
Run it as a normal user first — both UIDs match:
$ ./ep_privcheck
real UID = 1000, effective UID = 1000
Now make root the owner and set the SUID bit (4755 = SUID + rwxr-xr-x), then run it again as
the same normal user:
$ sudo chown root:root ep_privcheck
$ sudo chmod 4755 ep_privcheck
$ ls -l ep_privcheck
-rwsr-xr-x 1 root root 16824 Aug 13 10:02 ep_privcheck
$ ./ep_privcheck
real UID = 1000, effective UID = 0
Note the lowercase s in place of the owner’s execute bit in the ls -l
output — that’s the visible marker that SUID is active. The real UID still reflects
who launched the program, but the effective UID — the one the kernel checks for
permission decisions — is now 0. This is precisely the mechanism real system tools like
ping and passwd rely on to perform one privileged action on behalf
of an unprivileged user, then hand control back safely.
Fixing Ownership in a Staging Root Filesystem
While you’re populating a root filesystem on the build host, every file you create is owned
by your own login UID, not root. Devices, however, expect most files to be root-owned. The fix
is a single recursive chown pass just before packaging the image:
$ cd ~/ep_rootfs
$ sudo chown -R root:root .
| Approach | Pros | Cons |
|---|---|---|
sudo chown -R on the staging tree directly |
Simple, one command | Staging directory now needs root to modify further — easy to slip into developing as root |
| Ownership fix applied only at image-packaging time (e.g. inside a build tool’s fakeroot step) | Development stays unprivileged the whole time | Requires a packaging tool that supports it, such as Buildroot or Yocto’s fakeroot environment |
Modern build systems like Buildroot and Yocto solve this properly with fakeroot or
equivalent wrappers, which intercept ownership and permission syscalls during packaging without
ever requiring your build user to actually become root. If you’re assembling a root filesystem
by hand for learning purposes, the direct chown -R approach above is fine — just
be deliberate about when you run it.
Real-World Use Case: Locking Down /dev/mem
Device nodes are a common place where sloppy permissions turn into real vulnerabilities. A
node like the memory-access device should never be world-readable or world-writable, because
that would let any unprivileged process on the device read or overwrite arbitrary physical
memory. The correct treatment is root ownership with a restrictive mode:
$ sudo chown root:root dev/mem
$ sudo chmod 600 dev/mem
$ ls -l dev/mem
crw------- 1 root root 1, 1 Aug 13 10:05 dev/mem
Mode 600 denies read and write to everyone except the owning root account, which is exactly
the posture you want for any sensitive device node shipped in a production image.
Common Mistakes and Troubleshooting
- Developing as root after a chown -R. Once your staging tree is root-owned, it’s tempting to just keep using
sudofor every edit. This erodes the safety net that unprivileged development gives you — prefer fixing ownership only at packaging time. - Setting SUID on scripts. The Linux kernel deliberately ignores the SUID bit on interpreted scripts (shell scripts, Python, etc.) for security reasons. SUID only has effect on compiled binaries.
- Forgetting the sticky bit on shared writable directories. Any directory you make world-writable on a target device should almost always also get the sticky bit, mirroring how
/tmpis configured upstream. - Confusing real UID and effective UID when debugging permission-related bugs — always check both, as the demo above shows.
Best Practices
- Grant SUID only to the smallest possible binary that performs the privileged operation, never to a large multi-purpose tool.
- Audit every SUID/SGID binary that ships in your root filesystem image — each one is a potential privilege-escalation target.
- Prefer Linux capabilities (a finer-grained privilege model, covered later in this course) over SUID root where your kernel and userspace support it.
- Automate the ownership fix as a scripted, auditable step in your build pipeline rather than a manual command run by memory.
Security Considerations
Every SUID-root binary in your image is a piece of code that, if it has a bug, can be tricked
into doing something as root that an attacker could never do directly. Minimizing the count of
such binaries, and keeping them small and well-audited, is one of the highest-value security
practices available when building an embedded root filesystem.
Summary and Key Takeaways
- The kernel checks numeric UIDs and GIDs, not names — mappings live in userspace.
- Nine standard bits control owner/group/world read, write, and execute access.
- SUID and SGID temporarily change the effective UID/GID of a running process; sticky protects files in shared writable directories.
- Staging root filesystems need an explicit ownership pass before deployment, ideally via a fakeroot-style build step.
- Sensitive device nodes like memory-access devices must be tightly restricted to root only.
Conclusion
Getting file permissions right in a root filesystem isn’t a checkbox exercise — it’s one of
the core security boundaries of the entire device. Understanding what SUID, SGID, and sticky
actually do at the kernel level, rather than just copying commands from a reference, is what
separates developers who can debug a permissions problem at 2 AM from those who can’t. This
concept shows up throughout embedded Linux work, which is why it gets dedicated coverage in
this free linux kernel development course.
FAQ
Why does the kernel use numeric UIDs instead of usernames?
Numeric comparisons are fast and don’t depend on any userspace database being available, which matters during early boot before /etc/passwd is even mounted read-write in some configurations.
Does SUID work on shell scripts?
No. Modern Linux kernels ignore the SUID bit on scripts with a shebang line, specifically to prevent a well-known class of race-condition exploits. Use a small compiled wrapper instead.
What’s the difference between real UID and effective UID?
Real UID identifies who actually launched the process; effective UID is what the kernel checks for permission decisions. SUID makes these two values diverge temporarily.
Should I ever set SUID root on my own custom embedded tools?
Only if there’s no alternative — prefer Linux capabilities or a privileged helper daemon reached over a restricted socket, which limits the attack surface far more than a full SUID-root binary.
Why does /tmp have the sticky bit set?
Because it’s world-writable, so without the sticky bit any user could delete any other user’s temporary files. The sticky bit restricts deletion to each file’s own owner.
Is chown -R root:root safe to run on my whole staging directory?
Functionally yes, but it means you’ll need sudo for further edits in that directory. Many teams instead defer the ownership fix to a fakeroot-based packaging step so development stays unprivileged.
How do I find every SUID binary already in my root filesystem?
Run find / -perm -4000 -type f on the target (or against your staging tree) to list every file with the SUID bit set, then review each one.
Continue the Free Embedded Linux Course
Next up: populating the root filesystem with init, a shell, and BusyBox.
