You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
File "/Users/runner/hostedtoolcache/PyPy/3.11.13/x64/lib/pypy3.11/site-packages/flint/types/fq_default.pypy311-pp73-darwin.so", line 270, in flint.types.fq_default.fq_default_ctx.gen
Failed example:
gf.gen()
Expected:
w
Got:
<BLANKLINE>
The change here does not look like it could cause that so maybe there is something undefined here.
# Ensure the var used for the generator of GF(p^d) is a single byte
var =self._parse_input_var(var)
self.var = var
I'm a bit suspicious about just assigning a Python bytes object to a char*.
Is that making a copy or possibly just storing a pointer?
The test failure is under PyPy so it is possible that this takes a different codepath in the Cython-generated C code.
Looking in the generated C code in build/src/flint/types/fq_default.pypy311-pp73-x86_64-linux-gnu.so.p/src/flint/types/fq_default.pyx.c there is:
/* "flint/types/fq_default.pyx":85 * # Ensure the var used for the generator of GF(p^d) is a single byte * var = self._parse_input_var(var) * self.var = var # <<<<<<<<<<<<<< * * # Ensure the fq_type is an integer between 0, 5 -- we allow users to*/__pyx_t_4=__Pyx_PyObject_AsWritableString(__pyx_v_var); if (unlikely((!__pyx_t_4) &&PyErr_Occurred())) __PYX_ERR(0, 85, __pyx_L1_error)
__pyx_v_self->var=__pyx_t_4;
Chasing the __Pyx_PyObject_AsWritableString macro leads to:
My guess then is that the fq_default_ctx ends up holding a pointer to the buffer for a bytes object that was deallocated. The memory was overwritten with something and then self.ctx.var.decode() ends up having a pointer to a null byte "'\0'" which decode turns into an empty string.
I guess we need a malloc/copy here or on the other hand if as noted in the comments FLINT only allows a single character string we could change it so that var is char[2] rather than a char* and just copy the first byte in.
Also _parse_input_var should check that the string is not of length 0.
I'd be very suspicious of that assignment to char*. The Python string ultimately owns the memory so you need to ensure that it outlives that char*. That's often fine for char* function locals but not so fine for class attributes.
There's probably some cases where it'll work (the val is None case will end up pointing to a cached string for example) but a lot of others where it won't.
So I don't think this is specifically a PyPy issue here
(I suspect a short string like w might end up interned by the Python interpreter so you get away with it. But that's not something to rely on)
Thanks, I forgot about this issue. Yes the assignment to char* I think is unsafe. My notes above were just me trying to figure out what it does which I think is basically just
cdef bytes bytestring
cdef char *var
var = bytestring->ob_start
It isn't safe to store that pointer if we don't also hold a reference to the bytestring.
Looking at it I think that var should just be a Python string object rather than char * though. There are many places doing self.var.decode() and char* is only needed at context initialisation time for functions like fq_default_ctx_init_type.
Assuming it works as shown above I think that while we hold a reference to the bytes object it should be safe to pass a bytes from Cython into a C function that expects const char* so that FLINT can malloc/copy and attach it to the fq_default_ctx struct.
Assuming it works as shown above I think that while we hold a reference to the bytes object it should be safe to pass a bytes from Cython into a C function that expects const char* so that FLINT can malloc/copy and attach it to the fq_default_ctx struct.
Yes that should be safe (and is the intention of the bytes to const char* conversion)
Thanks. I've fixed this by going for the safer option of just storing a normal Python string that can be reference counted.
It isn't clearly documented in FLINT but the char* that gets passed in to fq_default_ctx_init_* functions is copied into a newly malloc'd buffer which is then attached to the struct and gets cleared when the struct is freed:
By the way @da-woods since you are here do you know if there are any guarantees about how many times __dealloc__ is called or whether it could be called from two separate threads (in free-threading)?
Thanks, that is reassuring. In our case here it doesn't matter which thread but we want to call that *_clear function exactly once for each object so that is the precise guarantee that is needed.
Activity
oscarbenjamin commentedon Sep 7, 2025
I can't reproduce it. I've tested under pypy and all tests pass.
The failed doctest is just something like:
I've run that under PyPy 1000 times though and don't see the failure.
The implementation here creates a polynomial and tries to print that:
python-flint/src/flint/types/fq_default.pyx
Lines 505 to 506 in 0a59f53
The polynomial printing code does not check if var is a non-empty or valid string:
python-flint/src/flint/flint_base/flint_base.pyx
Lines 191 to 224 in 0a59f53
It is maybe worth adding a check there so that we could see if that is what failed if the test ever fails again in future.
The var is coming from
self.ctx.var.decode()which is:python-flint/src/flint/types/fq_default.pxd
Lines 25 to 28 in 0a59f53
and is initialised here:
python-flint/src/flint/types/fq_default.pyx
Lines 65 to 85 in 0a59f53
I'm a bit suspicious about just assigning a Python
bytesobject to achar*.Is that making a copy or possibly just storing a pointer?
The test failure is under PyPy so it is possible that this takes a different codepath in the Cython-generated C code.
Looking in the generated C code in
build/src/flint/types/fq_default.pypy311-pp73-x86_64-linux-gnu.so.p/src/flint/types/fq_default.pyx.cthere is:Chasing the
__Pyx_PyObject_AsWritableStringmacro leads to:The
CYTHON_COMPILING_IN_PYPYdefine looks potentially relevant.The macro
PyByteArray_AS_STRINGis defined here:https://github.com/python/cpython/blob/d7b9ea5cab984497526fcc0b988e4eb5988c1e88/Include/cpython/bytearrayobject.h#L5-L28
It looks like this just returns the raw pointer i.e. it does not allocate a new string.
My guess then is that the
fq_default_ctxends up holding a pointer to the buffer for a bytes object that was deallocated. The memory was overwritten with something and thenself.ctx.var.decode()ends up having a pointer to a null byte "'\0'" which decode turns into an empty string.I guess we need a malloc/copy here or on the other hand if as noted in the comments FLINT only allows a single character string we could change it so that var is
char[2]rather than achar*and just copy the first byte in.Also
_parse_input_varshould check that the string is not of length 0.da-woods commentedon Nov 1, 2025
I'd be very suspicious of that assignment to
char*. The Python string ultimately owns the memory so you need to ensure that it outlives thatchar*. That's often fine forchar*function locals but not so fine for class attributes.There's probably some cases where it'll work (the
val is Nonecase will end up pointing to a cached string for example) but a lot of others where it won't.So I don't think this is specifically a PyPy issue here
(I suspect a short string like
wmight end up interned by the Python interpreter so you get away with it. But that's not something to rely on)oscarbenjamin commentedon Nov 1, 2025
Thanks, I forgot about this issue. Yes the assignment to
char*I think is unsafe. My notes above were just me trying to figure out what it does which I think is basically justIt isn't safe to store that pointer if we don't also hold a reference to the bytestring.
Looking at it I think that var should just be a Python string object rather than
char *though. There are many places doingself.var.decode()andchar*is only needed at context initialisation time for functions likefq_default_ctx_init_type.Assuming it works as shown above I think that while we hold a reference to the bytes object it should be safe to pass a bytes from Cython into a C function that expects
const char*so that FLINT can malloc/copy and attach it to thefq_default_ctxstruct.da-woods commentedon Nov 1, 2025
Yes that should be safe (and is the intention of the
bytestoconst char*conversion)fix(fq_default): store variable name as str
oscarbenjamin commentedon Nov 1, 2025
Thanks. I've fixed this by going for the safer option of just storing a normal Python string that can be reference counted.
It isn't clearly documented in FLINT but the
char*that gets passed in tofq_default_ctx_init_*functions is copied into a newly malloc'd buffer which is then attached to the struct and gets cleared when the struct is freed:python-flint/src/flint/types/fq_default.pyx
Lines 45 to 48 in 80f941f
By the way @da-woods since you are here do you know if there are any guarantees about how many times
__dealloc__is called or whether it could be called from two separate threads (in free-threading)?da-woods commentedon Nov 1, 2025
The user code in
__dealloc__is called exactly once.I don't think you can reason too much in advance about which thread might run it but it should only be one thread.
oscarbenjamin commentedon Nov 2, 2025
Thanks, that is reassuring. In our case here it doesn't matter which thread but we want to call that
*_clearfunction exactly once for each object so that is the precise guarantee that is needed.