From 767c76d5ac483e488c79c19f18e25a6c437fd768 Mon Sep 17 00:00:00 2001 From: Daniel Markstedt Date: Sat, 23 May 2026 22:51:46 +0200 Subject: [PATCH] CVE-2026-49390: afpd: strictly parse server quantum Reported-by: Michalis Vasileiadis (@vmihalis) Signed-off-by: Daniel Markstedt Reviewed-by: Andy Lemin (@andylemin) --- doc/manpages/man5/afp.conf.5.md | 2 +- libatalk/util/netatalk_conf.c | 72 ++++++++++++++++++++++++++++++++- 2 files changed, 71 insertions(+), 3 deletions(-) diff --git a/doc/manpages/man5/afp.conf.5.md b/doc/manpages/man5/afp.conf.5.md index 9bdb0f770..7021fa64f 100644 --- a/doc/manpages/man5/afp.conf.5.md +++ b/doc/manpages/man5/afp.conf.5.md @@ -446,7 +446,7 @@ the server (default is 200). server quantum = *number* **(G)** > This specifies the DSI server quantum. The default value is 0x100000 (1 -MiB). The maximum value is 0xFFFFFFFFF, the minimum is 32000. If you +MiB). The maximum value is 0xFFFFFFFF, the minimum is 32000. If you specify a value that is out of range, the default value will be set. Do not change this value unless you're absolutely sure, what you're doing diff --git a/libatalk/util/netatalk_conf.c b/libatalk/util/netatalk_conf.c index 30eb78908..a4a041e4b 100644 --- a/libatalk/util/netatalk_conf.c +++ b/libatalk/util/netatalk_conf.c @@ -872,6 +872,72 @@ static int getoption_int(const dictionary *conf, const char *vol, return result; } +/*! + * @brief Get uint32 option from config, use default value if unset or invalid + * + * @param[in] conf config handle + * @param[in] vol volume name (must be section name i.e. wo vars expanded) + * @param[in] opt option + * @param[in] defsec if "option" is not found in "vol", try to find it in section "defsec" + * @param[in] minval minimum accepted value + * @param[in] maxval maximum accepted value + * @param[in] defval if unset or out of range return "defval" + * + * @returns uint32 option from "vol" or "defsec", or "defval" + */ +static uint32_t getoption_uint32_strict(const dictionary *conf, const char *vol, + const char *opt, const char *defsec, + uint32_t minval, uint32_t maxval, + uint32_t defval) +{ + const char *val = getoption_str(conf, vol, opt, defsec, NULL); + const char *p; + char *endptr; + uintmax_t result; + + if (val == NULL) { + return defval; + } + + p = val; + + while (isspace((unsigned char) * p)) { + p++; + } + + if (*p == '\0' || *p == '-') { + LOG(log_warning, logtype_afpd, + "Config: %s value '%s' is invalid, using default %lu", + opt, val, (unsigned long)defval); + return defval; + } + + errno = 0; + result = strtoumax(p, &endptr, 0); + + while (isspace((unsigned char) * endptr)) { + endptr++; + } + + if (errno == ERANGE || result > UINT32_MAX || result < minval + || result > maxval) { + LOG(log_warning, logtype_afpd, + "Config: %s value '%s' out of valid range [%lu, %lu], using default %lu", + opt, val, (unsigned long)minval, (unsigned long)maxval, + (unsigned long)defval); + return defval; + } + + if (*endptr != '\0') { + LOG(log_warning, logtype_afpd, + "Config: %s value '%s' is invalid, using default %lu", + opt, val, (unsigned long)defval); + return defval; + } + + return (uint32_t)result; +} + /*! * @brief Get boolean option from volume, default section or global - * use default value if not set @@ -2643,8 +2709,10 @@ int afp_config_parse(AFPObj *AFPObj, char *processname) NULL, 4); options->dsireadbuf = getoption_int(config, INISEC_GLOBAL, "dsireadbuf", NULL, 12); - options->server_quantum = getoption_int(config, INISEC_GLOBAL, "server quantum", - NULL, DSI_SERVQUANT_DEF); + options->server_quantum = getoption_uint32_strict(config, INISEC_GLOBAL, + "server quantum", NULL, + DSI_SERVQUANT_MIN, DSI_SERVQUANT_MAX, + DSI_SERVQUANT_DEF); options->volnamelen = getoption_int(config, INISEC_GLOBAL, "volnamelen", NULL, 80); options->dircachesize = getoption_int(config, INISEC_GLOBAL, "dircachesize",