Learn › Bash Redirection and File Descriptors › Concurrency, permissions, and special files

Chapter 13. Concurrency, permissions and special files

Concurrency, permissions and special files. Try the examples, inspect their output, and explain the result. Practice feedback does not award XP or mastery.

When another writer arrives

Two jobs append records to the same log. Each record has a prefix and a suffix, written separately. The jobs both succeed, yet a reader cannot tell which suffix belongs to which prefix.

This Linux lesson distinguishes a filename, an open permission, a successful write, and a complete application record. You will use a private directory, Bash, GNU coreutils, Python 3, and the util-linux flock utility. No example changes system permissions or uses real shared data.

The special paths /dev/null and /dev/full are Linux devices. The former discards writes; the latter provides a controlled write failure. Neither experiment fills a disk. All ordinary files and named pipes stay inside your private directory.

In your managed Bash practice terminal

Run camp-redirection 13 to enter this chapter's private directory. While this lab machine is running, the same command returns to it after reconnecting; camp-redirection 13 --fresh creates a new one without deleting earlier files. Run the code blocks there and use cd to leave.

An append is not a record transaction

Opening with >> requests append behavior. On a normal local Linux filesystem, each write chooses the current end of the file. This prevents a separate seek-to-end race for that write. It does not combine several writes into one indivisible record. Filesystem implementations, particularly network filesystems, may add further limits.

Job A intends A:one and job B intends B:two. Predict the two lines produced by the deliberately chosen order of four writes below. This is one possible interleaving, with no scheduler timing to guess.

: > interleaved.log
printf 'A:' >> interleaved.log
printf 'B:' >> interleaved.log
printf 'one\n' >> interleaved.log
printf 'two\n' >> interleaved.log
cat interleaved.log

Expected output

A:B:one
two

This is a controlled schedule, not a claim that the kernel must choose this order. Job A owns A: and one; job B owns B: and two. Every write appended, but the logical records were damaged.

Do not apply the pipe-specific PIPE_BUF guarantee to regular file records. An application may split one print operation into multiple writes. Agree on a serialization mechanism when the complete record must stay together.

Put the whole record inside a lock

A lock coordinates participants that follow the same protocol. Here each writer obtains an exclusive lock before writing either piece. The Linux flock utility operates on an open descriptor and releases the lock when the relevant open references close.

(
: > records.log
(
  flock -x 9 || exit 1
  printf 'A:' >> records.log
  printf 'one\n' >> records.log
) 9> records.lock &
first=$!
(
  flock -x 9 || exit 1
  printf 'B:' >> records.log
  printf 'two\n' >> records.log
) 9> records.lock &
second=$!
if wait "$first"; then first_status=0; else first_status=$?; fi
if wait "$second"; then second_status=0; else second_status=$?; fi
printf 'writer statuses: %s %s\n' "$first_status" "$second_status"
if (( first_status != 0 || second_status != 0 )); then
  printf 'At least one record was not completed. Inspect records.log before reuse.\n' >&2
  exit 1
fi
sort records.log
)

The first line reports writer statuses: 0 0; the sorted records are A:one and B:two, each on its own line. The outer subshell owns both background jobs and waits for both even if one fails. A failed writer is reported only after its peer finishes; it cannot leave a second owned job uncollected. This private fresh-directory example has only these two short writers. A competing holder in a larger system calls for a separate wait bound. Sorting makes verification independent of which job acquires the lock first. Both pieces of each record are inside one lock lifetime. A writer that ignores the lock can still interfere. Keep the lock file itself stable; deleting and recreating it can divide writers between different objects. util-linux flock documentation.

Distinguish an open failure from a write failure

To open a file, the process needs the relevant path traversal and access permissions. Creating or replacing a pathname also involves its containing directory. A file's mode alone does not answer every access question. Privileged users and access control lists can change the result.

(
  if exec 3> /dev/full; then
    printf 'open succeeded\n'
  else
    exit 1
  fi
  if { printf 'report\n' >&3; } 2> write-failure.log; then
    printf 'unexpected write success\n'
  else
    printf 'write failed\n'
  fi
  exec 3>&-
)

The output is open succeeded, then write failed. Linux reports ENOSPC, meaning no space left on the device. The Bash write diagnostic is captured separately because its prefix varies with invocation.

Try in the practice terminal: confirm that the failed write recorded a diagnostic

test -s write-failure.log && printf 'write diagnostic recorded\n'

Expected result

write diagnostic recorded

This probe catches the failure to display its stage. A report program must return a failure status to its caller. A successful open is not proof that later writes, buffered flushes, or closing output will succeed. Actual storage can fail after work has already started.

Keep links and permissions inside the threat model

A symbolic link is a pathname that refers to another pathname. Ordinary shell output redirection usually follows it. A predictable temporary name in a shared directory can therefore target a different file than you intended.

mkdir link-demo
printf 'original\n' > link-demo/owned.txt
ln -s owned.txt link-demo/result.txt
printf 'replacement\n' > link-demo/result.txt
cat link-demo/owned.txt

The output is replacement. Every path here belongs to the practice directory. The demonstration needs no attacker and damages no real document.

Use a private directory and exclusive temporary creation. Checking whether a path is a link and then opening it leaves a race if someone can change the directory between those operations. Reliable hostile-directory handling needs stronger operating-system interfaces than this exercise provides.

For an actual permission failure, work as an ordinary user. Making a private file read-only is not a reliable failure test when the shell has elevated privileges. The capstone instead supplies deterministic source, validation, and output-device failures.

A FIFO needs both ends and a deadline

A FIFO, also called a named pipe, has a pathname but carries a stream between processes. Opening a blocking FIFO for writing normally waits for a reader, and vice versa. Starting both sides sequentially in one foreground shell can therefore stall before any bytes move.

mkfifo lesson.fifo
timeout 3 bash -c 'cat < lesson.fifo' > fifo.out &
reader=$!
timeout 3 bash -c 'printf "parcel\n" > lesson.fifo'
writer_status=$?
wait "$reader"
reader_status=$?
rm -- lesson.fifo
printf 'writer=%s reader=%s\n' "$writer_status" "$reader_status"
cat fifo.out

Expected output is writer=0 reader=0, then parcel. Both opens have an independently scheduled peer and a deadline. A timeout is a diagnosis signal, not permission to wait forever. Closing every writer lets the reader see end of file.

Repair a record, then test its boundaries

Your independent task is to extend each writer to three fragments and keep each record intact. Place all three writes under the same lock. Run two writers, wait for both, and inspect the complete records rather than assuming a particular order.

The worked reasoning is: the descriptor for records.lock identifies the shared lock object; flock acquires it; all record writes occur before that descriptor closes. Locking each fragment separately leaves the original defect. A lock around the final inspection cannot repair earlier mixed writes.

One more operator deserves care: <> opens a file for both reading and writing without ordinary > truncation. Its shared offset still advances. It does not mean append or “edit safely.”

Check the read character and final file: reading one byte advances the shared position, so X replaces the next byte.

(
  printf 'abc\n' > readwrite.txt
  exec 3<> readwrite.txt
  IFS= read -r -N 1 character <&3
  printf X >&3
  exec 3>&-
  printf 'read=%s\n' "$character"
  cat readwrite.txt
)

Expected output

read=a
aXc

Ready to practice

Explain why >> alone cannot protect a multi-write record, why an open can succeed before a write fails, and why opening a FIFO resembles neither reading a stored file nor opening /dev/null.

Use the private fixtures to test your repair. If a FIFO attempt reaches its deadline, collect both statuses and remove only the FIFO you created. Do not retry with an unbounded wait. In the next lesson, track descriptors across terminal, child-process, and remote-shell boundaries.

Practice Chapter 13. Concurrency, permissions and special files in a real Linux terminal at The Linux Camp. Progress is verified automatically as you type commands on the machine.