Control: tags -1 + patch

It looks like the Python 3.13 FTBFS in aflplusplus is caused by a failure to properly handle exceptions when initialising a Python mutator. I don't have a good understanding of Python's C interface, but it looks like when PyObject_GetAttrString() is called on a key that doesn't exist, it returns NULL and raises an AttributeError. And that somehow gets stored in the Python interpreter and emerges when AFL++ first tries to call Python code, which it does in init_py(). This is how you end up with an AttributeError apparently caused by calling random.seed().

According to the documentation at <https://docs.python.org/3/c-api/object.html#c.PyObject_GetAttrString>, if you don't want an exception in these circumstances, you can use PyObject_GetOptionalAttrString() instead. That doesn't raise AttributeError, though it can raise other exceptions. It's available starting in Python 3.13.

The attached patch switches init_py_module() to use PyObject_GetOptionalAttr() in place of PyObject_GetAttrString(). This is sufficient to get the package to compile, passing its tests, with Python 3.13. The patch also adds a version to the python3-dev build-dependency to support this.

However, this is not really a good fix. Any other exception raised by PyObject_GetOptionalAttr() will still be ignored, and will presumably emerge in init_py(). A better approach would be to add proper handling of Python exceptions to init_py_module(). And a patch that requires Python 3.13 probably won't be acceptable upstream right now.

Still, it's not any worse than the code that's already there, and it means that at least well-behaved Python mutators will work again.

--
Ben Harris
diff --git a/debian/control b/debian/control
index 7bdddea..69bc136 100644
--- a/debian/control
+++ b/debian/control
@@ -14,7 +14,7 @@ Build-Depends:
  gcc-multilib [amd64 i386 mips64el mipsel ppc64 s390x sparc64 x32],
  lld-19 [!s390x],
  llvm-19-dev,
- python3-dev,
+ python3-dev (>= 3.13~),
 # for the tests
  clang,
  libcmocka-dev,
diff --git a/debian/patches/replace-all-pyobject_getattrstring-with-.patch b/debian/patches/replace-all-pyobject_getattrstring-with-.patch
new file mode 100644
index 0000000..b31f085
--- /dev/null
+++ b/debian/patches/replace-all-pyobject_getattrstring-with-.patch
@@ -0,0 +1,105 @@
+From: Ben Harris <bj...@bjh21.me.uk>
+Date: Thu, 9 Jan 2025 21:54:14 +0000
+X-Dgit-Generated: 4.21c-5~1.gbp3dcdb9 f3ad11aa08a06c8862b7c49b45ced0c78350d1e0
+Subject: Replace all PyObject_GetAttrString() with ...Optional...()
+
+This suppresses the AttributeError that would otherwise be raised when
+the attribute doesn't exist.  It doesn't suppress other exceptions,
+though, and we still don't handle those properly.
+
+This enables compiling with Python 3.13.  Without this patch, the last
+AttributeError from PyObject_GetAttrString() seems to get saved and
+raised when we next call into Python code, which happens in init_py().
+Unfortunately, using PyObject_GetOptionalAttrString() means that this
+patch doesn't work with any Python older than 3.13.
+
+Bug-Debian: 1091402
+
+---
+
+diff --git a/src/afl-fuzz-python.c b/src/afl-fuzz-python.c
+index 873b25e..2a91c98 100644
+--- a/src/afl-fuzz-python.c
++++ b/src/afl-fuzz-python.c
+@@ -220,45 +220,50 @@ static py_mutator_t *init_py_module(afl_state_t *afl, u8 *module_name) {
+   if (py_module != NULL) {
+ 
+     u8 py_notrim = 0;
+-    py_functions[PY_FUNC_INIT] = PyObject_GetAttrString(py_module, "init");
++    PyObject_GetOptionalAttrString(py_module, "init",
++                                   &py_functions[PY_FUNC_INIT]);
+     if (!py_functions[PY_FUNC_INIT]) {
+ 
+       WARNF("init function not found in python module");
+ 
+     }
+ 
+-    py_functions[PY_FUNC_FUZZ] = PyObject_GetAttrString(py_module, "fuzz");
++    PyObject_GetOptionalAttrString(py_module, "fuzz",
++                                   &py_functions[PY_FUNC_FUZZ]);
+     if (!py_functions[PY_FUNC_FUZZ])
+-      py_functions[PY_FUNC_FUZZ] = PyObject_GetAttrString(py_module, "mutate");
+-    py_functions[PY_FUNC_DESCRIBE] =
+-        PyObject_GetAttrString(py_module, "describe");
+-    py_functions[PY_FUNC_FUZZ_COUNT] =
+-        PyObject_GetAttrString(py_module, "fuzz_count");
+-    py_functions[PY_FUNC_POST_PROCESS] =
+-        PyObject_GetAttrString(py_module, "post_process");
+-    py_functions[PY_FUNC_INIT_TRIM] =
+-        PyObject_GetAttrString(py_module, "init_trim");
+-    py_functions[PY_FUNC_POST_TRIM] =
+-        PyObject_GetAttrString(py_module, "post_trim");
+-    py_functions[PY_FUNC_TRIM] = PyObject_GetAttrString(py_module, "trim");
+-    py_functions[PY_FUNC_HAVOC_MUTATION] =
+-        PyObject_GetAttrString(py_module, "havoc_mutation");
+-    py_functions[PY_FUNC_HAVOC_MUTATION_PROBABILITY] =
+-        PyObject_GetAttrString(py_module, "havoc_mutation_probability");
+-    py_functions[PY_FUNC_QUEUE_GET] =
+-        PyObject_GetAttrString(py_module, "queue_get");
+-    py_functions[PY_FUNC_FUZZ_SEND] =
+-        PyObject_GetAttrString(py_module, "fuzz_send");
+-    py_functions[PY_FUNC_POST_RUN] =
+-        PyObject_GetAttrString(py_module, "post_run");
+-    py_functions[PY_FUNC_SPLICE_OPTOUT] =
+-        PyObject_GetAttrString(py_module, "splice_optout");
++      PyObject_GetOptionalAttrString(py_module, "mutate",
++                                     &py_functions[PY_FUNC_FUZZ]);
++    PyObject_GetOptionalAttrString(py_module, "describe",
++                                   &py_functions[PY_FUNC_DESCRIBE]);
++    PyObject_GetOptionalAttrString(py_module, "fuzz_count",
++                                   &py_functions[PY_FUNC_FUZZ_COUNT]);
++    PyObject_GetOptionalAttrString(py_module, "post_process",
++                                   &py_functions[PY_FUNC_POST_PROCESS]);
++    PyObject_GetOptionalAttrString(py_module, "init_trim",
++                                   &py_functions[PY_FUNC_INIT_TRIM]);
++    PyObject_GetOptionalAttrString(py_module, "post_trim",
++                                   &py_functions[PY_FUNC_POST_TRIM]);
++    PyObject_GetOptionalAttrString(py_module, "trim",
++                                   &py_functions[PY_FUNC_TRIM]);
++    PyObject_GetOptionalAttrString(py_module, "havoc_mutation",
++                                   &py_functions[PY_FUNC_HAVOC_MUTATION]);
++    PyObject_GetOptionalAttrString(py_module, "havoc_mutation_probability",
++                                   &py_functions[PY_FUNC_HAVOC_MUTATION_PROBABILITY]);
++    PyObject_GetOptionalAttrString(py_module, "queue_get",
++                                   &py_functions[PY_FUNC_QUEUE_GET]);
++    PyObject_GetOptionalAttrString(py_module, "fuzz_send",
++                                   &py_functions[PY_FUNC_FUZZ_SEND]);
++    PyObject_GetOptionalAttrString(py_module, "post_run",
++                                   &py_functions[PY_FUNC_POST_RUN]);
++    PyObject_GetOptionalAttrString(py_module, "splice_optout",
++                                   &py_functions[PY_FUNC_SPLICE_OPTOUT]);
+     if (py_functions[PY_FUNC_SPLICE_OPTOUT]) { afl->custom_splice_optout = 1; }
+-    py_functions[PY_FUNC_QUEUE_NEW_ENTRY] =
+-        PyObject_GetAttrString(py_module, "queue_new_entry");
+-    py_functions[PY_FUNC_INTROSPECTION] =
+-        PyObject_GetAttrString(py_module, "introspection");
+-    py_functions[PY_FUNC_DEINIT] = PyObject_GetAttrString(py_module, "deinit");
++    PyObject_GetOptionalAttrString(py_module, "queue_new_entry",
++                                   &py_functions[PY_FUNC_QUEUE_NEW_ENTRY]);
++    PyObject_GetOptionalAttrString(py_module, "introspection",
++                                   &py_functions[PY_FUNC_INTROSPECTION]);
++    PyObject_GetOptionalAttrString(py_module, "deinit",
++                                   &py_functions[PY_FUNC_DEINIT] );
+     if (!py_functions[PY_FUNC_DEINIT])
+       WARNF("deinit function not found in python module");
+ 
diff --git a/debian/patches/series b/debian/patches/series
index 25de15c..0c74927 100644
--- a/debian/patches/series
+++ b/debian/patches/series
@@ -1,3 +1,4 @@
 Do-not-ignore-failing-gcc_plugin-install.patch
 Disable-builds-trying-to-access-network.patch
 Ignore-laf-intel-compcov-test-failure-for-s390x-arch.patch
+replace-all-pyobject_getattrstring-with-.patch

Reply via email to