From 767c76d5ac483e488c79c19f18e25a6c437fd768 Mon Sep 17 00:00:00 2001
From: Daniel Markstedt <daniel@mindani.net>
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 <daniel@mindani.net>
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",
