File Server Application Message Structures, offsetof(), RESP_MT_* Types

 

File Server Application
Chapter 46 – Section 46.8  |  Message Structures, offsetof(), RESP_MT_* Types
46.8
TLPI Section
offsetof()
Safe Size Calculation
3 Types
FAILURE/DATA/END

What is This Application?

Section 46.8 of TLPI presents a complete, real-world client-server application built on System V message queues. A file server receives requests from clients asking for file contents, then streams the file back as a series of response messages. This application demonstrates many important design principles simultaneously:

  • One message queue per client (IPC_PRIVATE)
  • Well-known server queue key
  • Message type constants (RESP_MT_DATA, RESP_MT_END, RESP_MT_FAILURE)
  • Safe message size calculation using offsetof()
  • Fork-based request handling
  • Header file shared between server and client

Understanding this application means understanding how all the pieces of System V message queue programming fit together.

Key Concepts

SERVER_KEY 0x1aaaaaa1 requestMsg structure responseMsg structure offsetof() macro REQ_MSG_SIZE RESP_MSG_SIZE RESP_MT_FAILURE = 1 RESP_MT_DATA = 2 RESP_MT_END = 3 Protocol design

1. The Shared Header File — svmsg_file.h

In any client-server application, both the client and server must agree on the format of messages they exchange. This agreement is encoded in a shared header file. The file server header defines:

  • The IPC key for the server’s well-known queue (SERVER_KEY)
  • The structure of request messages (requestMsg)
  • The structure of response messages (responseMsg)
  • Constants for the message size calculations
  • Constants for response message types (RESP_MT_*)
/* svmsg_file.h — shared between server and client */
#include <sys/types.h>
#include <sys/msg.h>
#include <sys/stat.h>
#include <stddef.h>   /* for offsetof() */
#include <limits.h>   /* for PATH_MAX */
#include <fcntl.h>
#include <signal.h>
#include <sys/wait.h>

/* The IPC key used to create/find the server's message queue.
 * This is a hexadecimal constant chosen to be unique on this system.
 * Both server and client use this same key to msgget(). */
#define SERVER_KEY  0x1aaaaaa1

/* ================================================================
 * REQUEST MESSAGE: Client → Server
 * ================================================================ */
struct requestMsg {
    long mtype;               /* Required first field (unused — set to 1) */
    int  clientId;            /* ID of the client's private response queue */
    char pathname[PATH_MAX];  /* Null-terminated path of file to retrieve */
};

/* REQ_MSG_SIZE: size of the mtext part (excludes mtype).
 *
 * We CANNOT simply use sizeof(requestMsg) - sizeof(long) because
 * the compiler may insert padding bytes between clientId and pathname.
 * Using offsetof() gives the exact byte offset of each field,
 * correctly accounting for any padding.
 */
#define REQ_MSG_SIZE  (offsetof(struct requestMsg, pathname)  \
                      - offsetof(struct requestMsg, clientId) \
                      + PATH_MAX)

/* ================================================================
 * RESPONSE MESSAGE: Server → Client
 * ================================================================ */
#define RESP_MSG_SIZE  8192

struct responseMsg {
    long mtype;               /* One of RESP_MT_* values below */
    char data[RESP_MSG_SIZE]; /* File content chunk OR error message */
};

/* Response message type constants — these are protocol states: */
#define RESP_MT_FAILURE  1    /* Server could not open the requested file */
#define RESP_MT_DATA     2    /* This message contains a chunk of file data */
#define RESP_MT_END      3    /* All file data has been sent — transfer done */

2. Why offsetof() is Used for REQ_MSG_SIZE

This is one of the most instructive details in the file server design. A naive programmer might write:

/* WRONG — may not account for struct padding */
#define REQ_MSG_SIZE  (sizeof(struct requestMsg) - sizeof(long))

The problem is struct padding. The C compiler is allowed to insert invisible padding bytes between struct fields to satisfy alignment requirements. For example:

struct requestMsg {
    long mtype;       /* 8 bytes on 64-bit */
    int  clientId;    /* 4 bytes */
    /* POSSIBLE: 4 bytes of padding here for alignment of pathname */
    char pathname[PATH_MAX]; /* 4096 bytes */
};

/* sizeof(struct requestMsg) might be 8 + 4 + 4(padding) + 4096 = 4112 */
/* sizeof(long) = 8 */
/* sizeof(requestMsg) - sizeof(long) = 4104 — INCLUDES the 4 bytes of padding! */

If we pass an oversized msgsz to msgsnd(), we send unnecessary garbage bytes. The correct approach using offsetof():

#include <stddef.h>

/* offsetof(struct requestMsg, pathname):
 *   = byte offset of 'pathname' from the start of the struct
 *   = 8 (mtype) + 4 (clientId) + 4 (padding) = 16
 *
 * offsetof(struct requestMsg, clientId):
 *   = byte offset of 'clientId' from the start of the struct
 *   = 8 (just mtype)
 *
 * REQ_MSG_SIZE = (16 - 8) + PATH_MAX = 8 + 4096 = 4104
 *   This is: sizeof(clientId) + any_padding + sizeof(pathname)
 *   Which is exactly the mtext part (everything after mtype)
 */
#define REQ_MSG_SIZE  (offsetof(struct requestMsg, pathname)  \
                      - offsetof(struct requestMsg, clientId) \
                      + PATH_MAX)

/* Demonstration: */
#include <stdio.h>
int main(void) {
    printf("offset of mtype    : %zu\n", offsetof(struct requestMsg, mtype));
    printf("offset of clientId : %zu\n", offsetof(struct requestMsg, clientId));
    printf("offset of pathname : %zu\n", offsetof(struct requestMsg, pathname));
    printf("REQ_MSG_SIZE       : %zu\n", (size_t)REQ_MSG_SIZE);
    printf("sizeof full struct : %zu\n", sizeof(struct requestMsg));
    return 0;
}
offsetof(type, member) from <stddef.h> gives the byte offset of a member from the beginning of its containing struct. It is the only portable way to calculate field positions, especially when padding may exist. The TLPI code uses this to compute exactly the size of the “mtext” portion of the request struct, which starts at clientId (not mtype).

3. Response Message Types — The Transfer Protocol

The RESP_MT_* constants define a simple 3-state protocol for file transfer. The mtype field of each response message indicates what kind of message it is:

Response Message State Machine

Client sends
REQUEST

Server tries to open file
FAIL
mtype=1
RESP_MT_FAILURE
SUCCESS
mtype=2
RESP_MT_DATA (×N)
END
mtype=3, RESP_MT_END
Constant Value Meaning data field
RESP_MT_FAILURE 1 File could not be opened Error message string
RESP_MT_DATA 2 This message contains file data Up to 8192 bytes of file content
RESP_MT_END 3 Transfer complete, no more data Empty (or ignored)

The client reads messages in a loop. If it sees RESP_MT_DATA, it writes the data to stdout (or a file). If it sees RESP_MT_END, it exits the loop — transfer done. If it sees RESP_MT_FAILURE, it prints the error and exits.

Why mtype=1 for RESP_MT_FAILURE? In the per-client queue design, mtype is used for message classification rather than routing (since each client has its own queue). Values 1, 2, and 3 were simply chosen as meaningful protocol state identifiers. FAILURE = 1, DATA = 2, END = 3 — a natural ordering. The client uses msgtyp=0 in msgrcv() so it accepts any mtype from its private queue.

Code Example 1: Complete Header File with Annotations
/* svmsg_file_complete.h
 * Full header for file server application.
 * Include this in BOTH server and client source files.
 */
#ifndef SVMSG_FILE_H
#define SVMSG_FILE_H

#include <sys/types.h>
#include <sys/msg.h>
#include <sys/stat.h>
#include <stddef.h>      /* offsetof() */
#include <limits.h>      /* PATH_MAX */
#include <fcntl.h>
#include <signal.h>
#include <sys/wait.h>

/*
 * Server's well-known IPC key.
 * Both client and server call:
 *   msgget(SERVER_KEY, ...)
 * to locate/create the server's request queue.
 *
 * The value 0x1aaaaaa1 is arbitrary — any unique hex constant works.
 * In production, derive it with ftok() from a known file path.
 */
#define SERVER_KEY  0x1aaaaaa1

/*
 * REQUEST MESSAGE
 * Sent by client → server via the server's well-known queue.
 */
struct requestMsg {
    long mtype;                  /* Unused as a type; set to 1 by convention */
    int  clientId;               /* Queue ID of client's private response queue */
    char pathname[PATH_MAX];     /* Absolute path of file to retrieve */
};

/*
 * REQ_MSG_SIZE: exact size of the mtext portion (everything after mtype).
 *
 * We use offsetof() to safely skip over:
 *   - mtype itself (not part of mtext)
 *   - any padding the compiler places before clientId
 *
 * This ensures msgsnd() sends exactly the right number of bytes.
 */
#define REQ_MSG_SIZE \
    (offsetof(struct requestMsg, pathname) \
     - offsetof(struct requestMsg, clientId) \
     + PATH_MAX)

/*
 * RESPONSE MESSAGE
 * Sent by server child → client via the client's private queue.
 * mtype indicates the protocol state (see RESP_MT_* below).
 */
#define RESP_MSG_SIZE  8192     /* Max bytes per response chunk */

struct responseMsg {
    long mtype;                  /* RESP_MT_FAILURE, RESP_MT_DATA, or RESP_MT_END */
    char data[RESP_MSG_SIZE];    /* File chunk or error string */
};

/*
 * Response protocol states (used as mtype values):
 *
 *   RESP_MT_FAILURE (1): Server could not open the file.
 *                         data[] contains an error description.
 *
 *   RESP_MT_DATA    (2): data[] contains up to RESP_MSG_SIZE bytes
 *                         of file content. Multiple DATA messages may follow.
 *
 *   RESP_MT_END     (3): No more data. Transfer complete.
 *                         Client should stop reading.
 */
#define RESP_MT_FAILURE  1
#define RESP_MT_DATA     2
#define RESP_MT_END      3

#endif /* SVMSG_FILE_H */

Code Example 2: Demonstrating offsetof() and Struct Padding
#include <stdio.h>
#include <stddef.h>
#include <limits.h>
#include <sys/msg.h>

struct requestMsg {
    long mtype;
    int  clientId;
    char pathname[PATH_MAX];
};

#define REQ_MSG_SIZE \
    (offsetof(struct requestMsg, pathname) \
     - offsetof(struct requestMsg, clientId) \
     + PATH_MAX)

/* A struct with visible padding to illustrate the concept */
struct withPadding {
    char  a;    /* 1 byte */
    /* 3 bytes padding (on 32-bit) or 7 bytes padding (on 64-bit) */
    int   b;    /* 4 bytes */
    char  c;    /* 1 byte */
    /* 3 bytes padding (to align d to 4-byte boundary) */
    int   d;    /* 4 bytes */
};

int main(void) {
    printf("=== requestMsg field offsets ===\n");
    printf("offsetof(mtype)    : %2zu bytes from struct start\n",
           offsetof(struct requestMsg, mtype));
    printf("offsetof(clientId) : %2zu bytes from struct start\n",
           offsetof(struct requestMsg, clientId));
    printf("offsetof(pathname) : %2zu bytes from struct start\n",
           offsetof(struct requestMsg, pathname));
    printf("sizeof(requestMsg) : %zu\n",  sizeof(struct requestMsg));
    printf("REQ_MSG_SIZE       : %zu\n",  (size_t)REQ_MSG_SIZE);
    printf("Naive calculation  : %zu  (may be WRONG if padding exists!)\n",
           sizeof(struct requestMsg) - sizeof(long));

    printf("\n=== Struct padding illustration ===\n");
    printf("sizeof(withPadding): %zu  (expected ~12, actual may be larger)\n",
           sizeof(struct withPadding));
    printf("offset of 'b'      : %zu\n", offsetof(struct withPadding, b));
    printf("offset of 'c'      : %zu\n", offsetof(struct withPadding, c));
    printf("offset of 'd'      : %zu\n", offsetof(struct withPadding, d));

    return 0;
}
/* Sample output (64-bit Linux):
 * ==========================================================
 * offsetof(mtype)    :  0 bytes from struct start
 * offsetof(clientId) :  8 bytes from struct start
 * offsetof(pathname) : 12 bytes from struct start   (no padding here)
 * sizeof(requestMsg) : 4108
 * REQ_MSG_SIZE       : 4100   (8 + 4096 — just clientId + pathname)
 * Naive calculation  : 4100   (same here, but may differ on other ABI)
 * ===========================================================
 */
Real-world importance: On most common Linux/x86-64 targets, the naive calculation happens to give the same result as offsetof() for this particular struct. But on architectures with stricter alignment rules (e.g., some ARM or MIPS systems), padding may appear, and the offsetof() version is the only portable one. TLPI uses it as a best practice, not just because it’s needed here.

Code Example 3: Client Response-Reading Loop

This shows how the client handles the three RESP_MT_* message types:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/msg.h>
/* Include svmsg_file.h here in real code */

#define RESP_MT_FAILURE 1
#define RESP_MT_DATA    2
#define RESP_MT_END     3
#define RESP_MSG_SIZE   8192

struct responseMsg {
    long mtype;
    char data[RESP_MSG_SIZE];
};

/*
 * readFileFromServer()
 *
 * Reads all response messages from clientQid and writes
 * file contents to stdout. Returns 0 on success, -1 on failure.
 */
int readFileFromServer(int clientQid) {
    struct responseMsg resp;
    ssize_t msgLen;

    while (1) {
        /* msgtyp = 0 → accept any message from this private queue */
        msgLen = msgrcv(clientQid,
                        &resp,
                        sizeof(resp) - sizeof(long),
                        0,     /* Accept any mtype */
                        0);    /* Block until message available */

        if (msgLen == -1) {
            perror("msgrcv in client");
            return -1;
        }

        switch (resp.mtype) {
            case RESP_MT_DATA:
                /* Write this chunk to stdout */
                if (write(STDOUT_FILENO, resp.data, msgLen) != msgLen) {
                    perror("write");
                    return -1;
                }
                break;

            case RESP_MT_END:
                /* Transfer complete — stop reading */
                fprintf(stderr, "\n[File transfer complete]\n");
                return 0;

            case RESP_MT_FAILURE:
                /* Server reported an error */
                resp.data[msgLen] = '\0';   /* Ensure null termination */
                fprintf(stderr, "Server error: %s\n", resp.data);
                return -1;

            default:
                /* Unexpected mtype — protocol violation */
                fprintf(stderr, "Unexpected mtype: %ld\n", resp.mtype);
                return -1;
        }
    }
}

int main(void) {
    /* In real usage, create private queue, send request, then call: */
    /* return readFileFromServer(clientQid); */
    printf("Function defined successfully — integrate with full client.\n");
    return 0;
}
Why switch on mtype, not compare data content? The protocol uses mtype as a type indicator because it is checked by the kernel itself during selective receive. It separates control information (what kind of message is this?) from payload (the data[] field). This is more efficient than checking a field inside the data and avoids the need for a separate control channel.

4. Security Limitation of This File Server
⚠ No Authentication

The TLPI file server performs no authentication of clients. Any user who can run the client program can request any file that the server process has read access to. If the server runs as root, it could potentially serve any file on the system — including /etc/shadow.

A production server would need to: (1) verify the client’s identity using the UID from the IPC permission structure, (2) check that the requesting UID has permission to read the file, or (3) drop privileges and run with limited access. TLPI notes this limitation explicitly and leaves enhancement as an exercise.

Additionally, the server trusts the pathname field in the request without sanitization. A path like ../../etc/passwd (relative path traversal) or /etc/shadow (absolute) could be requested. Always validate pathnames in production servers.

Interview Questions — File Server Application Design

Q1. What is the purpose of the REQ_MSG_SIZE macro and why is offsetof() used instead of sizeof()?
REQ_MSG_SIZE defines the exact size of the mtext portion of the request message — everything after the long mtype field. It is used as the msgsz argument in msgsnd().

offsetof() is used because the C compiler may insert invisible padding bytes between struct members for alignment. A naive calculation of sizeof(requestMsg) - sizeof(long) would include the mtype field’s own size and any padding, potentially giving a larger number than the actual payload size. offsetof(struct requestMsg, pathname) - offsetof(struct requestMsg, clientId) + PATH_MAX gives the exact size from the start of clientId to the end of pathname — the true mtext size.

Q2. What are the RESP_MT_* constants and how does the client use them?
They are protocol state values used as the mtype field in response messages:
RESP_MT_FAILURE (1) — File could not be opened; data[] contains an error description.
RESP_MT_DATA (2)data[] contains a chunk of file content (up to RESP_MSG_SIZE bytes).
RESP_MT_END (3) — Transfer complete; client should stop reading.

The client receives messages with msgtyp = 0 (accept any type) and switches on the mtype: for DATA it writes to stdout, for END it exits the loop, for FAILURE it prints the error.

Q3. Why is SERVER_KEY defined as a hexadecimal constant (0x1aaaaaa1) rather than using ftok()?
For simplicity in the example. ftok(pathname, projId) generates a key from a file’s inode number and a project ID, which makes it more portable and unique — but it requires both the server and client to agree on the same file path and project ID. A hardcoded constant like 0x1aaaaaa1 works fine for teaching examples as long as it doesn’t collide with another application’s key. In production, ftok() is preferred because hardcoded keys may conflict between different applications on the same system.
Q4. What is PATH_MAX and why is it used in the requestMsg structure?
PATH_MAX (defined in <limits.h>) is the maximum length of a file path on the system — typically 4096 bytes on Linux. It is used as the size of the pathname field in requestMsg to ensure the pathname buffer is large enough to hold any valid absolute path. Using PATH_MAX instead of an arbitrary constant makes the code portable — if PATH_MAX changes on a different system, the struct adapts automatically.
Q5. How does the file server handle large files that exceed RESP_MSG_SIZE (8192 bytes)?
Large files are split into multiple RESP_MT_DATA messages. The server child reads the file in a loop with read(fd, resp.data, RESP_MSG_SIZE). Each successful read produces one DATA message containing up to 8192 bytes. The client receives these in order and writes each chunk to its output. After the last read (when read() returns 0 indicating EOF), the server sends a single RESP_MT_END message to signal completion. The client’s loop exits when it sees RESP_MT_END.
Q6. What happens in the server if two clients request files at the same time?
The server forks a separate child process for each request. Both children run concurrently. Each child has the ID of its respective client’s private queue and sends responses independently to that queue. Because each response goes to a different queue (the client’s private IPC_PRIVATE queue), there is no interference between the two transfers — they proceed in parallel. The server’s main process is free to accept more requests immediately after forking, without waiting for either child to finish.
Q7. What security vulnerabilities exist in this simple file server, and how would you fix them?
1. No authentication: Any process can request any file the server can read. Fix: check ds.msg_perm.uid of the client’s queue against the file’s ownership, or use Unix credentials passing.
2. Path traversal: A client can request arbitrary paths including /etc/shadow. Fix: validate that the requested path is within an allowed directory using realpath() and prefix checking.
3. Null termination: The pathname in the request may not be null-terminated if the client is malicious. Fix: force req.pathname[PATH_MAX-1] = '\0' before use.
4. No rate limiting: A client can flood the server with requests, causing many forks. Fix: limit concurrent children or use a thread pool instead.
Q8. What is the role of the mtype field in responseMsg, and how is it different from its role in a single-queue design?
In the per-client queue design used by the file server, mtype in responseMsg serves as a protocol state indicator (FAILURE/DATA/END), not as a routing key. Routing is already handled by using separate queues — the server sends to the client’s specific queue, so only that client will read it.

In the single-queue design, mtype in response messages equals the client’s PID, and its primary role is routing — the client filters messages by its own PID using msgrcv(msgtyp = getpid()).

This dual use of mtype — routing in single-queue vs. typing/classifying in per-client-queue — is an important distinction to understand.

Leave a Reply

Your email address will not be published. Required fields are marked *