Here’s what this program does, piece by piece, and how all the parts fit together.
What is it?
A tiny terminal “relay chat” that sends messages between unrelated processes on the same Linux machine by abusing POSIX advisory file locks (fcntl) as a signaling/transport layer. Instead of sockets/pipes/shared memory, it toggles byte-range locks on a shared file to encode bits and messages. The shared file chosen is the namespace handle at "/proc/self/ns/time" (an inode visible system-wide), opened read-only.
The high-level protocol
-
The file is conceptually divided into message blocks of size
0x1000bytes (4096). Each message occupies one block starting at offsets0, 0x1000, 0x2000, …. -
At the start of a block:
-
A short header lock (length
MSG_READY == 3) means “this block’s payload is ready”. -
A separate tracker lock of length 1 at
startis used by readers to mark which block they’re currently watching (and to release the previous one). -
The payload begins at
start + MSG_BEGIN_OFFSET(+4). Each byte of the message is encoded over 8 consecutive byte positions in the file: -
If a bit is
1, the sender sets a read lock (F_RDLCK) on that byte position. -
If a bit is
0, the sender leaves that byte position unlocked. -
The receiver scans the block and reconstructs each byte from which positions are locked. The message is a C-string: it ends when the receiver reconstructs a zero byte. The sender processes clear the block after a short TTL (5s): they unlock everything from
start + 1through the end of the block, leaving the 1-byte tracker lock atstartbehind. That “breadcrumb” helps readers advance to the next block, while the header+payload locks evaporate to make room for future traffic.
Key data structures & helpers
-
chat_statekeeps the receiver’s rolling state: -
buf[512]is where the decoded message lands. -
next_offsetis the next block boundary to watch. -
last_trackedremembers the previous block’s tracker. -
offset_trackeris just a flag (“have we set a tracker yet?”). -
Robust IO wrappers (
read_all,write_all) retry onEINTR, bail cleanly otherwise. -
Terminal helpers:
-
setup_input(1)switches stdin to noncanonical, no-echo, non-blocking mode so keystrokes arrive raw and immediately. -
setup_input(0)restores normal line input/echo on exit. -
Lock helpers:
-
set_lock(fd, off, len)andunset_lockplace/remove read locks for arbitrary byte ranges. -
query_lock(fd, &off, &len, &pid)asks the kernel, “if I tried a write lock here, who/what is in the way?” It returns the first conflicting lock (start, length, and owner PID). This is how the program discovers existing read locks (our “bits”).
Receiving messages
check_chatroom(fd, &chat)
-
Find next block: calls
query_lockstarting atchat->next_offset. If it sees any lock whose start aligns to a 4096 boundary, it treats that as a candidate message block. -
Set/rotate a tracker:
-
It sets a 1-byte read lock at the block start (
set_lock(fd, start, 1)). -
If it had a previous tracker, it unlocks that older 1-byte lock.
-
It advances
chat->next_offset += 0x1000. -
Wait for “ready”:
-
It polls (every 10ms) by calling
query_locknearstart + 1. Once the returned lock begins atstartand has length== MSG_READY(3), the payload is declared ready. If anything unexpected happens (misaligned locks, changing starts), it yells “interference on the wire” and exits. -
Decode the payload:
-
recv_str(fd, start + MSG_BEGIN_OFFSET, &chat)reads byte after byte: -
recv_bytelooks fromoffsettooffset + 7inclusive. It repeatedly callsquery_lockto pick up any lock segments, ORs the appropriate bits, and returns the reconstructed byte. -
Stops when a zero byte is seen (i.e., the next 8 positions have no locks set). If the message expired mid-read (the sender’s TTL has already cleared locks),
check_chatroomreturns 0 and the caller simply tries again on the next loop iteration.
Sending messages
send_msg(fd, hint, buf, len)
-
Forks a child so the parent keeps the UI snappy.
-
The child calls
claim_next_available_msg_block(fd, hint)to reserve a block: -
It scans blocks starting from
hint(usually the receiver’snext_offset) looking for no conflicting locks at the block start. -
When it finds a candidate, it temporarily locks 2 bytes at the start and runs a cross-process verification dance:
-
It spawns a helper child that calls
F_GETLKon that region to learn who owns the lock. -
If the lock’s
pidequals the parent’s PID, the reservation is confirmed; otherwise the parent unlocks and tries the next block. -
This double-check avoids subtle races where two processes think they reserved the same block.
-
With the block reserved, the child encodes the payload:
-
For each data byte,
send_bytesets 8 one-byte locks only where bits are1. -
Then it sets the header lock
set_lock(fd, start, MSG_READY)to signal “ready.” -
Sleeps 5 seconds (
MSG_TTL_SECONDS) and finally clears everything except the first byte (unset_lock(fd, start + 1, 0xFFF)), leaving the tracker behind.
Null termination detail: When the sender provides
lenbytes, it doesn’t explicitly send a trailing'\0'. That’s okay: any unsent byte position decodes to all-zero bits, so the receiver will hit a zero byte immediately after the last sent byte and stop. For special system messages (“entered/left the chat”), the code passessizeof(LITERAL)which includes the'\0'anyway.
The terminal UI & chat loop
chat_loop(fd)
- Probes the terminal width (
ioctl(TIOCGWINSZ)) for nicer prompt rendering. - Shows a little animated intro (
intro()), then asks your name with a retro “WHO R U?> ” effect. - After you type a non-trivial name (or
/qto quit), it: - Sends “
has entered the chat”. - Builds a prefix
"> "in an in-memory line buffer. - Switches stdin to raw/nonblocking and starts the main loop:
- Input side: uses
pollwith a 100ms timeout. Handles backspace (0x7F), Enter (sends the line), and/q(sends “left the chat” then exits). - Receive side: calls
check_chatroomto decode any new message blocks. On arrival, it prints a timestamp[HH:MM:SS]and the decoded line in blue. - Children cleanup: reaps any finished sender processes with
waitpid(-1, …, WNOHANG). - Prompt redraw: keeps the
_>prompt and your current line visible, trimming to terminal width. On exit (or any fatal error),hanguprestores the terminal to sane settings.
main()
Opens TIME_NS_PATH (/proc/self/ns/time) read-only and calls chat_loop.Why that file?
- It’s a real inode that all processes can open without special permission (on a typical system).
- It supports byte-range locks that are advisory and per-inode (visible across the host).
- Using a namespace handle is a cute trick to guarantee a single, shared rendezvous point.
Notable design choices & quirks
- Transport via advisory locks: Locks are visible to all processes and reported via
F_GETLK. By asking “who would block my write lock?” the program discovers which byte positions are already read-locked, i.e., which bits are “1”. - Bit packing: A
1bit ⇒ set a one-byte lock; a0bit ⇒ no lock. A contiguous run of1s might appear to receivers as a single longer lock region; the decoder handles that (it ORs the right range of bits). - Ready/TTL handshake: The header lock (
len == 3) gives a clean “ready” signal; the 5-second TTL prevents stale blocks from lingering forever, but leaves a one-byte breadcrumb so receivers can keep marching forward block-by-block. - Race avoidance: The reservation handshake in
claim_next_available_msg_blockconfirms that the process that thinks it holds the reservation actually does (by checkingpid), closing a lock-steal race window. - Input model: Raw nonblocking input with
pollproduces a responsive TUI without threads.
Limitations / gotchas
- Same-host only. This is not a network chat; it’s inter-process signaling on one machine.
- No authentication. Any local process can read/post/garble messages (or cause “interference on the wire”).
- Time window: Messages exist for ~5 seconds. If a receiver isn’t polling, it can miss messages (there’s no queue).
- Throughput: Each 1-bit triggers a
fcntllock op. It’s clever but inefficient for bulk data. - Portability: Assumes Linux (
/proc, namespace inodes). Whilefcntlbyte-locks are POSIX, the chosen rendezvous file path is Linux-specific. - Terminal behavior: If the process crashes before
hangup, your terminal might remain noncanonical/no-echo until you runstty sane.
How to compile & run
bash
gcc -Wall -O2 -o relay_chat relay_chat.c ./relay_chat
Open two terminals on the same machine, run it in both, choose different names, and type.
/q quits (and sends a “left the chat” notice).
Possible improvements
- Add a simple authentication tag inside each payload block (e.g., HMAC over text + timestamp) to filter noise.
- Replace
TIME_NS_PATHwith a configurable file (e.g., a world-readable file in/var/lock/…) so admins can confine/disable it. - Longer TTL or resend window; or a sequence number + reassembly for basic reliability.
- Use a single header lock whose length encodes payload length (saves scanning for
'0'). - Add rate limiting or backoff to reduce lock thrash under contention.