Linux permissions — practice with a group project folder, three users and a permission checker (and the teammate who couldn’t build)
First week with the shared folder for the IC2 project. I had set it up with all the care in the world: the ic2 group could get in, read the code and run the build script; everyone else, nothing. Ten minutes later, a message from Laura: *”It won’t let me build. I get something about ld: cannot open output file“. Me, absolutely sure: *”Impossible, the script has the group x”. I was right… and it didn’t help at all. The script ran perfectly; what it couldn’t do was create the compiled program, because that means writing in the folder, and I had taken the group’s w away from the folder. A lesson that repeats throughout the practice: every action needs one specific permission in one specific place, and it’s hardly ever the place you look first.
This practice puts everything from the Linux permissions theory to work with three medium-level programs (here, three terminal sessions and a script):
- Setting up the group folder with
chgrpandchmodin octal. - Testing it with three users (Laura, in the group; Pablo, from outside; and me, the owner) and fixing what fails with symbolic
chmod. - A permission checker in bash that warns about what “others” can touch, plus a
umaskfor whatever gets created later.
The data look like real data on purpose: a private file that sneaks into the shared folder, a slip that leaves the folder open to everyone and a teammate who can’t do her part.
Table of Contents
Before you start
It helps to be clear on:
- How to read an
ls -lline: type and threerwxtriplets (owner, group, others). - The octal weights:
r= 4,w= 2,x= 1. - Symbolic mode: for whom (
u,g,o,a), which operation (+,-,=) and which permissions. - That on a folder,
xmeans “get in” andwmeans “create and delete inside”.
If any of these feels shaky, go over the theory before carrying on.
To reproduce the whole practice you need several users. On your own Linux (or on the lab virtual machine) you can create them with sudo adduser laura, sudo adduser pablo and sudo groupadd ic2, and add the first two to the group with sudo usermod -aG ic2 sergio and sudo usermod -aG ic2 laura (replace sergio with your user). Then open a terminal as each of them with su - laura. If you can’t use sudo, follow the practice by reading the outputs: they are the real ones.
Program 1: setting up the group folder
We need to prepare ic2-lab/ for a C project: a src/ folder with the code, a docs/ folder with the submission and some private notes of mine, and a build.sh script. We want the ic2 group to be able to get in, read the code and the submission, and build, my notes to be mine only and nobody from outside to see anything. What makes it tricky is that we have to decide the permissions of seven different things, and folders and files aren’t treated the same way.
The idea
Order matters:
- Create the layout (
mkdir -p,touch, the script with a heredoc). - Hand everything to the group with a single
chgrp -R: no need to go file by file. - Set permissions by kind: folders, all
750(merwx; the groupr-xto get in and list; others nothing); files the group reads,640; my notes,600; the script,750so the group can run it. - Check with
ls -lR, which also lists the contents of the subfolders.
We use octal because we want exact permissions, not to add or remove on top of whatever was there.
Before you code, think of each folder as a door: the group needs x on every folder along the way to reach a file. If ic2-lab/ doesn’t have the group’s x, it doesn’t matter what you set on docs/submission.txt.
The code
# 1. The project layout: code in src/, documents in docs/
mkdir -p ic2-lab/src ic2-lab/docs
cat > ic2-lab/src/main.c << 'EOF'
int main(void) { return 0; }
EOF
touch ic2-lab/docs/submission.txt ic2-lab/docs/private-notes.txt
cat > ic2-lab/build.sh << 'EOF'
#!/bin/bash
gcc -Wall -std=c11 src/main.c -o main && echo "OK"
EOF
# 2. The whole tree goes to the project group (-R = recursive)
chgrp -R ic2 ic2-lab
# 3. Folders: me everything, the group gets in and lists, others stay out
chmod 750 ic2-lab ic2-lab/src ic2-lab/docs
# 4. Files: the group reads the code and the submission; my notes, only me
chmod 640 ic2-lab/src/main.c ic2-lab/docs/submission.txt
chmod 600 ic2-lab/docs/private-notes.txt
chmod 750 ic2-lab/build.sh # the group can run it too
# 5. Check: -R also lists what is inside each folder
ls -lR ic2-lab
The meatiest part is the four chmods at the end. Look at them on their own:
# 3. Folders: me everything, the group gets in and lists, others stay out chmod 750 ic2-lab ic2-lab/src ic2-lab/docs # 4. Files: the group reads the code and the submission; my notes, only me chmod 640 ic2-lab/src/main.c ic2-lab/docs/submission.txt chmod 600 ic2-lab/docs/private-notes.txt chmod 750 ic2-lab/build.sh # the group can run it too
Each chmod accepts several files at once, so we group them by permissions: one line for all the 750 folders, another for the 640s… It’s shorter and, above all, easier to review: if tomorrow you decide the submission should be 660, you know which line to touch. The script gets its own line because, even though it shares the number with the folders, the reason is different (on a file, x means “run”, not “get in”).
Output
$ mkdir -p ic2-lab/src ic2-lab/docs
$ cat > ic2-lab/src/main.c << 'EOF'
> int main(void) { return 0; }
> EOF
$ touch ic2-lab/docs/submission.txt ic2-lab/docs/private-notes.txt
$ cat > ic2-lab/build.sh << 'EOF'
> #!/bin/bash
> gcc -Wall -std=c11 src/main.c -o main && echo "OK"
> EOF
$ chgrp -R ic2 ic2-lab
$ chmod 750 ic2-lab ic2-lab/src ic2-lab/docs
$ chmod 640 ic2-lab/src/main.c ic2-lab/docs/submission.txt
$ chmod 600 ic2-lab/docs/private-notes.txt
$ chmod 750 ic2-lab/build.sh
$ ls -lR ic2-lab
ic2-lab:
total 12
-rwxr-x--- 1 sergio ic2 63 Oct 25 16:49 build.sh
drwxr-x--- 2 sergio ic2 4096 Oct 25 16:49 docs
drwxr-x--- 2 sergio ic2 4096 Oct 25 16:49 src
ic2-lab/docs:
total 0
-rw------- 1 sergio ic2 0 Oct 25 16:49 private-notes.txt
-rw-r----- 1 sergio ic2 0 Oct 25 16:49 submission.txt
ic2-lab/src:
total 4
-rw-r----- 1 sergio ic2 29 Oct 25 16:49 main.c
Let’s check what matters by hand:
build.sh:-rwxr-x---= 7 5 0. I read, change and run it; the group reads and runs it; others, nothing.docsandsrc:drwxr-x---: the group can get in (x) and list (r), but can’t create anything inside (now).private-notes.txt:-rw-------= 6 0 0, just me.submission.txtandmain.c:-rw-r-----= 6 4 0: the group reads but doesn’t write.- The group column says
ic2on every line thanks tochgrp -R.
The root folder doesn’t show up in its own listing, but we set it to 750 with the same chmod. The drawing sums up the whole tree:
This is how the permissions of ic2-lab/ change throughout the program:
| Step | Command | Permissions of ic2-lab/ | Group |
|---|---|---|---|
| 1 | mkdir -p … | 755 rwxr-xr-x (umask 022) | sergio |
| 2 | chgrp -R ic2 ic2-lab | 755 rwxr-xr-x | ic2 |
| 3 | chmod 750 ic2-lab … | 750 rwxr-x--- | ic2 |
Notice that, as it stands, the group can’t write anywhere. Is that what we want? Program 2 puts it to the test.
Questions you might ask
Why chgrp -R before the chmods? The order doesn’t actually change the result, but doing it first avoids a window in which the folder already has its group permissions open while still belonging to the sergio group. It’s a good habit: first decide who, then what.
Why are the folders 750 and not 740? Because a folder with r and no x lets you see the names but not get in or open anything (you saw it in the theory). We’d be giving the group a useless list of names.
Why not use chmod -R 750 ic2-lab and be done sooner? Because -R applies the same number to everything: main.c and submission.txt would end up with x (as if they were programs) and my private notes would be readable by the group. Octal with -R only makes sense when everything should end up the same.
What if Laura creates a file inside? It would be born with her primary group (laura), not ic2. We fix that with the setgid bit in level 3 of the exercises.
Does main.c need x to be compiled? No. gcc only needs to read the .c; the x is needed by the program it generates (and gcc creates that one with 755 automatically, because the linker does ask for it).
Setting the permissions on the file and forgetting the folders along the way. If docs/ didn’t have the group’s x, Laura couldn’t read submission.txt even if the file were 644.
Try it yourself
- Change the program so my notes live outside the shared folder (for example, in
~/private/) and remove thechmod 600line. Which folder needs protecting now? - Add a
final-submissions/folder the group can list but not change. What number do you give it? (Hint: the same asdocs/.)
Program 2: testing it with three users
Setting things up is nice, but permissions aren’t checked by looking at them: they’re checked by using them. In this program, Laura (group ic2) and Pablo (from outside) try to do real things with the folder: list, read, build and write to the submission. Then I fix just what’s needed and Laura tries again. We start from the folder exactly as program 1 left it.
The idea
For each attempt we have to answer two questions: which class applies? (Laura is in the group, Pablo is “others”) and which permission is needed, and where? (on the file or on the folder). Then, when something fails, we fix it with symbolic chmod: we don’t want to recalculate the whole number, just add a group w in two places.
Before looking at the output, write down on paper what you think will happen with each command. It’s the best exercise in this topic.
The code
Each # @user line means that from there on the commands are typed by that person in their own terminal:
# @laura — Laura (group ic2) tests the folder from her terminal ls ic2-lab/docs cat ic2-lab/docs/private-notes.txt cd ic2-lab ./build.sh echo "Laura's part: exercise 2" >> docs/submission.txt # @pablo — Pablo (another class, not in ic2) has a look around ls ic2-lab cat ic2-lab/docs/submission.txt # @sergio — Sergio opens just enough: writing in the folder and in the submission chmod g+w ic2-lab ic2-lab/docs/submission.txt stat -c '%a %A %n' ic2-lab ic2-lab/docs/submission.txt # @laura — Laura tries again ./build.sh echo "Laura's part: exercise 2" >> docs/submission.txt cat docs/submission.txt
Output
laura$ ls ic2-lab/docs private-notes.txt submission.txt laura$ cat ic2-lab/docs/private-notes.txt cat: ic2-lab/docs/private-notes.txt: Permission denied laura$ cd ic2-lab laura$ ./build.sh /usr/bin/ld: cannot open output file main: Permission denied collect2: error: ld returned 1 exit status laura$ echo "Laura's part: exercise 2" >> docs/submission.txt bash: docs/submission.txt: Permission denied pablo$ ls ic2-lab ls: cannot open directory 'ic2-lab': Permission denied pablo$ cat ic2-lab/docs/submission.txt cat: ic2-lab/docs/submission.txt: Permission denied sergio$ chmod g+w ic2-lab ic2-lab/docs/submission.txt sergio$ stat -c '%a %A %n' ic2-lab ic2-lab/docs/submission.txt 770 drwxrwx--- ic2-lab 660 -rw-rw---- ic2-lab/docs/submission.txt laura$ ./build.sh OK laura$ echo "Laura's part: exercise 2" >> docs/submission.txt laura$ cat docs/submission.txt Laura's part: exercise 2
Let’s go attempt by attempt:
laura$ ls ic2-lab/docsworks: Laura is in the group, and bothic2-lab/anddocs/haver-xfor the group. She sees both names.cat … private-notes.txt:Permission denied. The group triplet is---(600). Just what we wanted../build.sh: the script does start (it hasr-xfor the group), but it fails inside:ld: cannot open output file main: Permission denied. The linker wants to createmaininic2-lab/, and creating a file means writing in the folder, for which the group has now. This is the story from the beginning.echo … >> docs/submission.txt:Permission denied, because the group only hasr--onsubmission.txt.pablo$ ls ic2-lab:cannot open directory. Pablo is “others”, and others have---on the folder.pablo$ cat ic2-lab/docs/submission.txt: it fails too, and it doesn’t even get to look at the permissions ofsubmission.txt: withoutxonic2-lab/he can’t get through the first door.
Then, a single command from me: chmod g+w ic2-lab ic2-lab/docs/submission.txt. The stat confirms 770 on the folder (7 7 0: 5 + 2 = 7 for the group) and 660 on the submission (4 + 2 = 6). Laura tries again and now she gets OK when building, her line is added to the submission and cat shows it.
The drawing puts every attempt together with the permission that decides it:
This is the trace of Laura’s attempts, with the class and the triplet Linux looks at:
| Attempt | Class | Where it looks | Triplet | Needs | Result |
|---|---|---|---|---|---|
ls docs | group | docs/ | r-x | r and x | Yes |
cat private-notes.txt | group | the file | --- | r | No |
./build.sh (create main) | group | ic2-lab/ | r-x | w and x | No |
>> docs/submission.txt | group | the file | r-- | w | No |
./build.sh after g+w | group | ic2-lab/ | rwx | w and x | Yes |
>> docs/submission.txt after g+w | group | the file | rw- | w | Yes |
Questions you might ask
Why does the script run if it fails afterwards? Because they are two different permissions in two different places. Running build.sh checks the file’s x; creating main checks the folder’s w. The first Permission denied you see isn’t always about the first step.
Why g+w and not chmod 770? For the folder it would be the same, but submission.txt goes in the same command, and it needs 660. With symbolic mode, a single command adds the group’s w to both things while respecting what they already had.
Now that the group writes in ic2-lab/, can Laura delete my build.sh? Yes. Even though she can’t change it (the group has r-x on the script), deleting means removing a name from the folder, and she now has w on that folder. That’s the price of opening it up, and the sticky bit fixes it (level 3 of the exercises). My notes are still safe because they’re in docs/, which doesn’t have the group’s w.
Why can’t Pablo even see the names? Because ls needs the folder’s r to read its list, and others have ---.
Fixing a Permission denied by giving permissions to the file named in the message without thinking about which action failed. Laura’s error mentioned main, a file that didn’t even exist: the missing permission was the folder’s.
Try it yourself
- After the fix, have Laura try
rm docs/private-notes.txt. What do you think happens? Does she needwondocs/? - Keep the submission at
660but remove the group’swfromdocs/. Can Laura still append lines with>>? And createdocs/laura-part.txt?
Program 3: a permission checker and the group’s umask
With several users touching the folder, sooner or later someone leaves something open. This program has two parts: a script, check.sh, that walks through the folder and warns about everything “others” can touch, and a session that causes two slips, detects them, fixes them and sets a umask so new things are born right.
The idea
To know whether “others” can do anything with a file, it’s enough to look at the last octal digit of its permissions: if it’s 0, others can’t do anything; otherwise, there’s a warning. stat -c '%a' gives us the number, and bash can keep just its last character. We walk through everything with find, which lists a folder and everything inside it.
Before you code, think about order: find doesn’t promise any. If the report has to come out the same every time (so you can compare it from one day to the next), you have to sort.
The code
First, the checker script:
#!/bin/bash
# Checks a folder and warns about everything "others" can touch.
folder=${1:-.} # the folder passed as an argument (or the current one)
problems=0
# sort: find guarantees no order; sorted, the report always comes out the same
for f in $(find "$folder" | sort); do
mode=$(stat -c '%a' "$f") # permissions in octal, for example 640
others=${mode: -1} # the LAST digit is the others one (also works with 4 digits)
if [ "$others" -ne 0 ]; then # 0 = others can do nothing; any other value is a warning
echo "WARNING $mode $f"
problems=$((problems + 1))
fi
done
echo "Problems: $problems"
The key line is the one that keeps the others digit:
mode=$(stat -c '%a' "$f") # permissions in octal, for example 640
others=${mode: -1} # the LAST digit is the others one (also works with 4 digits)
${mode: -1} is bash syntax for “the last character of the variable” (the space before -1 is required; without it, bash reads it as something else). It works both with 640 and with a four-digit 2770, because the others digit is always the last one. And [ "$others" -ne 0 ] compares numbers (-ne = not equal).
And the session that uses it (it starts from the program 1 folder):
chmod u+x check.sh # the script arrives without x: I give it to its owner chmod 644 ic2-lab/docs/private-notes.txt # a slip: someone left it readable by everyone chmod o+rx ic2-lab # and another: the folder open to anyone ./check.sh ic2-lab chmod -R o-rwx ic2-lab # -R: removes everything from others in the whole tree chmod 600 ic2-lab/docs/private-notes.txt ./check.sh ic2-lab umask 027 # for whatever we create from now on touch ic2-lab/docs/minutes.txt mkdir ic2-lab/tests stat -c '%a %A %n' ic2-lab/docs/minutes.txt ic2-lab/tests ./check.sh ic2-lab
Output
$ chmod u+x check.sh $ chmod 644 ic2-lab/docs/private-notes.txt $ chmod o+rx ic2-lab $ ./check.sh ic2-lab WARNING 755 ic2-lab WARNING 644 ic2-lab/docs/private-notes.txt Problems: 2 $ chmod -R o-rwx ic2-lab $ chmod 600 ic2-lab/docs/private-notes.txt $ ./check.sh ic2-lab Problems: 0 $ umask 027 $ touch ic2-lab/docs/minutes.txt $ mkdir ic2-lab/tests $ stat -c '%a %A %n' ic2-lab/docs/minutes.txt ic2-lab/tests 640 -rw-r----- ic2-lab/docs/minutes.txt 750 drwxr-x--- ic2-lab/tests $ ./check.sh ic2-lab Problems: 0
The first check finds exactly the two slips, and this is the trace of its loop:
| Round | f | mode | others | Warning? | problems |
|---|---|---|---|---|---|
| 1 | ic2-lab | 755 | 5 | yes | 1 |
| 2 | ic2-lab/build.sh | 750 | 0 | no | 1 |
| 3 | ic2-lab/docs | 750 | 0 | no | 1 |
| 4 | ic2-lab/docs/private-notes.txt | 644 | 4 | yes | 2 |
| 5 | ic2-lab/docs/submission.txt | 640 | 0 | no | 2 |
| 6 | ic2-lab/src | 750 | 0 | no | 2 |
| 7 | ic2-lab/src/main.c | 640 | 0 | no | 2 |
Seven entries checked, two warnings: 755 → others 5 (r-x) and 644 → others 4 (r--).
The fix uses two commands because there are two different problems. chmod -R o-rwx removes everything from others across the whole tree without touching the owner’s or the group’s permissions (with octal and -R it would have been impossible, because each thing has a different number). But that leaves the notes at 640, readable by the group, so I put them back to 600 separately. The second check gives Problems: 0.
Finally, umask 027: new files come out 640 (666 without ----w-rwx) and folders 750 (777 without the same). Checked: minutes.txt is 640 and tests/ is 750, and the third check is still at zero. The folder no longer depends on someone remembering to remove permissions by hand.
Questions you might ask
Why $(find … | sort) and not find … -exec? Because of the order: find returns entries in the order they’re stored on disk, which can change. With sort the report always comes out the same. (There’s a catch: names with spaces would be split; there aren’t any in this folder.)
Why does the script start without x? Because I copied it (or downloaded it) and it’s born with 644, like any new file. That’s why the session’s first command is chmod u+x check.sh: the same problem as the hook in the theory.
Why is the umask 027 and not 007? 027 also removes the group’s w from whatever is created: Laura will be able to read the new minutes, but not change them unless I decide so. With 007 they’d come out 660 and 770.
Does the umask fix files that already existed? No. It only acts at creation time. That’s why you need both: chmod -R for what was already there and the umask for what’s coming.
chmod -R 640 ic2-lab to “lock down” the folder. It removes the x from every subfolder (nobody can get in anymore, not even the group) and from the script (it no longer runs). To remove permissions from one class across a whole tree, use symbolic mode: chmod -R o-rwx.
find ic2-lab -perm /o=rwx does the same as the loop in a single line: it lists everything that has any permission for others. The script is longer, but it teaches you how to read a mode and lets you customise the warning.
Try it yourself
- Change the script so it also warns if a
.txtfile has thexset (hint: the owner digit is${mode: -3:1}). - Make the script, besides warning, count how many entries it has checked and print it at the end.
Common mistakes in this practice
- Only looking at the file: many
Permission deniederrors come from the folder (creating or deleting inside, or going through it withoutx). chmod -Rwith a number: it applies the same thing to folders and files; folders lose theirxor files get an extra one.- Forgetting the
chgrp: with thesergiogroup, group permissions are useless to Laura. - Testing as root or with
sudo: it skips permissions and makes you believe everything works. - Relying on the umask for what already exists: it only acts at creation time.
- Giving the group
won the folder and forgetting that lets them delete other people’s files: you need the sticky bit.
Try it in your terminal
There’s no Python Tutor for this topic, so you do the step by step. This walkthrough only needs your own user, and it checks something that surprises almost everyone: when you’re the owner, Linux only looks at the owner triplet, even if the group one would let you. Type the commands after each $ in a test folder:
$ touch test.txt $ chmod 070 test.txt $ id -gn sergio $ cat test.txt cat: test.txt: Permission denied $ chmod 670 test.txt $ cat test.txt $ stat -c '%a %A %n' test.txt 670 -rw-rwx--- test.txt
What to look at:
chmod 070leaves the file with everything for the group and nothing for the owner or others.id -gnconfirms your primary group is the same as the file’s (the onetouchgave it).cat test.txtfails even though you’re in the group withrwx: since you’re the owner, Linux only looks at the first triplet, and it’s---. It doesn’t “add up” the group permissions.- With
670the owner getsrw-back andcatworks. The finalstatshows-rw-rwx---.
Summary
- A project group’s folders go to
750(or770if the group must create things inside); files the group reads, to640; private stuff, to600. chgrp -R group folderis the first step to sharing: without it, group permissions help nobody.- For every
Permission denied, ask yourself which action fails and where the permission that decides it lives: on the file or on the folder. - Octal is for setting exact permissions; symbolic, for adding or removing without touching the rest (especially with
-R). - A checker as simple as looking at the last octal digit catches what others can touch.
- The umask (
027for a group) makes new things be born right;chmodfixes what already exists.
Checklist: have you practised it?
- Have you run the three programs and checked the outputs?
- Could you explain why Laura could run
build.shbut couldn’t build? - Do you know which number to give a folder so the group can get in but not create anything?
- Did you predict on paper the result of each attempt in program 2 before looking?
- Have you tried the “Try it yourself” ideas without looking at the theory?
- Do you know why
chmod -R o-rwxis safe andchmod -R 640isn’t?
If you answered yes to almost everything, you’re ready for the Linux permissions exercises, where I no longer hand you the commands. See you there!
Next step: exercisesLinux permissions — exercises with chmod, umask and a shared submissions folder (and the chmod -R that locked me out of my own folder)Did this article help you? Here is how you can support SergioLearns →




What is an operating system — theory with your laptop on a study afternoon, a write() call and a CPU that never rests (and the three programs that weren’t running at the same time)