/*
* Copyright (c) 1999-2004, 2006-2008 Proofpoint, Inc. and its suppliers.
* All rights reserved.
*
* By using this file, you agree to the terms and conditions set
* forth in the LICENSE file which can be found at the top level of
* the sendmail distribution.
*
*/
#include "libmilter.h"
#endif /* NETINET || NETINET6 */
/* generic argument for functions in the command table */
struct arg_struct
{
};
/* structure for commands received from MTA */
struct cmdfct_t
{
};
/* possible values for cm_argt */
/* possible values for cm_todo */
/* not needed right now, done via return code instead */
/* index in macro array: macros only for these commands */
#define CI_CONN 0
#endif
#endif
#endif
#if CI_LAST < CI_ENVRCPT
#endif
#if CI_LAST < CI_ENVFROM
#endif
#endif
#if CI_LAST < CI_CONNECT
#endif
#if CI_LAST >= MAX_MACROS_ENTRIES
#endif
/* function prototypes */
static bool mi_rd_socket_ready __P((int));
#endif /* _FFR_WORKERS_POOL */
/* states */
/* in a mail transaction? must be before eom according to spec. */
/*
** set of next states
** each state (ST_*) corresponds to bit in an int value (1 << state)
** each state has a set of allowed transitions ('or' of bits of states)
** so a state transition is valid if the mask of the next state
** is set in the NX_* value
** this function is coded in trans_ok(), see below.
*/
#define NX_QUIT 0
#define NX_ABRT 0
static int next_states[] =
{
, NX_OPTS
, NX_CONN
, NX_HELO
, NX_MAIL
, NX_RCPT
, NX_DATA
, NX_HDRS
, NX_EOHS
, NX_BODY
, NX_ENDM
, NX_QUIT
, NX_ABRT
, NX_UNKN
, NX_Q_NC
};
/* commands received by milter */
{
};
/*
** Additional (internal) reply codes;
*/
/*
** MI_ENGINE -- receive commands and process them
**
** Parameters:
** ctx -- context structure
**
** Returns:
*/
int
{
int i;
int newstate;
bool call_abort;
sfsistat r;
char cmd;
{
mi_clr_macros(ctx, 0);
}
#else /* _FFR_WORKERS_POOL */
mi_clr_macros(ctx, 0);
#endif /* _FFR_WORKERS_POOL */
r = _SMFIS_NONE;
do
{
/* call abort only if in a mail transaction */
if (mi_stop() == MILTER_ABRT)
{
sm_dprintf("[%lu] milter_abort\n",
ret = MI_FAILURE;
break;
}
/*
** Notice: buf is allocated by mi_rd_cmd() and it will
** usually be free()d after it has been used in f().
** However, if the function returns _SMFIS_KEEP then buf
** contains macros and will not be free()d.
** Hence r must be set to _SMFIS_NONE if a new buf is
** allocated to avoid problem with housekeeping, esp.
** if the code "break"s out of the loop.
*/
/* Is the socket ready to be read ??? */
if (!mi_rd_socket_ready(sd))
{
ret = MI_CONTINUE;
break;
}
#endif /* _FFR_WORKERS_POOL */
r = _SMFIS_NONE;
{
sm_dprintf("[%lu] mi_engine: mi_rd_cmd error (%x)\n",
/*
** eof is currently treated as failure ->
** abort() instead of close(), otherwise use:
** if (cmd != SMFIC_EOF)
*/
ret = MI_FAILURE;
break;
}
sm_dprintf("[%lu] got cmd '%c' len %d\n",
for (i = 0; i < ncmds; i++)
{
break;
}
if (i >= ncmds)
{
/* unknown command */
sm_dprintf("[%lu] cmd '%c' unknown\n",
ret = MI_FAILURE;
break;
}
{
/* stop for now */
sm_dprintf("[%lu] cmd '%c' not impl\n",
ret = MI_FAILURE;
break;
}
/* is new state ok? */
sm_dprintf("[%lu] cur %x new %x nextmask %x\n",
{
sm_dprintf("[%lu] abort: cur %d (%x) new %d (%x) next %x\n",
/* call abort only if in a mail transaction */
/*
** try to reach the new state from HELO
** if it can't be reached, ignore the command.
*/
{
{
}
continue;
}
}
{
}
/* call function to deal with command */
r = (*f)(&arg);
{
}
{
ret = MI_FAILURE;
break;
}
if (r == SMFIS_ACCEPT)
{
/* accept mail, no further actions taken */
}
else if (r == SMFIS_REJECT || r == SMFIS_DISCARD ||
r == SMFIS_TEMPFAIL)
{
/*
** further actions depend on current state
** if the IGNO bit is set: "ignore" the error,
** i.e., stay in the current state
*/
}
else if (r == _SMFIS_ABORT)
{
sm_dprintf("[%lu] function returned abort\n",
ret = MI_FAILURE;
break;
}
if (ret == MI_FAILURE)
{
/* call abort only if in a mail transaction */
}
/* has close been called? */
&& ret != MI_CONTINUE
#endif /* _FFR_WORKERS_POOL */
)
{
}
#if !_FFR_WORKERS_POOL
mi_clr_macros(ctx, 0);
#endif /* _FFR_WORKERS_POOL */
return ret;
}
static size_t
char *buf;
char **newbuf;
{
int i;
mi_int32 v;
char *buffer;
len = 0;
for (i = 0; i < MAX_MACROS_ENTRIES; i++)
{
{
}
}
if (len > 0)
{
len += MILTER_OPTLEN;
{
for (i = 0; i < MAX_MACROS_ENTRIES; i++)
{
size_t l;
continue;
v = htonl(i);
ctx->ctx_mac_list[i], l);
offset += l;
}
}
else
{
/* oops ... */
}
}
else
{
len = MILTER_OPTLEN;
}
return len;
}
/*
** GET_NR_BIT -- get "no reply" bit matching state
**
** Parameters:
** state -- current protocol stage
**
** Returns:
** 0: no matching bit
** >0: the matching "no reply" bit
*/
static unsigned long get_nr_bit __P((int));
static unsigned long
int state;
{
unsigned long bit;
switch (state)
{
case ST_CONN:
bit = SMFIP_NR_CONN;
break;
case ST_HELO:
bit = SMFIP_NR_HELO;
break;
case ST_MAIL:
bit = SMFIP_NR_MAIL;
break;
case ST_RCPT:
bit = SMFIP_NR_RCPT;
break;
case ST_DATA:
bit = SMFIP_NR_DATA;
break;
case ST_UNKN:
bit = SMFIP_NR_UNKN;
break;
case ST_HDRS:
bit = SMFIP_NR_HDR;
break;
case ST_EOHS:
bit = SMFIP_NR_EOH;
break;
case ST_BODY:
bit = SMFIP_NR_BODY;
break;
default:
bit = 0;
break;
}
return bit;
}
/*
** SENDREPLY -- send a reply to the MTA
**
** Parameters:
** r -- reply code
** sd -- socket descriptor
** timeout_ptr -- (ptr to) timeout to use for sending
** ctx -- context structure
**
** Returns:
*/
static int
sfsistat r;
struct timeval *timeout_ptr;
{
int ret;
unsigned long bit;
ret = MI_SUCCESS;
{
if (r >= SMFIS_CONTINUE && r < _SMFIS_KEEP)
{
/* milter said it wouldn't reply, but it lied... */
"%s: milter claimed not to reply in state %d but did anyway %d\n",
}
/*
** Force specified behavior, otherwise libmilter
** and MTA will fail to communicate properly.
*/
switch (r)
{
case SMFIS_CONTINUE:
case SMFIS_TEMPFAIL:
case SMFIS_REJECT:
case SMFIS_DISCARD:
case SMFIS_ACCEPT:
case SMFIS_SKIP:
case _SMFIS_OPTIONS:
r = SMFIS_NOREPLY;
break;
}
}
switch (r)
{
case SMFIS_CONTINUE:
break;
case SMFIS_TEMPFAIL:
case SMFIS_REJECT:
{
}
else
{
}
break;
case SMFIS_DISCARD:
break;
case SMFIS_ACCEPT:
break;
case SMFIS_SKIP:
break;
case _SMFIS_OPTIONS:
{
mi_int32 v;
char *buffer;
(void *) &v, MILTER_LEN_BYTES);
else
ret = MI_FAILURE;
}
break;
case SMFIS_NOREPLY:
if (bit != 0 &&
{
/*
** milter doesn't want to send a reply,
** but the MTA doesn't have that feature: fake it.
*/
0);
}
break;
default: /* don't send a reply */
break;
}
return ret;
}
/*
** CLR_MACROS -- clear set of macros starting from a given index
**
** Parameters:
** ctx -- context structure
** m -- index from which to clear all macros
**
** Returns:
** None.
*/
void
int m;
{
int i;
for (i = m; i < MAX_MACROS_ENTRIES; i++)
{
{
}
{
}
}
}
/*
** MI_CLR_SYMLIST -- clear list of macros
**
** Parameters:
** ctx -- context structure
**
** Returns:
** None.
*/
static void
{
int i;
for (i = SMFIM_FIRST; i <= SMFIM_LAST; i++)
{
{
}
}
}
/*
** MI_CLR_CTX -- clear context
**
** Parameters:
** ctx -- context structure
**
** Returns:
** None.
*/
void
{
{
}
{
}
{
"%s: private data not NULL",
}
mi_clr_macros(ctx, 0);
}
/*
** ST_OPTIONNEG -- negotiate options
**
** Parameters:
** g -- generic argument structure
**
** Returns:
*/
static int
st_optionneg(g)
genarg *g;
{
bool testmode = false;
#endif /* _FFR_MILTER_CHECK */
unsigned long, unsigned long,
unsigned long, unsigned long,
unsigned long *, unsigned long *,
unsigned long *, unsigned long *));
return SMFIS_CONTINUE;
/* check for minimum length */
if (g->a_len < MILTER_OPTLEN)
{
"%s: st_optionneg[%ld]: len too short %d < %d",
return _SMFIS_ABORT;
}
/* protocol version */
v = ntohl(i);
/* check for minimum version */
if (v < SMFI_PROT_VERSION_MIN)
{
"%s: st_optionneg[%ld]: protocol version too old %d < %d",
return _SMFIS_ABORT;
}
ctx->ctx_mta_prot_vers = v;
else
v = ntohl(i);
/* no flags? set to default value for V1 actions */
if (v == 0)
v = SMFI_V1_ACTS;
internal_pflags = 0;
v = ntohl(i);
/* no flags? set to default value for V1 protocol */
if (v == 0)
v = SMFI_V1_PROT;
{
/*
** Allow changing the size only if milter is compiled
** against a version that supports this.
** If a milter is dynamically linked against a newer
** libmilter version, we don't want to "surprise"
** it with a larger buffer as it may rely on it
** even though it is not documented as a limit.
*/
if (bitset(SMFIP_MDS_1M, v))
{
(void) smfi_setmaxdatasize(MILTER_MDS_1M);
}
else if (bitset(SMFIP_MDS_256K, v))
{
(void) smfi_setmaxdatasize(MILTER_MDS_256K);
}
}
# if 0
/* don't log this for now... */
{
"%s: st_optionneg[%ld]: milter version=%X, trying flags=%X",
}
# endif /* 0 */
#endif /* _FFR_MDS_NEGOTIATE */
/*
** MTA protocol flags.
** We pass the internal flags to the milter as "read only",
** i.e., a milter can read them so it knows which size
** will be used, but any changes by a milter will be ignored
** (see below, search for SMFI_INTERNAL).
*/
/*
** Copy flags from milter struct into libmilter context;
** this variable will be used later on to check whether
** the MTA "actions" can fulfill the milter requirements,
** but it may be overwritten by the negotiate callback.
*/
;
{
int r;
/*
** let milter decide whether the features offered by the
** MTA are "good enough".
** Notes:
** - libmilter can "fake" some features (e.g., SMFIP_NR_HDR)
** - m_f2, m_f3 are for future extensions
*/
m_pflags |= SMFIP_SKIP;
r = fi_negotiate(g->a_ctx,
0, 0,
if (testmode)
m_pflags &= ~SMFIP_TEST;
#endif /* _FFR_MILTER_CHECK */
/*
** Types of protocol flags (pflags):
** 1. do NOT send protocol step X
** 2. MTA can do/understand something extra (SKIP,
** send unknown RCPTs)
** 3. MTA can deal with "no reply" for various protocol steps
** Note: this mean that it isn't possible to simply set all
** flags to get "everything":
** setting a flag of type 1 turns off a step
** (it should be the other way around:
** a flag means a protocol step can be sent)
** setting a flag of type 3 requires that milter
** never sends a reply for the corresponding step.
** Summary: the "negation" of protocol flags is causing
** problems, but at least for type 3 there is no simple
** solution.
**
** What should "all options" mean?
** send all protocol steps _except_ those for which there is
** no callback (currently registered in ctx_pflags)
** expect SKIP as return code? Yes
** send unknown RCPTs? No,
** must be explicitly requested?
** "no reply" for some protocol steps? No,
** must be explicitly requested.
*/
if (SMFIS_ALL_OPTS == r)
{
}
else if (r != SMFIS_CONTINUE)
{
"%s: st_optionneg[%ld]: xxfi_negotiate returned %d (protocol options=0x%lx, actions=0x%lx)",
return _SMFIS_ABORT;
}
else
{
}
/* check whether some flags need to be "faked" */
i = ctx->ctx_pflags2mta;
if ((ctx->ctx_mta_pflags & i) != i)
{
unsigned int idx;
unsigned long b;
/*
** If some behavior can be faked (set in fake_pflags),
** but the MTA doesn't support it, then unset
** that flag in the value that is sent to the MTA.
*/
{
b = 1 << idx;
if ((ctx->ctx_mta_pflags & b) != b &&
(fake_pflags & b) == b)
ctx->ctx_pflags2mta &= ~b;
}
}
}
else
{
/*
** Set the protocol flags based on the values determined
** in mi_listener() which checked the defined callbacks.
*/
}
/* check whether actions and protocol requirements can be satisfied */
i = ctx->ctx_aflags;
if ((i & ctx->ctx_mta_aflags) != i)
{
"%s: st_optionneg[%ld]: 0x%lx does not fulfill action requirements 0x%x",
return _SMFIS_ABORT;
}
i = ctx->ctx_pflags2mta;
if ((ctx->ctx_mta_pflags & i) != i)
{
/*
** Older MTAs do not support some protocol steps.
** As this protocol is a bit "wierd" (it asks for steps
** should turn off those "negative" requests.
** Currently these are only SMFIP_NODATA and SMFIP_NOUNKNOWN.
*/
i = ctx->ctx_pflags2mta;
}
if ((ctx->ctx_mta_pflags & i) != i)
{
"%s: st_optionneg[%ld]: 0x%lx does not fulfill protocol requirements 0x%x",
return _SMFIS_ABORT;
}
sm_dprintf("[%lu] milter_negotiate:"
" mta_actions=0x%lx, mta_flags=0x%lx"
" actions=0x%lx, flags=0x%lx\n"
sm_dprintf("[%lu] milter_negotiate:"
" testmode=%d, pflags2mta=%X, internal_pflags=%X\n"
/* in test mode: take flags without further modifications */
if (!testmode)
/* Warning: check statement below! */
#endif /* _FFR_MILTER_CHECK */
/*
** Remove the internal flags that might have been set by a milter
** and set only those determined above.
*/
return _SMFIS_OPTIONS;
}
/*
** ST_CONNECTINFO -- receive connection information
**
** Parameters:
** g -- generic argument structure
**
** Returns:
** continue or filter-specified value
*/
static int
genarg *g;
{
size_t l;
size_t i;
char *s, family;
unsigned short port = 0;
if (g == NULL)
return _SMFIS_ABORT;
return SMFIS_CONTINUE;
s = g->a_buf;
i = 0;
l = g->a_len;
while (s[i] != '\0' && i <= l)
++i;
if (i + 1 >= l)
return _SMFIS_ABORT;
/* Move past trailing \0 in host string */
i++;
family = s[i++];
if (family != SMFIA_UNKNOWN)
{
if (i + sizeof port >= l)
{
"%s: connect[%ld]: wrong len %d >= %d",
return _SMFIS_ABORT;
}
sizeof port);
i += sizeof port;
/* make sure string is terminated */
if (s[l - 1] != '\0')
return _SMFIS_ABORT;
# if NETINET
if (family == SMFIA_INET)
{
!= 1)
{
"%s: connect[%ld]: inet_aton failed",
return _SMFIS_ABORT;
}
if (port > 0)
}
else
# endif /* NETINET */
# if NETINET6
if (family == SMFIA_INET6)
{
if (mi_inet_pton(AF_INET6, s + i,
{
"%s: connect[%ld]: mi_inet_pton failed",
return _SMFIS_ABORT;
}
if (port > 0)
}
else
# endif /* NETINET6 */
# if NETUNIX
if (family == SMFIA_UNIX)
{
{
"%s: connect[%ld]: path too long",
return _SMFIS_ABORT;
}
}
else
# endif /* NETUNIX */
{
"%s: connect[%ld]: unknown family %d",
return _SMFIS_ABORT;
}
}
}
/*
** ST_EOH -- end of headers
**
** Parameters:
** g -- generic argument structure
**
** Returns:
** continue or filter-specified value
*/
static int
st_eoh(g)
genarg *g;
{
if (g == NULL)
return _SMFIS_ABORT;
return SMFIS_CONTINUE;
}
/*
** ST_DATA -- DATA command
**
** Parameters:
** g -- generic argument structure
**
** Returns:
** continue or filter-specified value
*/
static int
st_data(g)
genarg *g;
{
if (g == NULL)
return _SMFIS_ABORT;
return SMFIS_CONTINUE;
}
/*
**
** Parameters:
** g -- generic argument structure
**
** Returns:
** continue or filter-specified value
*/
static int
st_helo(g)
genarg *g;
{
if (g == NULL)
return _SMFIS_ABORT;
{
/* paranoia: check for terminating '\0' */
return MI_FAILURE;
}
return SMFIS_CONTINUE;
}
/*
** ST_HEADER -- header line
**
** Parameters:
** g -- generic argument structure
**
** Returns:
** continue or filter-specified value
*/
static int
st_header(g)
genarg *g;
{
if (g == NULL)
return _SMFIS_ABORT;
return SMFIS_CONTINUE;
else
return _SMFIS_ABORT;
}
char **argv; \
int r; \
\
if (g == NULL) \
return _SMFIS_ABORT; \
return SMFIS_CONTINUE; \
return _SMFIS_ABORT; \
return r;
/*
** ST_SENDER -- MAIL FROM command
**
** Parameters:
** g -- generic argument structure
**
** Returns:
** continue or filter-specified value
*/
static int
st_sender(g)
genarg *g;
{
}
/*
** ST_RCPT -- RCPT TO command
**
** Parameters:
** g -- generic argument structure
**
** Returns:
** continue or filter-specified value
*/
static int
st_rcpt(g)
genarg *g;
{
}
/*
** ST_UNKNOWN -- unrecognized or unimplemented command
**
** Parameters:
** g -- generic argument structure
**
** Returns:
** continue or filter-specified value
*/
static int
st_unknown(g)
genarg *g;
{
if (g == NULL)
return _SMFIS_ABORT;
return SMFIS_CONTINUE;
}
/*
** ST_MACROS -- deal with macros received from the MTA
**
** Parameters:
** g -- generic argument structure
**
** Returns:
**
** Side effects:
** set pointer in macro array to current values.
*/
static int
st_macros(g)
genarg *g;
{
int i;
char **argv;
return _SMFIS_FAIL;
return _SMFIS_FAIL;
switch (g->a_buf[0])
{
case SMFIC_CONNECT:
i = CI_CONN;
break;
case SMFIC_HELO:
i = CI_HELO;
break;
case SMFIC_MAIL:
i = CI_MAIL;
break;
case SMFIC_RCPT:
i = CI_RCPT;
break;
case SMFIC_DATA:
i = CI_DATA;
break;
case SMFIC_BODYEOB:
i = CI_EOM;
break;
case SMFIC_EOH:
i = CI_EOH;
break;
default:
return _SMFIS_FAIL;
}
return _SMFIS_KEEP;
}
/*
** ST_QUIT -- quit command
**
** Parameters:
** g -- generic argument structure
**
** Returns:
** noreply
*/
/* ARGSUSED */
static int
st_quit(g)
genarg *g;
{
if (g == NULL)
return _SMFIS_ABORT;
mi_clr_macros(g->a_ctx, 0);
return _SMFIS_NOREPLY;
}
/*
** ST_BODYCHUNK -- deal with a piece of the mail body
**
** Parameters:
** g -- generic argument structure
**
** Returns:
** continue or filter-specified value
*/
static int
st_bodychunk(g)
genarg *g;
{
if (g == NULL)
return _SMFIS_ABORT;
g->a_len);
return SMFIS_CONTINUE;
}
/*
** ST_BODYEND -- deal with the last piece of the mail body
**
** Parameters:
** g -- generic argument structure
**
** Returns:
** continue or filter-specified value
**
** Side effects:
** sends a reply for the body part (if non-empty).
*/
static int
st_bodyend(g)
genarg *g;
{
sfsistat r;
if (g == NULL)
return _SMFIS_ABORT;
r = SMFIS_CONTINUE;
{
g->a_len > 0)
{
g->a_len);
if (r != SMFIS_CONTINUE &&
return _SMFIS_ABORT;
}
}
if (r == SMFIS_CONTINUE &&
return r;
}
/*
** ST_ABORTFCT -- deal with aborts
**
** Parameters:
** g -- generic argument structure
**
** Returns:
** abort or filter-specified value
*/
static int
st_abortfct(g)
genarg *g;
{
if (g == NULL)
return _SMFIS_ABORT;
return _SMFIS_NOREPLY;
}
/*
** TRANS_OK -- is the state transition ok?
**
** Parameters:
** old -- old state
** new -- new state
**
** Returns:
** state transition ok
*/
static bool
{
int s, n;
s = old;
if (s >= SIZE_NEXT_STATES)
return false;
do
{
/* is this state transition allowed? */
return true;
/*
** no: try next state;
** this works since the relevant states are ordered
** strict sequentially
*/
n = s + 1;
if (n >= SIZE_NEXT_STATES)
return false;
/*
** can we actually "skip" this state?
** see fix_stm() which sets this bit for those
** states which the filter program is not interested in
*/
s = n;
else
return false;
} while (s < SIZE_NEXT_STATES);
return false;
}
/*
** FIX_STM -- add "skip" bits to the state transition table
**
** Parameters:
** ctx -- context structure
**
** Returns:
** None.
**
** Side effects:
** may change state transition table.
*/
static void
{
unsigned long fl;
return;
}
/*
** DEC_ARGV -- split a buffer into a list of strings, NULL terminated
**
** Parameters:
** buf -- buffer with several strings
** len -- length of buffer
**
** Returns:
** array of pointers to the individual strings
*/
static char **
char *buf;
{
char **s;
size_t i;
nelem = 0;
for (i = 0; i < len; i++)
{
if (buf[i] == '\0')
++nelem;
}
if (nelem == 0)
return NULL;
/* last entry is only for the name */
if (s == NULL)
return NULL;
s[0] = buf;
{
if (buf[i] == '\0')
{
++elem;
if (i + 1 >= len)
else
}
}
/* overwrite last entry (already done above, just paranoia) */
return s;
}
/*
** DEC_ARG2 -- split a buffer into two strings
**
** Parameters:
** buf -- buffer with two strings
** len -- length of buffer
** s1,s2 -- pointer to result strings
**
** Returns:
*/
static int
char *buf;
char **s1;
char **s2;
{
size_t i;
/* paranoia: check for terminating '\0' */
return MI_FAILURE;
continue;
if (i >= len - 1)
return MI_FAILURE;
return MI_SUCCESS;
}
/*
** SENDOK -- is it ok for the filter to send stuff to the MTA?
**
** Parameters:
** ctx -- context structure
** flag -- flag to check
**
** Returns:
** sending allowed (in current state)
*/
bool
int flag;
{
return false;
/* did the milter request this operation? */
return false;
/* are we in the correct state? It must be "End of Message". */
}
/*
** MI_RD_SOCKET_READY - checks if the socket is ready for read(2)
**
** Parameters:
** sd -- socket_t
**
** Returns:
** true iff socket is ready for read(2)
*/
static bool
{
int n;
int nerr = 0;
#if SM_CONF_POLL
#else /* SM_CONF_POLL */
#endif /* SM_CONF_POLL */
do
{
#if SM_CONF_POLL
#else /* SM_CONF_POLL */
#endif /* SM_CONF_POLL */
if (n < 0)
{
{
nerr++;
continue;
}
return true;
}
if (n == 0)
return false;
break;
} while (nerr < MI_RD_MAX_ERR);
if (nerr >= MI_RD_MAX_ERR)
return false;
#if SM_CONF_POLL
#else /* SM_CONF_POLL */
#endif /* SM_CONF_POLL */
}
#endif /* _FFR_WORKERS_POOL */