Returning a partial object on error is an invitation for a careless caller to leak memory. We already fixed things in an earlier patch to guarantee NULL if visit_start fails ("qapi: Guarantee NULL obj on input visitor callback error"), but that does not help the case where visit_start succeeds but some other failure happens before visit_end, such that we leak a partially constructed object outside visit_type_FOO(). As no one outside the testsuite was actually relying on these semantics, it is cleaner to just document and guarantee that ALL pointer-based visit_type_FOO() functions always leave a safe value in *obj during an input visitor (either the new object on success, or NULL if an error is encountered), so callers can now unconditionally use qapi_free_FOO() to clean up regardless of whether an error occurred.
The decision is done by adding visit_is_input(), then updating the generated code to check if additional cleanup is needed based on the type of visitor in use. Note that we still leave *obj unchanged after a scalar-based visit_type_FOO(); I did not feel like auditing all uses of visit_type_Enum() to see if the callers would tolerate a specific sentinel value (not to mention having to decide whether it would be better to use 0 or ENUM__MAX as that sentinel). Signed-off-by: Eric Blake <ebl...@redhat.com> --- v15: rebase, redo semantics on how generated code learns about input visitors, hoist unrelated assertions earlier v14: no change v13: rebase to latest, documentation tweaks v12: rebase to latest, don't modify callback signatures, use newer approach for detecting input visitors, avoid 'allocated' boolean [no v10, v11] v9: fix bug in use of errp v8: rebase to earlier changes v7: rebase to earlier changes, enhance commit message, also fix visit_type_str() and visit_type_any() v6: rebase on top of earlier doc and formatting improvements, mention that *obj can be uninitialized on entry to an input visitor, rework semantics to keep valgrind happy on uninitialized input, break some long lines --- include/qapi/visitor.h | 24 ++++++++++++++++-------- scripts/qapi-visit.py | 22 +++++++++++++--------- qapi/qapi-visit-core.c | 6 ++++++ tests/test-qmp-commands.c | 13 ++++++------- tests/test-qmp-input-strict.c | 17 +++++++---------- tests/test-qmp-input-visitor.c | 10 ++-------- docs/qapi-code-gen.txt | 8 ++++++++ 7 files changed, 58 insertions(+), 42 deletions(-) diff --git a/include/qapi/visitor.h b/include/qapi/visitor.h index 4f2a460..7b97711 100644 --- a/include/qapi/visitor.h +++ b/include/qapi/visitor.h @@ -66,11 +66,14 @@ * member @name is not present, or is present but not the specified * type). * - * FIXME: At present, input visitors may allocate an incomplete *@obj - * even when visit_type_FOO() reports an error. Using an output - * visitor with an incomplete object has undefined behavior; callers - * must call qapi_free_FOO() (which uses the dealloc visitor, and - * safely handles an incomplete object) to avoid a memory leak. + * If an error is detected during visit_type_FOO() with an input + * visitor, then *@obj will be NULL for pointer types, and left + * unchanged for scalar types. Using an output visitor with an + * incomplete object has undefined behavior (other than a special case + * for visit_type_str() treating NULL like ""), while the dealloc + * visitor safely handles incomplete objects. Since input visitors + * never produce an incomplete object, such an object is possible only + * by manual construction. * * For the QAPI object types (structs, unions, and alternates), there * is an additional generated function in qapi-visit.h compatible @@ -105,7 +108,6 @@ * v = ...obtain input visitor... * visit_type_Foo(v, NULL, &f, &err); * if (err) { - * qapi_free_Foo(f); * ...handle error... * } else { * ...use f... @@ -123,7 +125,6 @@ * v = ...obtain input visitor... * visit_type_FooList(v, NULL, &l, &err); * if (err) { - * qapi_free_FooList(l); * ...handle error... * } else { * for ( ; l; l = l->next) { @@ -153,7 +154,9 @@ * helpers that rely on in-tree information to control the walk: * visit_optional() for the 'has_member' field associated with * optional 'member' in the C struct; and visit_next_list() for - * advancing through a FooList linked list. Only the generated + * advancing through a FooList linked list. Similarly, the + * visit_is_input() helper makes it possible to write code that is + * visitor-agnostic everywhere except for cleanup. Only the generated * visit_type functions need to use these helpers. * * It is also possible to use the visitors to do a virtual walk, where @@ -403,6 +406,11 @@ bool visit_optional(Visitor *v, const char *name, bool *present); void visit_type_enum(Visitor *v, const char *name, int *obj, const char *const strings[], Error **errp); +/* + * Check if visitor is an input visitor. + */ +bool visit_is_input(Visitor *v); + /*** Visiting built-in types ***/ /* diff --git a/scripts/qapi-visit.py b/scripts/qapi-visit.py index 8b7efcc..70ea8ca 100644 --- a/scripts/qapi-visit.py +++ b/scripts/qapi-visit.py @@ -108,10 +108,6 @@ out: def gen_visit_list(name, element_type): - # FIXME: if *obj is NULL on entry, and the first visit_next_list() - # assigns to *obj, while a later one fails, we should clean up *obj - # rather than leaving it non-NULL. As currently written, the caller must - # call qapi_free_FOOList() to avoid a memory leak of the partial FOOList. return mcgen(''' void visit_type_%(c_name)s(Visitor *v, const char *name, %(c_name)s **obj, Error **errp) @@ -134,6 +130,10 @@ void visit_type_%(c_name)s(Visitor *v, const char *name, %(c_name)s **obj, Error } visit_end_list(v); + if (err && visit_is_input(v)) { + qapi_free_%(c_name)s(*obj); + *obj = NULL; + } out: error_propagate(errp, err); } @@ -211,20 +211,20 @@ void visit_type_%(c_name)s(Visitor *v, const char *name, %(c_name)s **obj, Error "%(name)s"); } visit_end_alternate(v); + if (err && visit_is_input(v)) { + qapi_free_%(c_name)s(*obj); + *obj = NULL; + } out: error_propagate(errp, err); } ''', - name=name) + name=name, c_name=c_name(name)) return ret def gen_visit_object(name, base, members, variants): - # FIXME: if *obj is NULL on entry, and visit_start_struct() assigns to - # *obj, but then visit_type_FOO_members() fails, we should clean up *obj - # rather than leaving it non-NULL. As currently written, the caller must - # call qapi_free_FOO() to avoid a memory leak of the partial FOO. return mcgen(''' void visit_type_%(c_name)s(Visitor *v, const char *name, %(c_name)s **obj, Error **errp) @@ -245,6 +245,10 @@ void visit_type_%(c_name)s(Visitor *v, const char *name, %(c_name)s **obj, Error visit_check_struct(v, &err); out_obj: visit_end_struct(v); + if (err && visit_is_input(v)) { + qapi_free_%(c_name)s(*obj); + *obj = NULL; + } out: error_propagate(errp, err); } diff --git a/qapi/qapi-visit-core.c b/qapi/qapi-visit-core.c index e6d57f3..b30a22e 100644 --- a/qapi/qapi-visit-core.c +++ b/qapi/qapi-visit-core.c @@ -48,6 +48,7 @@ void visit_end_struct(Visitor *v) v->end_struct(v); } + void visit_start_list(Visitor *v, const char *name, GenericList **list, size_t size, Error **errp) { @@ -98,6 +99,11 @@ bool visit_optional(Visitor *v, const char *name, bool *present) return *present; } +bool visit_is_input(Visitor *v) +{ + return v->type == VISITOR_INPUT; +} + void visit_type_int(Visitor *v, const char *name, int64_t *obj, Error **errp) { assert(obj); diff --git a/tests/test-qmp-commands.c b/tests/test-qmp-commands.c index 597fb44..5c3edd7 100644 --- a/tests/test-qmp-commands.c +++ b/tests/test-qmp-commands.c @@ -228,14 +228,13 @@ static void test_dealloc_partial(void) QDECREF(ud2_dict); } - /* verify partial success */ - assert(ud2 != NULL); - assert(ud2->string0 != NULL); - assert(strcmp(ud2->string0, text) == 0); - assert(ud2->dict1 == NULL); - - /* confirm & release construction error */ + /* verify that visit_type_XXX() cleans up properly on error */ error_free_or_abort(&err); + assert(!ud2); + + /* Manually create a partial object, leaving ud2->dict1 at NULL */ + ud2 = g_new0(UserDefTwo, 1); + ud2->string0 = g_strdup(text); /* tear down partial object */ qapi_free_UserDefTwo(ud2); diff --git a/tests/test-qmp-input-strict.c b/tests/test-qmp-input-strict.c index 2b053a2..4602529 100644 --- a/tests/test-qmp-input-strict.c +++ b/tests/test-qmp-input-strict.c @@ -182,10 +182,7 @@ static void test_validate_fail_struct(TestInputVisitorData *data, visit_type_TestStruct(v, NULL, &p, &err); error_free_or_abort(&err); - if (p) { - g_free(p->string); - } - g_free(p); + g_assert(!p); } static void test_validate_fail_struct_nested(TestInputVisitorData *data, @@ -199,7 +196,7 @@ static void test_validate_fail_struct_nested(TestInputVisitorData *data, visit_type_UserDefTwo(v, NULL, &udp, &err); error_free_or_abort(&err); - qapi_free_UserDefTwo(udp); + g_assert(!udp); } static void test_validate_fail_list(TestInputVisitorData *data, @@ -213,7 +210,7 @@ static void test_validate_fail_list(TestInputVisitorData *data, visit_type_UserDefOneList(v, NULL, &head, &err); error_free_or_abort(&err); - qapi_free_UserDefOneList(head); + g_assert(!head); } static void test_validate_fail_union_native_list(TestInputVisitorData *data, @@ -228,7 +225,7 @@ static void test_validate_fail_union_native_list(TestInputVisitorData *data, visit_type_UserDefNativeListUnion(v, NULL, &tmp, &err); error_free_or_abort(&err); - qapi_free_UserDefNativeListUnion(tmp); + g_assert(!tmp); } static void test_validate_fail_union_flat(TestInputVisitorData *data, @@ -242,7 +239,7 @@ static void test_validate_fail_union_flat(TestInputVisitorData *data, visit_type_UserDefFlatUnion(v, NULL, &tmp, &err); error_free_or_abort(&err); - qapi_free_UserDefFlatUnion(tmp); + g_assert(!tmp); } static void test_validate_fail_union_flat_no_discrim(TestInputVisitorData *data, @@ -257,7 +254,7 @@ static void test_validate_fail_union_flat_no_discrim(TestInputVisitorData *data, visit_type_UserDefFlatUnion2(v, NULL, &tmp, &err); error_free_or_abort(&err); - qapi_free_UserDefFlatUnion2(tmp); + g_assert(!tmp); } static void test_validate_fail_alternate(TestInputVisitorData *data, @@ -271,7 +268,7 @@ static void test_validate_fail_alternate(TestInputVisitorData *data, visit_type_UserDefAlternate(v, NULL, &tmp, &err); error_free_or_abort(&err); - qapi_free_UserDefAlternate(tmp); + g_assert(!tmp); } static void do_test_validate_qmp_introspect(TestInputVisitorData *data, diff --git a/tests/test-qmp-input-visitor.c b/tests/test-qmp-input-visitor.c index 70eec9f..828aaa3 100644 --- a/tests/test-qmp-input-visitor.c +++ b/tests/test-qmp-input-visitor.c @@ -770,18 +770,12 @@ static void test_visitor_in_errors(TestInputVisitorData *data, visit_type_TestStruct(v, NULL, &p, &err); error_free_or_abort(&err); - /* FIXME - a failed parse should not leave a partially-allocated p - * for us to clean up; this could cause callers to leak memory. */ - g_assert(p->string == NULL); - - g_free(p->string); - g_free(p); + g_assert(!p); v = visitor_input_test_init(data, "[ '1', '2', false, '3' ]"); visit_type_strList(v, NULL, &q, &err); error_free_or_abort(&err); - assert(q); - qapi_free_strList(q); + assert(!q); } static void test_visitor_in_wrong_type(TestInputVisitorData *data, diff --git a/docs/qapi-code-gen.txt b/docs/qapi-code-gen.txt index fe61c5c..d7d6987 100644 --- a/docs/qapi-code-gen.txt +++ b/docs/qapi-code-gen.txt @@ -905,6 +905,10 @@ Example: visit_check_struct(v, &err); out_obj: visit_end_struct(v); + if (err && visit_is_input(v)) { + qapi_free_UserDefOne(*obj); + *obj = NULL; + } out: error_propagate(errp, err); } @@ -929,6 +933,10 @@ Example: } visit_end_list(v); + if (err && visit_is_input(v)) { + qapi_free_UserDefOneList(*obj); + *obj = NULL; + } out: error_propagate(errp, err); } -- 2.5.5