Skip to content

Commit 644103a

Browse files
authored
feat(native): Improve safety for Python into CLRepr conversion (#9984)
1 parent 7e6037c commit 644103a

File tree

3 files changed

+95
-23
lines changed

3 files changed

+95
-23
lines changed

packages/cubejs-backend-native/src/cross/clrepr_python.rs

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
use crate::cross::clrepr::CLReprObject;
22
use crate::cross::{CLRepr, CLReprObjectKind, StringType};
3-
use pyo3::exceptions::{PyNotImplementedError, PyTypeError};
3+
use crate::python::utils::PyAnyHelpers;
4+
use pyo3::exceptions::{PyException, PyNotImplementedError, PyTypeError};
45
use pyo3::types::{
5-
PyBool, PyComplex, PyDate, PyDict, PyFloat, PyFrame, PyFunction, PyInt, PyList, PySequence,
6-
PySet, PyString, PyTraceback, PyTuple,
6+
PyBool, PyComplex, PyDate, PyDateTime, PyDelta, PyDict, PyFloat, PyFrame, PyFrozenSet,
7+
PyFunction, PyInt, PyList, PySequence, PySet, PyString, PyTraceback, PyTuple,
78
};
89
use pyo3::{Py, PyAny, PyErr, PyObject, Python, ToPyObject};
910

@@ -12,7 +13,7 @@ pub enum PythonRef {
1213
PyObject(PyObject),
1314
PyFunction(Py<PyFunction>),
1415
/// Special type to transfer functions through JavaScript
15-
/// In JS it's an external object. It's not the same as Function.
16+
/// In JS it's an external object.
1617
PyExternalFunction(Py<PyFunction>),
1718
}
1819

@@ -87,10 +88,28 @@ impl CLRepr {
8788
return Err(PyErr::new::<PyTypeError, _>(
8889
"Unable to represent PyComplex type as CLR from Python".to_string(),
8990
));
91+
} else if v.get_type().is_subclass_of::<PyDateTime>()? {
92+
return Err(PyErr::new::<PyTypeError, _>(
93+
"Unable to represent PyDateTime type as CLR from Python".to_string(),
94+
));
9095
} else if v.get_type().is_subclass_of::<PyDate>()? {
9196
return Err(PyErr::new::<PyTypeError, _>(
9297
"Unable to represent PyDate type as CLR from Python".to_string(),
9398
));
99+
} else if v.get_type().is_subclass_of::<PyFrozenSet>()? {
100+
let set = v.downcast::<PyFrozenSet>()?;
101+
102+
return Err(PyErr::new::<PyTypeError, _>(format!(
103+
"Unable to represent PyFrozenSet type as CLR from Python, value: {:?}",
104+
set
105+
)));
106+
} else if v.get_type().is_subclass_of::<PyException>()? {
107+
let exception = v.downcast::<PyException>()?;
108+
109+
return Err(PyErr::new::<PyTypeError, _>(format!(
110+
"Unable to represent PyException type as CLR from Python, value: {:?}",
111+
exception
112+
)));
94113
} else if v.get_type().is_subclass_of::<PyFrame>()? {
95114
let frame = v.downcast::<PyFrame>()?;
96115

@@ -105,17 +124,22 @@ impl CLRepr {
105124
"Unable to represent PyTraceback type as CLR from Python, value: {:?}",
106125
trb
107126
)));
108-
} else {
109-
let is_sequence = unsafe { pyo3::ffi::PySequence_Check(v.as_ptr()) == 1 };
110-
if is_sequence {
111-
let seq = v.downcast::<PySequence>()?;
112-
113-
return Err(PyErr::new::<PyTypeError, _>(format!(
114-
"Unable to represent PySequence type as CLR from Python, value: {:?}",
115-
seq
116-
)));
117-
}
127+
} else if v.get_type().is_subclass_of::<PyDelta>()? {
128+
let delta = v.downcast::<PyDelta>()?;
129+
130+
return Err(PyErr::new::<PyTypeError, _>(format!(
131+
"Unable to represent PyDelta type as CLR from Python, value: {:?}",
132+
delta
133+
)));
134+
} else if v.is_sequence()? {
135+
let seq = v.downcast::<PySequence>()?;
118136

137+
return Err(PyErr::new::<PyTypeError, _>(format!(
138+
"Unable to represent PySequence type as CLR from Python, value: {:?}",
139+
seq
140+
)));
141+
} else {
142+
// Fallback to PyObject, it will lead to throw error in the JS side
119143
Self::PythonRef(PythonRef::PyObject(v.into()))
120144
})
121145
}

packages/cubejs-backend-native/src/python/runtime.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::cross::CLRepr;
22
use crate::python::neon_py::*;
3+
use crate::python::utils::PyAnyHelpers;
34
use crate::tokio_runtime_node;
45
use cubesql::CubeError;
56
use log::{error, trace};
@@ -143,8 +144,7 @@ impl PyRuntime {
143144
let py_args = PyTuple::new(py, prep_tuple);
144145
let call_res = fun.call(py, py_args, py_kwargs)?;
145146

146-
let is_coroutine = unsafe { pyo3::ffi::PyCoro_CheckExact(call_res.as_ptr()) == 1 };
147-
if is_coroutine {
147+
if call_res.is_coroutine()? {
148148
let fut = pyo3_asyncio::tokio::into_future(call_res.as_ref(py))?;
149149
Ok(PyScheduledFunResult::Poll(Box::pin(fut)))
150150
} else {

packages/cubejs-backend-native/src/python/utils.rs

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
use crate::cross::*;
2-
use pyo3::exceptions::PyNotImplementedError;
2+
use pyo3::exceptions::{PyNotImplementedError, PySystemError};
33
use pyo3::prelude::*;
44
use pyo3::types::{PyFunction, PyString, PyTuple};
5+
use pyo3::{ffi, AsPyPointer};
6+
use std::ffi::c_int;
57

68
pub fn python_fn_call_sync(py_fun: &Py<PyFunction>, arguments: Vec<CLRepr>) -> PyResult<CLRepr> {
79
Python::with_gil(|py| {
@@ -15,8 +17,7 @@ pub fn python_fn_call_sync(py_fun: &Py<PyFunction>, arguments: Vec<CLRepr>) -> P
1517

1618
let call_res = py_fun.call1(py, tuple)?;
1719

18-
let is_coroutine = unsafe { pyo3::ffi::PyCoro_CheckExact(call_res.as_ptr()) == 1 };
19-
if is_coroutine {
20+
if call_res.is_coroutine()? {
2021
Err(PyErr::new::<PyNotImplementedError, _>(
2122
"Calling function with async response is not supported",
2223
))
@@ -38,8 +39,7 @@ pub fn python_obj_call_sync(py_fun: &PyObject, arguments: Vec<CLRepr>) -> PyResu
3839

3940
let call_res = py_fun.call1(py, tuple)?;
4041

41-
let is_coroutine = unsafe { pyo3::ffi::PyCoro_CheckExact(call_res.as_ptr()) == 1 };
42-
if is_coroutine {
42+
if call_res.is_coroutine()? {
4343
Err(PyErr::new::<PyNotImplementedError, _>(
4444
"Calling object with async response is not supported",
4545
))
@@ -68,8 +68,7 @@ where
6868

6969
let call_res = py_fun.call_method1(py, name, tuple)?;
7070

71-
let is_coroutine = unsafe { pyo3::ffi::PyCoro_CheckExact(call_res.as_ptr()) == 1 };
72-
if is_coroutine {
71+
if call_res.is_coroutine()? {
7372
Err(PyErr::new::<PyNotImplementedError, _>(
7473
"Calling object method with async response is not supported",
7574
))
@@ -78,3 +77,52 @@ where
7877
}
7978
})
8079
}
80+
81+
pub trait PyAnyHelpers {
82+
fn is_sequence(&self) -> PyResult<bool>;
83+
84+
fn is_coroutine(&self) -> PyResult<bool>;
85+
}
86+
87+
pub(crate) fn internal_error_on_minusone(result: c_int) -> PyResult<()> {
88+
if result != -1 {
89+
Ok(())
90+
} else {
91+
Err(PyErr::new::<PySystemError, _>(
92+
"Error on call via ffi, result is -1",
93+
))
94+
}
95+
}
96+
97+
impl<T> PyAnyHelpers for T
98+
where
99+
T: AsPyPointer,
100+
{
101+
fn is_sequence(&self) -> PyResult<bool> {
102+
let ptr = self.as_ptr();
103+
if ptr.is_null() {
104+
return Err(PyErr::new::<PySystemError, _>(
105+
"Unable to verify that object is sequence, because ptr is null",
106+
));
107+
}
108+
109+
let v = unsafe { ffi::PySequence_Check(ptr) };
110+
internal_error_on_minusone(v)?;
111+
112+
Ok(v != 0)
113+
}
114+
115+
fn is_coroutine(&self) -> PyResult<bool> {
116+
let ptr = self.as_ptr();
117+
if ptr.is_null() {
118+
return Err(PyErr::new::<PySystemError, _>(
119+
"Unable to verify that object is coroutine, because ptr is null",
120+
));
121+
}
122+
123+
let v = unsafe { ffi::PyCoro_CheckExact(ptr) };
124+
internal_error_on_minusone(v)?;
125+
126+
Ok(v != 0)
127+
}
128+
}

0 commit comments

Comments
 (0)