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];
Why Pre-allocated Threads?
Dynamic allocation during an active transfer is a latency spike waiting to happen. By pre-allocating the entire pool at startup, zftpd eliminates runtime allocation, caps concurrency at a known bound (preventing resource-exhaustion attacks), and keeps state transitions lock-free via atomics.
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
}
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.
Why No Dynamic Allocation?
On embedded platforms like PS4 and PS5, heap fragmentation and unpredictable allocator latency are unacceptable. zftpd uses a static streaming buffer pool (atomic bitmask, no locks), per-thread scratch buffers, and a deterministic arena allocator (pal_alloc) for the few bounded allocations that do exist. The result: no allocation surprises during transfers, ever.
/*
* 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);
Cyclomatic Complexity Note
ftp_normalize_path is the single function in the codebase with complexity above 10 (measured at 12). This is explicitly accepted for security-critical path canonicalization, where every edge case must be handled explicitly. All other functions remain below the project limit of 10.
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
| 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 (
FEATcommand): 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
zftpdapplies 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/O —
sendfile()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.
