Dev Docs AFP Daemon
AFP Daemon (afpd)
Audited against Netatalk
d63155e13on 2026-10-10. This describes the current source checkout; older releases may differ.
1. AFP Daemon Overview
The afpd daemon is the core component of Netatalk that implements the Apple Filing Protocol (AFP) server. It uses a process-per-connection architecture where a single parent process accepts connections and forks a dedicated child worker for each client session.
Key Source Files
| Component | File |
|---|---|
| Parent process & event loop | etc/afpd/main.c |
| Child worker event loop | etc/afpd/afp_dsi.c |
| Session forking | libatalk/dsi/dsi_getsess.c |
| AFP command dispatch | etc/afpd/switch.c |
| Open fork management | etc/afpd/fork.h, etc/afpd/ofork.c, etc/afpd/fork.c |
| Child process table | libatalk/util/server_child.c, include/atalk/server_child.h |
| Idle worker thread | etc/afpd/idle_worker.c, etc/afpd/idle_worker.h |
Caching subsystem (directory cache, AD metadata cache, resource fork cache, IPC cache hint system) is documented in the dedicated Caching Architecture page.
Process Architecture Diagram
%%{ init: { 'themeVariables': { 'fontSize': '14px' }, 'flowchart': { 'nodeSpacing': 15, 'rankSpacing': 40 } } }%%
graph TB
subgraph "afpd Parent Process"
A["poll() event loop<br/>(main.c: main())"]:::yellow
B["Signal handler<br/>afp_goaway()"]:::orange
C["Child table<br/>server_child_t"]:::cyan
D["Hint buffer<br/>hint_buf[128]"]:::purple
end
subgraph "afpd Child Workers"
E["Child 1<br/>afp_over_dsi()"]:::green
F["Child 2<br/>afp_over_dsi()"]:::green
G["Child N<br/>afp_over_dsi()"]:::green
end
subgraph "Each Child Process"
H["AFP command<br/>dispatch"]:::purple
I["Idle worker<br/>pthread"]:::green
J["Dircache<br/>(ARC)"]:::cyan
K["AD cache<br/>(Tier 1)"]:::cyan
L["Rfork cache<br/>(Tier 2)"]:::cyan
end
Mac1["Mac Client 1"]:::blue
Mac2["Mac Client 2"]:::blue
MacN["Mac Client N"]:::blue
CNID["CNID<br/>Database"]:::salmon
Mac1 --> E
Mac2 --> F
MacN --> G
A -->|"fork()"| E
A -->|"fork()"| F
A -->|"fork()"| G
A --> C
A --> D
D -->|"hint pipe"| E
D -->|"hint pipe"| F
D -->|"hint pipe"| G
E -->|"IPC socketpair"| A
F -->|"IPC socketpair"| A
E --- H
E --- I
H --> J
J --> K
K --> L
H --> CNID
classDef blue fill:#74b9ff,stroke:#333,rx:10,ry:10
classDef purple fill:#a29bfe,stroke:#333,rx:10,ry:10
classDef green fill:#55efc4,stroke:#333,rx:10,ry:10
classDef yellow fill:#ffeaa7,stroke:#333,rx:10,ry:10
classDef grey fill:#dfe6e9,stroke:#333,rx:10,ry:10
classDef salmon fill:#fab1a0,stroke:#333,rx:10,ry:10
classDef cyan fill:#81ecec,stroke:#333,rx:10,ry:10
classDef orange fill:#ff9f43,stroke:#333,rx:10,ry:10
2. Parent Process Architecture
The parent process in main() performs initialization, then enters an event loop that manages listening sockets, child IPC, signals, and cache hint batching.
Global State
Defined as file-scope state in etc/afpd/main.c:
| Variable | Type | Purpose |
|---|---|---|
dsi_obj |
AFPObj |
DSI protocol configuration object |
asp_obj |
AFPObj |
ASP protocol configuration object (DDP) |
server_children |
server_child_t * |
Hash table of active child processes |
reloadconfig |
sig_atomic_t |
Flag set by SIGHUP handler |
gotsigchld |
sig_atomic_t |
Flag set by SIGCHLD handler |
asev |
struct asev * |
Poll fd set and metadata |
Event Loop
The main event loop in main() uses poll() with a dynamic timeout:
%%{ init: { 'themeVariables': { 'fontSize': '14px' }, 'flowchart': { 'nodeSpacing': 15, 'rankSpacing': 40 } } }%%
flowchart TD
START["while (1)"]:::yellow
TIMEOUT{"hint_buf_count()<br/>> 0?"}:::cream
POLL["poll(asev→fdset,<br/>asev→used,<br/>poll_timeout)"]:::yellow
SIGCHLD{"gotsigchld?"}:::orange
CH["child_handler():<br/>waitpid loop,<br/>server_child_remove,<br/>asev_del_fd"]:::orange
ERR{"ret < 0?"}:::red
EINTR{"EINTR?"}:::cream
RELOAD{"reloadconfig?"}:::orange
DOCFG["Reset sockets,<br/>re-parse config,<br/>re-init listeners,<br/>server_child_kill<br/>(SIGHUP)"]:::orange
FDEVT{"ret > 0?"}:::cream
FDLOOP["for each fd<br/>with revents"]:::purple
LISTEN["LISTEN_FD:<br/>dsi_start() →<br/>fork child,<br/>add IPC_FD"]:::cyan
IPC["IPC_FD:<br/>ipc_server_read()"]:::purple
FLUSH{"hint_buf_count > 0<br/>AND (ret==0 OR<br/>count ≥ 128)?"}:::cream
DOFLUSH["hint_flush_pending<br/>(server_children)"]:::purple
START --> TIMEOUT
TIMEOUT -->|"Yes: 50ms"| POLL
TIMEOUT -->|"No: -1 (block)"| POLL
POLL --> SIGCHLD
SIGCHLD -->|"Yes"| CH --> ERR
SIGCHLD -->|"No"| ERR
ERR -->|"Yes"| EINTR
EINTR -->|"Yes"| START
EINTR -->|"No"| BREAK["break (fatal)"]:::red
ERR -->|"No"| RELOAD
RELOAD -->|"Yes"| DOCFG --> START
RELOAD -->|"No"| FDEVT
FDEVT -->|"Yes"| FDLOOP
FDLOOP --> LISTEN
FDLOOP --> IPC
FDLOOP --> STATS["STATS_FD:<br/>afpstats_handle_accept()"]:::cyan
STATS --> FLUSH
LISTEN --> FLUSH
IPC --> FLUSH
FDEVT -->|"No (timeout)"| FLUSH
FLUSH -->|"Yes"| DOFLUSH --> START
FLUSH -->|"No"| START
classDef yellow fill:#ffeaa7,stroke:#333,rx:10,ry:10
classDef cream fill:#f8f4e8,stroke:#333,rx:10,ry:10
classDef orange fill:#ff9f43,stroke:#333,rx:10,ry:10
classDef red fill:#ee5a5a,stroke:#333,color:#fff,rx:10,ry:10
classDef purple fill:#a29bfe,stroke:#333,rx:10,ry:10
classDef cyan fill:#81ecec,stroke:#333,rx:10,ry:10
classDef green fill:#55efc4,stroke:#333,rx:10,ry:10
Step ordering is critical (in main() event loop):
- SIGCHLD first — removes dead children from the table before hint flush or fd events can reference them
- Error handling — uses
saveerrnobecausechild_handler()clobbers errno viawaitpid/close/asev_del_fd - Config reload — resets listening sockets, re-parses config, broadcasts SIGHUP to children
- FD events — dispatches
LISTEN_FD(new connections),IPC_FD(child messages), and optionalSTATS_FD(afpstats Unix-socket requests) events - Hint flush — flushes buffered hints when timeout expires (ret == 0) or buffer is full (count ≥
HINT_BUF_SIZE= 128). See Caching Architecture: IPC Hint System for details.
Signal Mask Handling
Signals are unblocked before poll() and blocked immediately after (in the main() event loop):
pthread_sigmask(SIG_UNBLOCK, &sigs, NULL);
ret = poll(asev->fdset, asev->used, poll_timeout);
pthread_sigmask(SIG_BLOCK, &sigs, NULL);
The signal set sigs includes SIGALRM, SIGHUP, SIGUSR1, and SIGCHLD (configured in main() before the event loop).
3. Session Establishment
New connections are handled by dsi_getsession() called from dsi_start() (static function in main.c).
Fork and IPC Setup
%%{ init: { 'themeVariables': { 'fontSize': '14px' }, 'flowchart': { 'nodeSpacing': 15, 'rankSpacing': 40 } } }%%
sequenceDiagram
participant P as Parent (main.c)
participant G as dsi_getsession()
participant C as Child Process
P->>G: dsi_start() → dsi_getsession()
Note over G: socketpair(PF_UNIX, SOCK_STREAM)<br/>→ ipc_fds[0], ipc_fds[1]
Note over G: pipe() → hint_pipe[0], hint_pipe[1]
Note over G: setnonblock() on all 4 fds
G->>G: dsi->proto_open(dsi) [fork()]
rect rgb(255, 238, 170)
Note over P: Parent path
P->>P: close(ipc_fds[1])<br/>close(hint_pipe[0])
P->>P: server_child_add(pid,<br/>ipc_fds[0], hint_pipe[1])
P->>P: dsi->proto_close(dsi)
P->>P: asev_add_fd(child→ipc_fd,<br/>IPC_FD)
end
rect rgb(85, 239, 196)
Note over C: Child path
C->>C: obj→ipc_fd = ipc_fds[1]<br/>obj→hint_fd = hint_pipe[0]
C->>C: close(ipc_fds[0])<br/>close(hint_pipe[1])<br/>close(serversock)
C->>C: server_child_free()
C->>C: dsi_opensession(dsi)
C->>C: → return to dsi_start()
C->>C: configfree() → afp_over_dsi()
end
Key details from dsi_getsession():
- IPC socketpair (
ipc_fds): bidirectionalSOCK_STREAMfor session control (state updates, reconnect transfer viasend_fd/recv_fd+SIGURG) - Hint pipe (
hint_pipe): unidirectional parent→child pipe for dircache invalidation hints. Separate from IPC to avoid interfering withsend_fd()/recv_fd()+SIGURGsignaling. See Caching Architecture: Communication Channels for the rationale. - Pipe atomicity: hint messages are 22 bytes, within the platform’s
PIPE_BUF, guaranteeing non-interleaved writes of bounded batches (verified by_Static_assertinserver_ipc.c) - Child setup: the child closes the parent-side fds, frees the child table, and returns to
dsi_start()which callsafp_over_dsi()
4. Child Worker Architecture
afp_over_dsi() in etc/afpd/afp_dsi.c initializes the directory cache, starts iw_init() with synchronous fallback, applies socket options, and creates the signal self-pipe. It then processes DSI frames and AFP commands until the session exits.
Child Event Loop
flowchart TD
signals[Drain self-pipe and process deferred signals] --> buffered{DSI data buffered?}
buffered -->|yes| receive[dsi_stream_receive]
buffered -->|no| state{Disconnected or dying?}
state -->|yes| hints[Process pending cache hints]
hints --> receive
state -->|no| grant[iw_grant]
grant --> poll[Poll client socket, hint pipe, signal self-pipe]
poll --> revoke[iw_revoke before any cache access]
revoke --> ready[Process signals and hints; check socket events]
ready --> receive
receive --> dispatch[DSI command / AFP handler]
dispatch --> reply[Reply or streamed transfer]
reply --> signals
Sleeping sessions still poll so SIGURG reconnect notifications can wake them through the self-pipe. process_deferred_signals() executes pending signal actions in main-loop context. Descriptor errors and EOF have explicit paths; a hint pipe failure is not handled identically to a failed signal self-pipe.
AFP Command Dispatch Table
The two 256-entry tables in etc/afpd/switch.c are preauth_switch and postauth_switch. Before login, only afp_login, afp_logincont, afp_logout, and afp_login_ext are available. set_auth_switch() adjusts authenticated commands for protocol version and features; UAMs can also modify handlers.
Replay Cache
replaycache[REPLAYCACHE_SIZE] holds request ID, AFP command, and result, indexed by dsi->clientID % 128. The DSIWrite dispatch path checks it before re-executing a matching write. It stores a result, not an arbitrary full response payload; it should not be described as universal replay protection for every AFP command.
Tickle Handling
alarm_handler() only records alarm_pending and wakes the self-pipe. handle_alarm() performs the actual tickle and timeout work: recent DSI_DATA clears the idle indication; otherwise tickles advance and sleeping, disconnected, dying, or active sessions follow their state-specific limits. The parser converts sleep time and disconnect time from hours to tickles. Transport disconnects can retain authenticated sessions for primary reconnect.
5. Open Fork Management
AFP file access is tracked through the open fork table defined in fork.h and ofork.c.
struct ofork
Defined in fork.h:
struct file_key {
dev_t dev;
ino_t inode;
};
struct ofork {
struct file_key key; /* dev_t dev + ino_t inode */
struct adouble *of_ad; /* AppleDouble metadata handle */
struct vol *of_vol; /* volume */
cnid_t of_did; /* parent directory CNID */
uint16_t of_refnum; /* AFP fork reference number */
int of_flags; /* AFPFORK_* flags */
const unsigned char *of_virtual_data; /* virtual file data pointer */
off_t of_virtual_len; /* virtual file length */
struct ofork **prevp, *next; /* hash chain pointers */
};
Hash Table
OFORK_HASHSIZE: 64 buckets (defined inofork.c)- Hash function
hashfn():key->inode & (OFORK_HASHSIZE - 1) - Linear array
oforks[]: indexed byrefnum % nforks, wherenforks = min(max(getdtablesize() / 2, 2), 0xffff) - Dual indexing: hash chain for dev/ino lookup, array for refnum lookup
- Free-slot FIFO:
of_freeqavoids a linear free-slot search on ordinary allocation; slot 0 is reserved
Key Operations
| Function | File | Purpose |
|---|---|---|
of_alloc() |
ofork.c |
Allocate fork, hash by dev/ino, assign refnum |
of_find() |
ofork.c |
Lookup by refnum (array index + refnum verification) |
of_findname() |
ofork.c |
Lookup by dev/ino from stat() result |
of_dealloc() |
ofork.c |
Unhash, decrement ad_refcount, free if zero |
of_closefork() |
ofork.c |
Full close: flush dirty metadata, send IPC cache hints, ad_close |
AFPFORK Flags
Defined in fork.h:
| Flag | Value | Purpose |
|---|---|---|
AFPFORK_DATA |
1<<0 |
Open data fork |
AFPFORK_RSRC |
1<<1 |
Open resource fork |
AFPFORK_META |
1<<2 |
Open metadata |
AFPFORK_DIRTY |
1<<3 |
AD metadata modified (set in write_mark() in fork.c when ad_meta_fileno != -1) |
AFPFORK_ACCRD |
1<<4 |
Read access |
AFPFORK_ACCWR |
1<<5 |
Write access |
AFPFORK_MODIFIED |
1<<6 |
Data written (set in write_mark(), triggers FCE and IPC hints) |
AFPFORK_ERROR |
1<<7 |
Error opening fork |
AFPFORK_VIRTUAL |
1<<8 |
Virtual file fork |
Refcounting
The struct adouble has a reference count (ad_refcount). When multiple forks of the same file are open simultaneously, they share the same adouble via ad_ref(). of_dealloc() decrements the refcount and only frees the adouble when it reaches zero.
6. Caching Subsystem
Full documentation: Caching Architecture
Each afpd worker child maintains its own private, per-process three-tier cache hierarchy. There is no shared memory between workers — cross-process coherence is handled by the IPC cache hint relay system.
| Tier | What | Key File |
|---|---|---|
| Dircache | struct dir entries (files & dirs) with stat fields, ARC eviction |
dircache.c |
| AD cache | AppleDouble metadata (FinderInfo, dates, attributes) inline in struct dir |
ad_cache.c |
| Rfork cache | Resource fork content buffers with byte-budget LRU | ad_cache.c |
| IPC hints | Cross-process invalidation via parent relay | server_ipc.c |
The dircache supports both ARC (Adaptive Replacement Cache) and LRU modes. The IPC hint system uses batched, priority-sorted, PIPE_BUF-safe writes via dedicated hint pipes. See the Caching Architecture page for complete details including:
- ARC algorithm (T1/T2/B1/B2 lists, adaptation parameter p)
- AD metadata cache (Tier 1b):
ad_metadata_cached()with strict/non-strict validation - Resource fork content cache (Tier 2): budget management, LRU eviction
- IPC hint wire format, batching, rate limiting, receiver logic
struct dirfield layout and cache validation
7. Signal Handling
Parent Process Signals
afp_goaway() in etc/afpd/main.c handles parent shutdown, login shutdown, reload, and child-reaping flags. SIGPIPE and SIGXFSZ are ignored. The poll loop reaps SIGCHLD children before accessing their table/fds, saves errno across reaping, and reparses configuration when reloadconfig is set. Single-user mode ignores SIGHUP.
Child Process Signals
afp_over_dsi_sighandlers() installs request-recording handlers. Main-loop processing performs the corresponding work after revoking idle-worker access:
| Signal | Installed handler | Main-loop action |
|---|---|---|
| SIGHUP | afp_dsi_reload() |
Record/reload volume configuration in normal mode |
| SIGURG | afp_dsi_transfer_handler() |
handle_transfer_session(): receive the transferred socket and resume the session |
| SIGTERM/SIGQUIT | afp_dsi_die_handler() |
process_deferred_signals(): close/exit with the recorded cause |
| SIGUSR1 | afp_dsi_timedown_handler() |
handle_timedown(): initiate the five-minute shutdown path |
| SIGUSR2 | afp_dsi_getmesg_handler() |
handle_getmesg(): deliver server-message attention |
| SIGINT | afp_dsi_debug() |
Deferred cache dump and debug-log toggle |
| SIGALRM | alarm_handler() |
handle_alarm(): tickle and timeout processing |
The signal channel is implemented by atalk_sigpipe_* in libatalk/util/sigpipe.c. Reconnect no longer uses siglongjmp() to escape a signal handler. A shutdown function being called from the main loop must not be confused with the installed asynchronous handler.
8. Idle Worker
The background worker in etc/afpd/idle_worker.c frees queued invalidated directory entries and scans deferred child-removal chains. Its API is in etc/afpd/idle_worker.h.
The main thread owns caches while serving commands and processing hints. iw_note_work() records newly queued work. Immediately before blocking in poll(), iw_grant() permits background access only if work is pending. After every poll return, including EINTR, iw_revoke() waits for the worker to release access before the main thread touches cache state.
The handshake uses sequentially consistent iw_has_work, iw_can_work, and iw_is_working atomics. The worker publishes that it is working, then rechecks the grant before touching queues. This ordering prevents main/worker cache access from overlapping; acquire/release stores alone would not establish the required crossing guarantee.
| API | Contract |
|---|---|
iw_init() |
Start after fork and dircache initialization; failure enables synchronous cleanup |
iw_note_work() |
Main-thread producer hook after enqueue |
iw_grant() / iw_revoke() |
Grant the poll window and reclaim exclusive access |
iw_revoke_signal_safe() |
Revoke without waiting; exit paths only |
iw_shutdown() |
Join the worker and abandon queued work because the session exits |
iw_is_active() / iw_grant_active() |
Check worker presence/grant |
iw_log_stats() |
Log noted work, grants, completed/interrupted/aborted cycles and cleanup counts |
IW_WAKE_MS is 10 ms and the worker sleeps with nanosleep(), not a 1 ms condition-variable wait. Revocation first uses a bounded relax spin, then 100 µs sleeps while waiting. The worker blocks asynchronous signals but leaves fault signals deliverable and installs thread fault reporting.
If ATOMIC_INT_LOCK_FREE != 2, the module uses stubs and synchronous cleanup. When the worker is active, AFP command cleanup leaves queued frees to the next granted window.
9. Child Process Table
The child process table is managed by server_child.c with types in server_child.h.
server_child_t
typedef struct {
int servch_count; /* Current active session count */
int servch_nsessions; /* Maximum allowed sessions */
afp_child_t *servch_table[CHILD_HASHSIZE]; /* Hash table (32 buckets) */
} server_child_t;
afp_child_t
typedef struct afp_child {
pid_t afpch_pid; /* Worker process PID */
uid_t afpch_uid; /* Connected user ID */
int afpch_valid; /* 1 if clientid is set */
int afpch_killed; /* 1 if SIGTERM already sent */
uint32_t afpch_boottime; /* Client boot time */
time_t afpch_logintime; /* Time child was added */
uint32_t afpch_idlen; /* Client ID length */
char *afpch_clientid; /* Client ID string */
size_t afpch_sessiontoken_len; /* Reconnect token length */
char *afpch_sessiontoken; /* Opaque random reconnect token */
int afpch_ipc_fd; /* IPC socketpair (parent end) */
int afpch_hint_fd; /* Hint pipe (parent write end) */
char *afpch_hostname; /* Client hostname/address */
int16_t afpch_state; /* Session state (active/sleeping/disconnected) */
char *afpch_volumes; /* Mounted volumes string */
struct afp_child **afpch_prevp;
struct afp_child *afpch_next;
} afp_child_t;
Hash Function
CHILD_HASHSIZE = 32. Hash: ((pid >> 8) ^ pid) & (CHILD_HASHSIZE - 1) (defined as HASH() macro in server_child.c).
Key Operations
| Function | Purpose |
|---|---|
server_child_alloc() |
Allocate and initialize server_child_t with session limit |
server_child_add() |
Add child with PID, IPC fd, hint fd; enforces session limit |
server_child_remove() |
Unhash child, close IPC + hint fds, free memory, return IPC fd |
server_child_free() |
Free all children (called by child process after fork) |
server_child_kill() |
Send signal to all children |
server_child_resolve() |
Find child by PID |
server_child_transfer_session() |
Primary reconnect: write DSI ID + send_fd() + SIGURG |
Reconnect token tracking
IPC_SESSIONTOKEN publishes an opaque 8-byte token to server_child_set_session_token(). Session transfer checks token and UID in libatalk/util/server_child.c. Current tokens are random, not PID-derived. See Authentication.
Footnotes
Generated from the Netatalk GitHub Wiki
Last updated 2026-10-21