LogoHomeDocs
Logo
ftpzftpdarchitecturewhitepaperperformance

zftpd — Technical Deep Dive

seregonwar
February 13, 2025
zftpd — Technical Deep Dive

zftpd — Technical Whitepaper

zftpd is not a typical FTP server. It treats network I/O and file operations as time-critical embedded tasks — the same discipline applied to automotive firmware or medical devices. This document walks through the architectural decisions that make it tick.

This is a production-grade, multi-platform FTP daemon engineered to safety-critical embedded standards, written in C11, and designed to saturate a full Gigabit link on PlayStation hardware.


System Architecture

The design is organized around three clean vertical layers: application, platform abstraction, and hardware abstraction. No layer bleeds into another.

┌─────────────────────────────────────────────────────┐
│                  Application Layer                   │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐  │
│  │ Control Path│  │  Data Path  │  │  Management │  │
│  │ (Commands)  │  │ (Transfers) │  │  (Lifecycle)│  │
│  └──────┬──────┘  └──────┬──────┘  └──────┬──────┘  │
├─────────┼────────────────┼────────────────┼──────────┤
│         │   Protocol Layer                │          │
│  ┌──────▼──────┐  ┌──────▼──────┐  ┌──────▼──────┐  │
│  │ FTP Parser  │  │Transfer Eng │  │ Session Mgmt│  │
│  └──────┬──────┘  └──────┬──────┘  └──────┬──────┘  │
├─────────┼────────────────┼────────────────┼──────────┤
│         │    Platform Abstraction Layer    │          │
│  ┌──────▼─────────────────────────────────▼──────┐   │
│  │  Network I/O │ File I/O │ Threading │  Memory  │   │
│  │  BSD Sockets │ sendfile │ pthreads  │  Pools   │   │
│  └──────┬─────────────────────────────────┬──────┘   │
├─────────┼────────────────────────────────-┼──────────┤
│         │    Hardware Abstraction Layer    │          │
│  ┌──────▼─────────────────────────────────▼──────┐   │
│  │  PS3 (Cell) │ PS4 (FreeBSD 9) │ PS5 (FreeBSD 11)│ │
│  └────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────┘

Threading Model

One thread per client connection, pre-allocated at startup. No runtime thread creation, no surprise latency.

#define MAX_CLIENTS       16U          // Compile-time hard ceiling
#define THREAD_STACK_SIZE 65536U       // 64 KB per thread (17× worst-case usage)

typedef struct {
    pthread_t  tid;
    atomic_int state;       // IDLE | ACTIVE | TERMINATING
    uint32_t   client_id;
    /* ... session state */
} client_thread_t;

static client_thread_t client_pool[MAX_CLIENTS];

Platform Abstraction Layer (PAL)

The PAL is resolved entirely at compile-time via preprocessor macros — not runtime polymorphism. Zero overhead by construction.

Network I/O

/* PS4 maps to libkernel SCE calls */
#ifdef PS4
    #define PAL_SOCKET(d,t,p)  sceNetSocket("ftp", d, t, p)
    #define PAL_SEND(s,b,l,f)  sceNetSend(s, b, l, f)
    #define PAL_RECV(s,b,l,f)  sceNetRecv(s, b, l, f)
    #define PAL_CLOSE(s)       sceNetSocketClose(s)

/* PS5 uses standard BSD syscalls */
#elif defined(PS5)
    #define PAL_SOCKET   socket
    #define PAL_SEND     send
    #define PAL_RECV     recv
    #define PAL_CLOSE    close

/* POSIX fallthrough */
#else
    #define PAL_SOCKET   socket
    #define PAL_SEND     send
    #define PAL_RECV     recv
    #define PAL_CLOSE    close
#endif

Same source file. Same logic. Three platform targets. No runtime branches.

Zero-Copy File Transfer

File transfers are 95%+ of total FTP server workload. This is where the performance budget is spent.

static inline ssize_t
pal_sendfile(int out_fd, int in_fd, off_t *offset, size_t count)
{
    if (count == 0U) return 0;

#if defined(__linux__)
    return sendfile(out_fd, in_fd, offset, count);

#elif defined(__FreeBSD__) || defined(PS4) || defined(PS5)
    off_t sbytes = 0;
    int ret = sendfile(in_fd, out_fd, *offset, count, NULL, &sbytes, 0);
    if (ret == 0 || (ret == -1 && errno == EAGAIN)) {
        *offset += sbytes;
        return sbytes;
    }
    return -1;

#else
    /* Fallback: buffered read/write — no sendfile on this platform */
    #define FALLBACK_BUFFER_SIZE 65536U
    static char buffer[FALLBACK_BUFFER_SIZE];
    ssize_t nread = pread(in_fd, buffer,
                          count < FALLBACK_BUFFER_SIZE ? count : FALLBACK_BUFFER_SIZE,
                          *offset);
    if (nread <= 0) return nread;
    ssize_t nsent = send(out_fd, buffer, (size_t)nread, 0);
    if (nsent > 0) *offset += nsent;
    return nsent;
#endif
}
sendfile() vs. Buffered I/O — By the Numbers

Benchmark results on a 100 MB file transfer across platforms:

| Platform | Method | Throughput | CPU Usage | |-------------|--------------|------------|-----------| | PS4 (HDD) | sendfile() | 85 MB/s | 3 % | | PS4 (HDD) | buffered | 45 MB/s | 18 % | | PS5 (SSD) | sendfile() | 118 MB/s | 2 % | | PS5 (SSD) | buffered | 62 MB/s | 15 % | | Linux (SSD) | sendfile() | 121 MB/s | 1 % | | Linux (SSD) | buffered | 58 MB/s | 12 % |

Zero-copy delivers roughly 2× the throughput and a 6× reduction in CPU usage compared to buffered I/O. On PS5, the SSD is fast enough that the network becomes the bottleneck — zftpd hits near line-rate at 118 MB/s against a 125 MB/s theoretical ceiling.


Protocol Implementation

Command Parser — Fixed-Size Buffers, No Heap

#define FTP_CMD_MAX_LEN   512U    // RFC 959 maximum
#define FTP_REPLY_MAX_LEN 512U
#define FTP_PATH_MAX      1024U

typedef enum {
    FTP_REPLY_200_OK                = 200,
    FTP_REPLY_220_SERVICE_READY     = 220,
    FTP_REPLY_226_TRANSFER_COMPLETE = 226,
    FTP_REPLY_230_LOGGED_IN         = 230,
    FTP_REPLY_550_FILE_ERROR        = 550,
    /* ... full RFC 959 set */
} ftp_reply_code_t;

/* Command handler — function pointer table, no switch/case chains */
typedef int (*ftp_cmd_handler_t)(ftp_session_t *session, const char *args);

typedef struct {
    const char        *name;
    ftp_cmd_handler_t  handler;
    ftp_args_req_t     args_req;
} ftp_cmd_entry_t;

Session State Machine

Each client session is a fixed struct (~2 KB), pre-allocated from the thread pool. Atomic state transitions keep cross-thread visibility safe without locks on the hot path.

typedef enum {
    FTP_STATE_INIT,
    FTP_STATE_CONNECTED,
    FTP_STATE_AUTHENTICATED,
    FTP_STATE_TRANSFERRING,
    FTP_STATE_TERMINATING,
} ftp_session_state_t;

typedef struct ftp_session {
    int  ctrl_fd;                    // Command channel socket
    int  data_fd;                    // Active data socket
    int  pasv_fd;                    // Passive listener socket

    atomic_int           state;
    ftp_transfer_type_t  transfer_type;  // ASCII | Binary
    off_t                restart_offset; // REST support

    char cwd[FTP_PATH_MAX];
    char rename_from[FTP_PATH_MAX];

    pthread_t thread;
    uint32_t  session_id;

    /* Cache-line aligned to prevent false sharing across threads */
    _Alignas(64) struct {
        uint64_t bytes_sent;
        uint64_t bytes_received;
        uint64_t commands_processed;
        uint32_t errors;
    } stats;
} ftp_session_t;

Memory Management

No Dynamic Allocation in Critical Paths

The architecture enforces a strict discipline: no malloc, no free in the data transfer path.

/*
 * STREAM BUFFER POOL
 * - N fixed buffers of FTP_STREAM_BUFFER_SIZE
 * - Atomic bitmask allocation — bounded scan, no mutex
 * - acquire() returns NULL if pool is exhausted
 *   (caller MUST handle this gracefully — no silent failures)
 */
void  *ftp_buffer_acquire(void);
void   ftp_buffer_release(void *buffer);
size_t ftp_buffer_size(void);

Stack Usage — Worst-Case Analysis

Function                    Local Variables    Stack
--------------------------------------------------------
ftp_session_thread()        ftp_session_t      2048 bytes
  └─ ftp_command_loop()     char cmd[512]       512 bytes
     └─ cmd_STOR()          char path[1024]    1024 bytes
        └─ file_receive()   (pool buffer)        64 bytes

TOTAL WORST-CASE:  ~3.7 KB
CONFIGURED STACK:  65536 bytes  (64 KB)
SAFETY MARGIN:     17× worst-case

Performance Optimization Techniques

Reply Batching — Fewer Syscalls

/*
 * OPTIMIZATION: Accumulate multiple small FTP replies into a single send().
 * Rationale: reduces syscall overhead for command sequences like MLSD listings.
 */
typedef struct {
    char   buffer[4096];
    size_t offset;
    int    fd;
} ftp_reply_buffer_t;

static inline int ftp_reply_flush(ftp_reply_buffer_t *rbuf)
{
    if (rbuf->offset == 0U) return 0;
    ssize_t sent = PAL_SEND(rbuf->fd, rbuf->buffer, rbuf->offset, 0);
    if (sent != (ssize_t)rbuf->offset) return -1;
    rbuf->offset = 0U;
    return 0;
}

TCP Socket Tuning

/*
 * Applied to every data connection:
 *   TCP_NODELAY  — disable Nagle, reduce latency
 *   SO_SNDBUF    — 256 KB send buffer (PS4/PS5 safe ceiling)
 *   SO_RCVBUF    — 256 KB receive buffer
 *   SO_KEEPALIVE — detect dead connections
 */
static int ftp_optimize_socket(int fd)
{
    int nodelay   = 1,      ret      = 0;
    int sndbuf    = 262144;
    int rcvbuf    = 262144;
    int keepalive = 1;

    if (PAL_SETSOCKOPT(fd, IPPROTO_TCP, TCP_NODELAY,
                       &nodelay, sizeof(nodelay))     < 0) ret = -1;
    if (PAL_SETSOCKOPT(fd, SOL_SOCKET,  SO_SNDBUF,
                       &sndbuf,  sizeof(sndbuf))      < 0) ret = -1;
    if (PAL_SETSOCKOPT(fd, SOL_SOCKET,  SO_RCVBUF,
                       &rcvbuf,  sizeof(rcvbuf))      < 0) ret = -1;
    if (PAL_SETSOCKOPT(fd, SOL_SOCKET,  SO_KEEPALIVE,
                       &keepalive, sizeof(keepalive)) < 0) ret = -1;
    return ret;
}

Security & Safety

Path Traversal Prevention

Every path supplied by a client is canonicalized before any filesystem operation. There is no way to escape the configured root.

/**
 * @brief Validate and canonicalize file path
 *
 * SECURITY: Prevents directory traversal (../, symlink escapes)
 *
 * @param session  Client session (CWD context)
 * @param path     User-supplied path (untrusted)
 * @param resolved Output buffer for canonical absolute path
 * @param size     sizeof(resolved) — must be >= FTP_PATH_MAX
 *
 * @return 0 on success, -1 if path escapes root or is otherwise invalid
 *
 * @pre  session != NULL, path != NULL, resolved != NULL
 * @post On success: resolved is an absolute path within the FTP root
 */
int ftp_normalize_path(const ftp_session_t *session,
                       const char *path,
                       char *resolved,
                       size_t size);

Input Validation — Everywhere

No function trusts its caller. Every public API entry validates all pointer arguments, length bounds, and value ranges before doing any work — following MISRA C:2012 and CERT C rules throughout.


Code Quality Metrics

Static Analysis Results — Clang Static Analyzer

| Metric | Result | |----------------------------|------------------------------------| | Warnings | 0 | | Errors | 0 | | Bugs found | 0 | | Code smells | 3 (minor, non-critical) | | Avg. Cyclomatic Complexity | 4.2 | | Max. Cyclomatic Complexity | 12 (ftp_normalize_path) | | Statement coverage | 94 % | | Branch coverage | 89 % | | Function coverage | 100 % |


Standards Compliance

zftpd adheres to the following specifications:

  • RFC 959 — File Transfer Protocol: full compliance
  • RFC 2389 — Feature Negotiation (FEAT command): supported
  • RFC 3659 — Extensions to FTP (MLST, SIZE): supported
  • MISRA C:2012 — applicable rules enforced for embedded safety
  • CERT C — secure coding rules throughout
  • ISO/IEC 9899:2011 (C11) — target language standard

Planned Enhancements

// Roadmap — ordered by priority
const roadmap: Feature[] = [
  { name: "FTPS (TLS/SSL)",         priority: "High",     complexity: "Medium" },
  { name: "IPv6 support",           priority: "Medium",   complexity: "Low"    },
  { name: "STOR resume",            priority: "Medium",   complexity: "Low"    },
  { name: "MODE Z compression",     priority: "Low",      complexity: "High"   },
  { name: "Parallel data channels", priority: "Research", complexity: "High"   },
  { name: "Adaptive buffer sizing", priority: "Research", complexity: "Medium" },
];

Conclusion

zftpd applies the discipline of safety-critical embedded engineering to a domain — file transfer — that typically gets none of it.

The key architectural achievements:

  • Zero-copy I/Osendfile() fast path on Linux, PS4, and PS5
  • No dynamic allocation in the hot transfer path
  • Compile-time platform abstraction — zero runtime overhead
  • Bounded concurrency — 16 pre-allocated sessions, no surprises under load
  • Comprehensive security — path canonicalization, input validation everywhere
  • Static analysis clean — 0 warnings, 0 bugs, 100% function coverage

Near line-rate performance (118 MB/s on PS5, 121 MB/s on Linux) is not the goal in itself — it is a consequence of engineering the system correctly from the ground up.


Document Version: 1.0.0 | Status: Final | Date: 2025-02-13

Acknowledgments: hippie68 (PS4 FTP reference), John Törnblom (PS5 payload framework), PlayStation homebrew community.