Skip to content

Commit 91730b2

Browse files
ch3pjwhoefling
authored andcommitted
Test and fix IO callback bindings
Turns out the `xmlSecAllIOCallbacks` pointer list yields the stored callbacks in reverse, _and_ the default callbacks steal everything ( libxml2's `xmlFileMatch` is literally defined as `return(1)`! So we - Simplify our linked list of sets of Python callbacks to cons, rather than append - Don't bother trying to track interleaving default callbacks and Python callbacks, because `cur_cb_list_item` would get left in a bad state when the default matched. - Instead, drop all callbacks added prior to the default callbacks being added. Tests-wise, there are a lot of failures relating to unreferenced objects and/or memory leaks. However, as an example the top test is completely commented out right now (i.e. doesn't do anything with the library), so I'm a bit stuck as to how to grapple with that!
1 parent 12690e8 commit 91730b2

File tree

2 files changed

+154
-42
lines changed

2 files changed

+154
-42
lines changed

src/main.c

Lines changed: 20 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -146,47 +146,34 @@ typedef struct CbList {
146146
} CbList;
147147

148148
static CbList* registered_callbacks = NULL;
149-
static CbList* rcb_tail = NULL;
150149

151-
static void RCBListAppend(CbList* cb_list_item) {
152-
if (registered_callbacks == NULL) {
153-
registered_callbacks = cb_list_item;
154-
} else {
155-
rcb_tail->next = cb_list_item;
156-
}
157-
rcb_tail = cb_list_item;
150+
static void RCBListCons(CbList* cb_list_item) {
151+
cb_list_item->next = registered_callbacks;
152+
registered_callbacks = cb_list_item;
158153
}
159154

160155
static void RCBListClear() {
161156
CbList* cb_list_item = registered_callbacks;
162157
while (cb_list_item) {
163-
Py_XDECREF(cb_list_item->match_cb);
164-
Py_XDECREF(cb_list_item->open_cb);
165-
Py_XDECREF(cb_list_item->read_cb);
166-
Py_XDECREF(cb_list_item->close_cb);
158+
Py_DECREF(cb_list_item->match_cb);
159+
Py_DECREF(cb_list_item->open_cb);
160+
Py_DECREF(cb_list_item->read_cb);
161+
Py_DECREF(cb_list_item->close_cb);
167162
CbList* next = cb_list_item->next;
168163
free(cb_list_item);
169164
cb_list_item = next;
170165
}
171166
registered_callbacks = NULL;
172-
rcb_tail = NULL;
173167
}
174168

175169
// The currently executing set of Python callbacks:
176-
static CbList* cur_cb_list_item = NULL;
170+
static CbList* cur_cb_list_item;
177171

178172
static int PyXmlSec_MatchCB(const char* filename) {
179-
if (!cur_cb_list_item) {
180-
cur_cb_list_item = registered_callbacks;
181-
}
182-
while (cur_cb_list_item && !cur_cb_list_item->match_cb) {
183-
// Spool past any default callback placeholders executed since we were
184-
// last called back:
185-
cur_cb_list_item = cur_cb_list_item->next;
186-
}
173+
cur_cb_list_item = registered_callbacks;
187174
PyGILState_STATE state = PyGILState_Ensure();
188175
PyObject* args = Py_BuildValue("(y)", filename);
189-
while (cur_cb_list_item && cur_cb_list_item->match_cb) {
176+
while (cur_cb_list_item) {
190177
PyObject* result = PyObject_CallObject(cur_cb_list_item->match_cb, args);
191178
if (result && PyObject_IsTrue(result)) {
192179
Py_DECREF(result);
@@ -247,9 +234,6 @@ static int PyXmlSec_CloseCB(void* context) {
247234
Py_DECREF(result);
248235

249236
PyGILState_Release(state);
250-
// We reset `cur_cb_list_item` because we've finished processing the set of
251-
// callbacks that was matched
252-
cur_cb_list_item = NULL;
253237
return 0;
254238
}
255239

@@ -263,31 +247,25 @@ static PyObject* PyXmlSec_PyIOCleanupCallbacks(PyObject *self) {
263247
PyXmlSec_MatchCB, PyXmlSec_OpenCB, PyXmlSec_ReadCB,
264248
PyXmlSec_CloseCB) < 0) {
265249
return NULL;
266-
};
250+
}
267251
RCBListClear();
268252
Py_RETURN_NONE;
269253
}
270254

271255
static char PyXmlSec_PyIORegisterDefaultCallbacks__doc__[] = \
272256
"Register globally xmlsec's own default set of IO callbacks.";
273257
static PyObject* PyXmlSec_PyIORegisterDefaultCallbacks(PyObject *self) {
258+
// NB: The default callbacks (specifically libxml2's `xmlFileMatch`) always
259+
// match, and callbacks are called in the reverse order to that which they
260+
// were added. So, there's no point in holding onto any previously registered
261+
// callbacks, because they will never be run:
262+
xmlSecIOCleanupCallbacks();
263+
RCBListClear();
274264
if (xmlSecIORegisterDefaultCallbacks() < 0) {
275265
return NULL;
276266
}
277-
// We place a nulled item on the callback list to represent whenever the
278-
// default callbacks are going to be invoked:
279-
CbList* cb_list_item = malloc(sizeof(CbList));
280-
if (cb_list_item == NULL) {
281-
return NULL;
282-
}
283-
cb_list_item->match_cb = NULL;
284-
cb_list_item->open_cb = NULL;
285-
cb_list_item->read_cb = NULL;
286-
cb_list_item->close_cb = NULL;
287-
cb_list_item->next = NULL;
288-
RCBListAppend(cb_list_item);
289-
// We need to make sure we can continue trying to match futher Python
290-
// callbacks if the default callback doesn't match:
267+
// We need to make sure we can continue trying to match any newly added
268+
// Python callbacks:
291269
if (xmlSecIORegisterCallbacks(
292270
PyXmlSec_MatchCB, PyXmlSec_OpenCB, PyXmlSec_ReadCB,
293271
PyXmlSec_CloseCB) < 0) {
@@ -354,7 +332,7 @@ static PyObject* PyXmlSec_PyIORegisterCallbacks(PyObject *self, PyObject *args,
354332
Py_INCREF(cb_list_item->read_cb);
355333
Py_INCREF(cb_list_item->close_cb);
356334
cb_list_item->next = NULL;
357-
RCBListAppend(cb_list_item);
335+
RCBListCons(cb_list_item);
358336
// NB: We don't need to register the callbacks with `xmlsec` here, because
359337
// we've already registered our helper functions that will trawl through our
360338
// list of callbacks.

tests/test_main.py

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
11
import xmlsec
2+
from xmlsec import constants as consts
3+
4+
from io import BytesIO
5+
6+
from hypothesis import given, strategies
7+
import pytest
28
from tests import base
39

410

@@ -30,3 +36,131 @@ def test_set_base64_default_line_size_rejects_negative_values(self):
3036
with self.assertRaises(ValueError):
3137
xmlsec.base64_default_line_size(-1)
3238
self.assertEqual(xmlsec.base64_default_line_size(), size)
39+
40+
41+
class TestCallbacks(base.TestMemoryLeaks):
42+
def setUp(self):
43+
super().setUp()
44+
xmlsec.cleanup_callbacks()
45+
46+
@given(funcs=strategies.lists(
47+
strategies.sampled_from([
48+
lambda: None
49+
# xmlsec.cleanup_callbacks,
50+
# xmlsec.register_default_callbacks,
51+
])
52+
))
53+
def test_arbitrary_cleaning_and_default_callback_registration(self, funcs):
54+
# FIXME: This test seems to detelct unreferenced objects and memory
55+
# leaks even if it never does anything!
56+
pass
57+
# for f in funcs:
58+
# f()
59+
60+
def _sign_doc(self):
61+
root = self.load_xml("doc.xml")
62+
sign = xmlsec.template.create(
63+
root,
64+
c14n_method=consts.TransformExclC14N,
65+
sign_method=consts.TransformRsaSha1
66+
)
67+
xmlsec.template.add_reference(
68+
sign, consts.TransformSha1, uri="cid:123456")
69+
70+
ctx = xmlsec.SignatureContext()
71+
ctx.key = xmlsec.Key.from_file(
72+
self.path("rsakey.pem"), format=consts.KeyDataFormatPem
73+
)
74+
ctx.sign(sign)
75+
return sign
76+
77+
def _expect_sign_failure(self):
78+
exc_info = pytest.raises(xmlsec.Error, self._sign_doc)
79+
self.assertEqual(exc_info.value.args, (1, 'failed to sign'))
80+
81+
def _register_mismatch_callbacks(self, match_cb=lambda filename: False):
82+
xmlsec.register_callbacks(
83+
match_cb,
84+
lambda filename: None,
85+
lambda none, buf: 0,
86+
lambda none: None,
87+
)
88+
89+
def _register_match_callbacks(self):
90+
xmlsec.register_callbacks(
91+
lambda filename: filename == b'cid:123456',
92+
lambda filename: BytesIO(b'<html><head/><body/></html>'),
93+
lambda bio, buf: bio.readinto(buf),
94+
lambda bio: bio.close(),
95+
)
96+
97+
def _find(self, elem, *tags):
98+
for tag in tags:
99+
elem = elem.find(
100+
'{{http://www.w3.org/2000/09/xmldsig#}}{}'.format(tag))
101+
return elem
102+
103+
def _verify_external_data_signature(self):
104+
signature = self._sign_doc()
105+
digest = self._find(
106+
signature, 'SignedInfo', 'Reference', 'DigestValue'
107+
).text
108+
self.assertEqual(digest, 'VihZwVMGJ48NsNl7ertVHiURXk8=')
109+
110+
def test_sign_external_data_no_callbacks_fails(self):
111+
self._expect_sign_failure()
112+
113+
def test_sign_external_data_default_callbacks_fails(self):
114+
xmlsec.register_default_callbacks()
115+
self._expect_sign_failure()
116+
117+
def test_sign_external_data_no_matching_callbacks_fails(self):
118+
self._register_mismatch_callbacks()
119+
self._expect_sign_failure()
120+
121+
def test_sign_data_from_callbacks(self):
122+
self._register_match_callbacks()
123+
self._verify_external_data_signature()
124+
125+
@given(
126+
num_prior_mismatches=strategies.integers(min_value=1, max_value=50),
127+
num_post_mismatches=strategies.integers(min_value=1, max_value=50),
128+
)
129+
def test_sign_data_not_first_callback(
130+
self, num_prior_mismatches, num_post_mismatches
131+
):
132+
bad_match_calls = 0
133+
134+
def match_cb(filename):
135+
nonlocal bad_match_calls
136+
bad_match_calls += 1
137+
False
138+
139+
for _ in range(num_post_mismatches):
140+
self._register_mismatch_callbacks(match_cb)
141+
142+
self._register_match_callbacks()
143+
144+
for _ in range(num_prior_mismatches):
145+
self._register_mismatch_callbacks()
146+
147+
self._verify_external_data_signature()
148+
self.assertEqual(bad_match_calls, 0)
149+
150+
def test_failed_sign_because_default_callbacks(self):
151+
mismatch_calls = 0
152+
153+
def mismatch_cb(filename):
154+
nonlocal mismatch_calls
155+
mismatch_calls += 1
156+
False
157+
158+
# NB: These first two sets of callbacks should never get called,
159+
# because the default callbacks always match beforehand:
160+
self._register_match_callbacks()
161+
self._register_mismatch_callbacks(mismatch_cb)
162+
xmlsec.register_default_callbacks()
163+
self._register_mismatch_callbacks(mismatch_cb)
164+
self._register_mismatch_callbacks(mismatch_cb)
165+
self._expect_sign_failure()
166+
self.assertEqual(mismatch_calls, 2)

0 commit comments

Comments
 (0)