Fun privacy bugs: Type confusion in bindings
When building a library of differential privacy primitives, you have two options. You can either re-implement all the basic logic, in which case you have to avoid all known pitfalls. Or you can simply write bindings to reuse an existing implementation from another language.
PyDP, a Python DP library maintained by OpenMined, chose the second path: the library is a thin wrapper around Google's C++ DP library. In theory, this limits the number of new things that can go wrong. But in practice, you might encounter different kinds of unexpected issues!
C++ has static types, and each function only works on arguments of a
predefined type. Therefore, the C++ library defines two distinct AddNoise
methods, with different input types: one to add integer noise, the other to
add floating-point noise.
By contrast, Python's type system is dynamic: you can't predict the type of a
variable just by reading the source code. Instead, you have to actually run the
program and see what that variable contains. PyDP defines a single add_noise
method which calls either C++ method depending on input type. This logic is
implemented as follows.
template <typename T>
py::class_<dp::NumericalMechanism>& DefPyAddNoise(
py::class_<dp::NumericalMechanism>& pyclass) {
using FunctorType = T (dp::NumericalMechanism::*)(T);
return pyclass.def("add_noise",
static_cast<FunctorType>(&dp::NumericalMechanism::AddNoise),
py::arg("result"));
}
// ...
DefPyAddNoise<int64_t>(numerical_mech);
DefPyAddNoise<double>(numerical_mech);
This code may look a bit obscure, but it does two simple things:
- It declares a Python
add_noisemethod on allNumericalMechanismclasses. - It defines which C++ method to use when this method is called. Integers are
converted to
int64then use the integer C++ method; floating-point numbers are converted todoublethen use the floating-point C++ method.
Except… Data analysis code doesn't always use Python's native types (int or
float): often, using NumPy and its numeric types can be a lot
more performant. Those allow users to specify the size of each numeric type,
e.g. by using np.int16 or np.float32. How do the bindings handle such types?
The default logic of the underlying library checks two conditions, in order.
- First, it look at whether the value type matches the declared type. For
integers, it checks whether it implements
__index__. For floats, it checks whether it's a subclass of Python's built-infloat. - If the first pass fails, it look at whether the value can be converted to
the declared type, using conversions methods like
int(v)orfloat(v). In that case, the first declared binding with an available conversion is used.
The first condition is met for Python's int type and all NumPy integer types,
which implement __index__. It also catches Python's float type and NumPy's
np.float64: the latter is a subclass of float type, so the bindings
correctly assign it to a C++ double.
However, what happens if a value v has type np.float32? This type is not a
subclass of float, so the first condition doesn't apply! So the logic moves on
to the second condition, and goes through the bindings in order. The first is
the integer binding, so it checks whether it's possible to call int(v). This
conversion exists, and simply uses truncation: for example,
int(np.float32(1.7))==1. So our np.float32 value gets truncated, and is then
perturbed using integer noise.
But this messes up all our DP calculations! If we're calculating a DP sum of small numbers, with clamping bounds of [0, 0.1], the sensitivity is 0.1. But adding a single record can move the total sum from 41.95 to 42.05; once truncated, the result goes from 41 to 42: the sensitivity gets multiplied by a factor of 10. Oops!
The cleanest solution is likely to use an option to prevent the binding library
from automatically converting inputs. Alternatively, declaring the double
binding first would also solve the issue: all the integer types would still hit
the first pass, while np.float32 would be converted to double (a lossless
conversion) in the second pass.
If you liked this article, you may enjoy the previous one on the same theme!
