res_pjsip_session: segfault on already disconnected session

On heavy loaded system the TCP/TLS incoming calls could be
disconnected by pjproject while these calls are being
processed by asterisk which could use the session's memory pools.
If the session in the disconnected state then the session memory
pools were already freed, so we get segfault.

This patch adds a lifetime control on an INVITE session to pjproject.
The lifetime of the session is manipulated by calling
pjsip_inv_add_ref/pjsip_inv_dec_ref.
This patch uses these functions to inform pjproject that the
session is in use.

This patch adds check if the session state is not disconnected
and also checks if the memory pool is not NULL.

This patch also places tasks 'session_end' and 'session_end_completion'
into session's serializer to avoid race condition.

ASTERISK-26291 #close

Change-Id: I4d28b1fb3b91f0492a911d110049d670fdc3c8d7
changes/33/3833/1
Alexei Gradinari 9 years ago
parent e34f299a96
commit 7bb7f7b9d5

123
configure vendored

@ -935,6 +935,10 @@ PBX_POPT
POPT_DIR
POPT_INCLUDE
POPT_LIB
PBX_PJSIP_INV_SESSION_REF
PJSIP_INV_SESSION_REF_DIR
PJSIP_INV_SESSION_REF_INCLUDE
PJSIP_INV_SESSION_REF_LIB
PBX_PJSIP_EVSUB_GRP_LOCK
PJSIP_EVSUB_GRP_LOCK_DIR
PJSIP_EVSUB_GRP_LOCK_INCLUDE
@ -11001,6 +11005,18 @@ PBX_PJSIP_EVSUB_GRP_LOCK=0
PJSIP_INV_SESSION_REF_DESCRIP="PJSIP INVITE Session Reference Count support"
PJSIP_INV_SESSION_REF_OPTION=pjsip
PJSIP_INV_SESSION_REF_DIR=${PJPROJECT_DIR}
PBX_PJSIP_INV_SESSION_REF=0
POPT_DESCRIP="popt"
POPT_OPTION="popt"
@ -25269,6 +25285,9 @@ $as_echo "#define HAVE_PJSIP_TLS_TRANSPORT_PROTO 1" >>confdefs.h
$as_echo "#define HAVE_PJSIP_EVSUB_GRP_LOCK 1" >>confdefs.h
$as_echo "#define HAVE_PJSIP_INV_SESSION_REF 1" >>confdefs.h
else
if test "x${PBX_PJPROJECT}" != "x1" -a "${USE_PJPROJECT}" != "no"; then
@ -26089,6 +26108,110 @@ _ACEOF
fi
if test "x${PBX_PJSIP_INV_SESSION_REF}" != "x1" -a "${USE_PJSIP_INV_SESSION_REF}" != "no"; then
pbxlibdir=""
# if --with-PJSIP_INV_SESSION_REF=DIR has been specified, use it.
if test "x${PJSIP_INV_SESSION_REF_DIR}" != "x"; then
if test -d ${PJSIP_INV_SESSION_REF_DIR}/lib; then
pbxlibdir="-L${PJSIP_INV_SESSION_REF_DIR}/lib"
else
pbxlibdir="-L${PJSIP_INV_SESSION_REF_DIR}"
fi
fi
pbxfuncname="pjsip_inv_add_ref"
if test "x${pbxfuncname}" = "x" ; then # empty lib, assume only headers
AST_PJSIP_INV_SESSION_REF_FOUND=yes
else
ast_ext_lib_check_save_CFLAGS="${CFLAGS}"
CFLAGS="${CFLAGS} $PJPROJECT_CFLAGS"
as_ac_Lib=`$as_echo "ac_cv_lib_pjsip_${pbxfuncname}" | $as_tr_sh`
{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for ${pbxfuncname} in -lpjsip" >&5
$as_echo_n "checking for ${pbxfuncname} in -lpjsip... " >&6; }
if eval \${$as_ac_Lib+:} false; then :
$as_echo_n "(cached) " >&6
else
ac_check_lib_save_LIBS=$LIBS
LIBS="-lpjsip ${pbxlibdir} $PJPROJECT_LIB $LIBS"
cat confdefs.h - <<_ACEOF >conftest.$ac_ext
/* end confdefs.h. */
/* Override any GCC internal prototype to avoid an error.
Use char because int might match the return type of a GCC
builtin and then its argument prototype would still apply. */
#ifdef __cplusplus
extern "C"
#endif
char ${pbxfuncname} ();
int
main ()
{
return ${pbxfuncname} ();
;
return 0;
}
_ACEOF
if ac_fn_c_try_link "$LINENO"; then :
eval "$as_ac_Lib=yes"
else
eval "$as_ac_Lib=no"
fi
rm -f core conftest.err conftest.$ac_objext \
conftest$ac_exeext conftest.$ac_ext
LIBS=$ac_check_lib_save_LIBS
fi
eval ac_res=\$$as_ac_Lib
{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_res" >&5
$as_echo "$ac_res" >&6; }
if eval test \"x\$"$as_ac_Lib"\" = x"yes"; then :
AST_PJSIP_INV_SESSION_REF_FOUND=yes
else
AST_PJSIP_INV_SESSION_REF_FOUND=no
fi
CFLAGS="${ast_ext_lib_check_save_CFLAGS}"
fi
# now check for the header.
if test "${AST_PJSIP_INV_SESSION_REF_FOUND}" = "yes"; then
PJSIP_INV_SESSION_REF_LIB="${pbxlibdir} -lpjsip $PJPROJECT_LIB"
# if --with-PJSIP_INV_SESSION_REF=DIR has been specified, use it.
if test "x${PJSIP_INV_SESSION_REF_DIR}" != "x"; then
PJSIP_INV_SESSION_REF_INCLUDE="-I${PJSIP_INV_SESSION_REF_DIR}/include"
fi
PJSIP_INV_SESSION_REF_INCLUDE="${PJSIP_INV_SESSION_REF_INCLUDE} $PJPROJECT_CFLAGS"
if test "xpjsip.h" = "x" ; then # no header, assume found
PJSIP_INV_SESSION_REF_HEADER_FOUND="1"
else # check for the header
ast_ext_lib_check_saved_CPPFLAGS="${CPPFLAGS}"
CPPFLAGS="${CPPFLAGS} ${PJSIP_INV_SESSION_REF_INCLUDE}"
ac_fn_c_check_header_mongrel "$LINENO" "pjsip.h" "ac_cv_header_pjsip_h" "$ac_includes_default"
if test "x$ac_cv_header_pjsip_h" = xyes; then :
PJSIP_INV_SESSION_REF_HEADER_FOUND=1
else
PJSIP_INV_SESSION_REF_HEADER_FOUND=0
fi
CPPFLAGS="${ast_ext_lib_check_saved_CPPFLAGS}"
fi
if test "x${PJSIP_INV_SESSION_REF_HEADER_FOUND}" = "x0" ; then
PJSIP_INV_SESSION_REF_LIB=""
PJSIP_INV_SESSION_REF_INCLUDE=""
else
if test "x${pbxfuncname}" = "x" ; then # only checking headers -> no library
PJSIP_INV_SESSION_REF_LIB=""
fi
PBX_PJSIP_INV_SESSION_REF=1
cat >>confdefs.h <<_ACEOF
#define HAVE_PJSIP_INV_SESSION_REF 1
_ACEOF
fi
fi
fi
fi
fi

@ -487,6 +487,7 @@ AST_EXT_LIB_SETUP_OPTIONAL([PJ_SSL_CERT_LOAD_FROM_FILES2], [pj_ssl_cert_load_fro
AST_EXT_LIB_SETUP_OPTIONAL([PJSIP_EXTERNAL_RESOLVER], [PJSIP External Resolver Support], [PJPROJECT], [pjsip])
AST_EXT_LIB_SETUP_OPTIONAL([PJSIP_TLS_TRANSPORT_PROTO], [PJSIP TLS Transport proto field support], [PJPROJECT], [pjsip])
AST_EXT_LIB_SETUP_OPTIONAL([PJSIP_EVSUB_GRP_LOCK], [PJSIP EVSUB Group Lock support], [PJPROJECT], [pjsip])
AST_EXT_LIB_SETUP_OPTIONAL([PJSIP_INV_SESSION_REF], [PJSIP INVITE Session Reference Count support], [PJPROJECT], [pjsip])
AST_EXT_LIB_SETUP([POPT], [popt], [popt])
AST_EXT_LIB_SETUP([PORTAUDIO], [PortAudio], [portaudio])
@ -2210,6 +2211,7 @@ if test "$USE_PJPROJECT" != "no" ; then
CPPFLAGS="${saved_cppflags}"
AST_EXT_LIB_CHECK([PJSIP_EVSUB_GRP_LOCK], [pjsip], [pjsip_evsub_add_ref], [pjsip.h], [$PJPROJECT_LIB], [$PJPROJECT_CFLAGS])
AST_EXT_LIB_CHECK([PJSIP_INV_SESSION_REF], [pjsip], [pjsip_inv_add_ref], [pjsip.h], [$PJPROJECT_LIB], [$PJPROJECT_CFLAGS])
fi
fi

@ -598,6 +598,9 @@
/* Define if your system has pjsip_get_dest_info declared. */
#undef HAVE_PJSIP_GET_DEST_INFO
/* Define if your system has PJSIP_INV_SESSION_REF */
#undef HAVE_PJSIP_INV_SESSION_REF
/* Define if your system has the PJSIP_REPLACE_MEDIA_STREAM headers. */
#undef HAVE_PJSIP_REPLACE_MEDIA_STREAM

@ -213,6 +213,11 @@ static int handle_incoming_sdp(struct ast_sip_session *session, const pjmedia_sd
int i;
int handled = 0;
if (session->inv_session && session->inv_session->state == PJSIP_INV_STATE_DISCONNECTED) {
ast_log(LOG_ERROR, "Failed to handle incoming SDP. Session has been already disconnected\n");
return -1;
}
for (i = 0; i < sdp->media_count; ++i) {
/* See if there are registered handlers for this media stream type */
char media[20];
@ -2087,6 +2092,16 @@ static int new_invite(void *data)
* so that we will be notified so we can destroy the session properly
*/
if (invite->session->inv_session->state == PJSIP_INV_STATE_DISCONNECTED) {
ast_log(LOG_ERROR, "Session already DISCONNECTED [reason=%d (%s)]\n",
invite->session->inv_session->cause,
pjsip_get_status_text(invite->session->inv_session->cause)->ptr);
#ifdef HAVE_PJSIP_INV_SESSION_REF
pjsip_inv_dec_ref(invite->session->inv_session);
#endif
return -1;
}
switch (get_destination(invite->session, invite->rdata)) {
case SIP_GET_DEST_EXTEN_FOUND:
/* Things worked. Keep going */
@ -2097,7 +2112,7 @@ static int new_invite(void *data)
} else {
pjsip_inv_terminate(invite->session->inv_session, 416, PJ_TRUE);
}
return 0;
goto end;
case SIP_GET_DEST_EXTEN_NOT_FOUND:
case SIP_GET_DEST_EXTEN_PARTIAL:
default:
@ -2110,7 +2125,7 @@ static int new_invite(void *data)
} else {
pjsip_inv_terminate(invite->session->inv_session, 404, PJ_TRUE);
}
return 0;
goto end;
};
if ((sdp_info = pjsip_rdata_get_sdp_info(invite->rdata)) && (sdp_info->sdp_err == PJ_SUCCESS) && sdp_info->sdp) {
@ -2120,7 +2135,7 @@ static int new_invite(void *data)
} else {
pjsip_inv_terminate(invite->session->inv_session, 488, PJ_TRUE);
}
return 0;
goto end;
}
/* We are creating a local SDP which is an answer to their offer */
local = create_local_sdp(invite->session->inv_session, invite->session, sdp_info->sdp);
@ -2136,7 +2151,7 @@ static int new_invite(void *data)
} else {
pjsip_inv_terminate(invite->session->inv_session, 500, PJ_TRUE);
}
return 0;
goto end;
} else {
pjsip_inv_set_local_sdp(invite->session->inv_session, local);
pjmedia_sdp_neg_set_prefer_remote_codec_order(invite->session->inv_session->neg, PJ_FALSE);
@ -2153,12 +2168,16 @@ static int new_invite(void *data)
/* At this point, we've verified what we can, so let's go ahead and send a 100 Trying out */
if (pjsip_inv_initial_answer(invite->session->inv_session, invite->rdata, 100, NULL, NULL, &tdata) != PJ_SUCCESS) {
pjsip_inv_terminate(invite->session->inv_session, 500, PJ_TRUE);
return 0;
goto end;
}
ast_sip_session_send_response(invite->session, tdata);
handle_incoming_request(invite->session, invite->rdata);
end:
#ifdef HAVE_PJSIP_INV_SESSION_REF
pjsip_inv_dec_ref(invite->session->inv_session);
#endif
return 0;
}
@ -2179,6 +2198,20 @@ static void handle_new_invite_request(pjsip_rx_data *rdata)
return;
}
#ifdef HAVE_PJSIP_INV_SESSION_REF
if (pjsip_inv_add_ref(inv_session) != PJ_SUCCESS) {
ast_log(LOG_ERROR, "Can't increase the session reference counter\n");
if (inv_session->state != PJSIP_INV_STATE_DISCONNECTED) {
if (pjsip_inv_initial_answer(inv_session, rdata, 500, NULL, NULL, &tdata) == PJ_SUCCESS) {
pjsip_inv_terminate(inv_session, 500, PJ_FALSE);
} else {
internal_pjsip_inv_send_msg(inv_session, endpoint->transport, tdata);
}
}
return;
}
#endif
session = ast_sip_session_alloc(endpoint, NULL, inv_session, rdata);
if (!session) {
if (pjsip_inv_initial_answer(inv_session, rdata, 500, NULL, NULL, &tdata) == PJ_SUCCESS) {
@ -2186,6 +2219,9 @@ static void handle_new_invite_request(pjsip_rx_data *rdata)
} else {
internal_pjsip_inv_send_msg(inv_session, endpoint->transport, tdata);
}
#ifdef HAVE_PJSIP_INV_SESSION_REF
pjsip_inv_dec_ref(inv_session);
#endif
return;
}
@ -2196,6 +2232,9 @@ static void handle_new_invite_request(pjsip_rx_data *rdata)
} else {
internal_pjsip_inv_send_msg(inv_session, endpoint->transport, tdata);
}
#ifdef HAVE_PJSIP_INV_SESSION_REF
pjsip_inv_dec_ref(inv_session);
#endif
ao2_cleanup(invite);
}
ao2_ref(session, -1);
@ -2467,8 +2506,9 @@ static void handle_outgoing(struct ast_sip_session *session, pjsip_tx_data *tdat
}
}
static void session_end(struct ast_sip_session *session)
static int session_end(void *vsession)
{
struct ast_sip_session *session = vsession;
struct ast_sip_session_supplement *iter;
/* Stop the scheduled termination */
@ -2480,6 +2520,7 @@ static void session_end(struct ast_sip_session *session)
iter->session_end(session);
}
}
return 0;
}
/*!
@ -2617,8 +2658,11 @@ static void session_inv_on_state_changed(pjsip_inv_session *inv, pjsip_event *e)
}
if (inv->state == PJSIP_INV_STATE_DISCONNECTED) {
if (ast_sip_push_task(session->serializer, session_end, session)) {
/* Do it anyway even though this is not the right thread. */
session_end(session);
}
}
}
static void session_inv_on_new_session(pjsip_inv_session *inv, pjsip_event *e)
@ -2784,7 +2828,11 @@ static void session_inv_on_tsx_state_changed(pjsip_inv_session *inv, pjsip_trans
* Pass the session ref held by session->inv_session to
* session_end_completion().
*/
if (session
&& ast_sip_push_task(session->serializer, session_end_completion, session)) {
/* Do it anyway even though this is not the right thread. */
session_end_completion(session);
}
return;
}
break;
@ -2909,7 +2957,12 @@ static struct pjmedia_sdp_session *create_local_sdp(pjsip_inv_session *inv, stru
static const pj_str_t STR_IP6 = { "IP6", 3 };
pjmedia_sdp_session *local;
if (!(local = PJ_POOL_ZALLOC_T(inv->pool_prov, pjmedia_sdp_session))) {
if (inv->state == PJSIP_INV_STATE_DISCONNECTED) {
ast_log(LOG_ERROR, "Failed to create session SDP. Session has been already disconnected\n");
return NULL;
}
if (!inv->pool_prov || !(local = PJ_POOL_ZALLOC_T(inv->pool_prov, pjmedia_sdp_session))) {
return NULL;
}

@ -45,4 +45,5 @@ AC_DEFUN([PJPROJECT_CONFIGURE],
PJPROJECT_SYMBOL_CHECK([PJSIP_EXTERNAL_RESOLVER], [pjsip_endpt_set_ext_resolver], [pjsip.h])
AC_DEFINE([HAVE_PJSIP_TLS_TRANSPORT_PROTO], 1, [Define if your system has PJSIP_TLS_TRANSPORT_PROTO])
AC_DEFINE([HAVE_PJSIP_EVSUB_GRP_LOCK], 1, [Define if your system has PJSIP_EVSUB_GRP_LOCK])
AC_DEFINE([HAVE_PJSIP_INV_SESSION_REF], 1, [Define if your system has PJSIP_INV_SESSION_REF])
])

@ -0,0 +1,212 @@
When a transport error occured on an INVITE session
the stack calls on_tsx_state_changed with new state
PJSIP_INV_STATE_DISCONNECTED and immediately destroys
the INVITE session.
At the same time this INVITE session could being processed
on another thread. This thread could use the session's
memory pools which were already freed, so we get segfault.
This patch adds a reference counter and new functions:
pjsip_inv_add_ref and pjsip_inv_dec_ref.
The INVITE session is destroyed only when the reference
counter has reached zero.
To avoid race condition an application should call
pjsip_inv_add_ref/pjsip_inv_dec_ref.
Index: pjsip/include/pjsip-ua/sip_inv.h
===================================================================
--- a/pjsip/include/pjsip-ua/sip_inv.h (revision 5434)
+++ b/pjsip/include/pjsip-ua/sip_inv.h (revision 5435)
@@ -383,6 +383,11 @@
* Other applications that want to use these pools must understand
* that the flip-flop pool's lifetimes are synchronized to the
* SDP offer-answer negotiation.
+ *
+ * The lifetime of this session is controlled by the reference counter in this
+ * structure, which is manipulated by calling #pjsip_inv_add_ref and
+ * #pjsip_inv_dec_ref. When the reference counter has reached zero, then
+ * this session will be destroyed.
*/
struct pjsip_inv_session
{
@@ -412,6 +417,7 @@
struct pjsip_timer *timer; /**< Session Timers. */
pj_bool_t following_fork; /**< Internal, following
forked media? */
+ pj_atomic_t *ref_cnt; /**< Reference counter. */
};
@@ -631,6 +637,30 @@
/**
+ * Add reference counter to the INVITE session. The reference counter controls
+ * the life time of the session, ie. when the counter reaches zero, then it
+ * will be destroyed.
+ *
+ * @param inv The INVITE session.
+ * @return PJ_SUCCESS if the INVITE session reference counter
+ * was increased.
+ */
+PJ_DECL(pj_status_t) pjsip_inv_add_ref( pjsip_inv_session *inv );
+
+/**
+ * Decrement reference counter of the INVITE session.
+ * When the session is no longer used, it will be destroyed and
+ * caller is informed with PJ_EGONE return status.
+ *
+ * @param inv The INVITE session.
+ * @return PJ_SUCCESS if the INVITE session reference counter
+ * was decreased. A status PJ_EGONE will be returned to
+ * inform that session is destroyed.
+ */
+PJ_DECL(pj_status_t) pjsip_inv_dec_ref( pjsip_inv_session *inv );
+
+
+/**
* Forcefully terminate and destroy INVITE session, regardless of
* the state of the session. Note that this function should only be used
* when there is failure in the INVITE session creation. After the
Index: pjsip/src/pjsip-ua/sip_inv.c
===================================================================
--- a/pjsip/src/pjsip-ua/sip_inv.c (revision 5434)
+++ b/pjsip/src/pjsip-ua/sip_inv.c (revision 5435)
@@ -195,6 +195,65 @@
}
/*
+ * Add reference to INVITE session.
+ */
+PJ_DEF(pj_status_t) pjsip_inv_add_ref( pjsip_inv_session *inv )
+{
+ PJ_ASSERT_RETURN(inv && inv->ref_cnt, PJ_EINVAL);
+
+ pj_atomic_inc(inv->ref_cnt);
+
+ return PJ_SUCCESS;
+}
+
+static void inv_session_destroy(pjsip_inv_session *inv)
+{
+ if (inv->last_ack) {
+ pjsip_tx_data_dec_ref(inv->last_ack);
+ inv->last_ack = NULL;
+ }
+ if (inv->invite_req) {
+ pjsip_tx_data_dec_ref(inv->invite_req);
+ inv->invite_req = NULL;
+ }
+ if (inv->pending_bye) {
+ pjsip_tx_data_dec_ref(inv->pending_bye);
+ inv->pending_bye = NULL;
+ }
+ pjsip_100rel_end_session(inv);
+ pjsip_timer_end_session(inv);
+ pjsip_dlg_dec_session(inv->dlg, &mod_inv.mod);
+
+ /* Release the flip-flop pools */
+ pj_pool_release(inv->pool_prov);
+ inv->pool_prov = NULL;
+ pj_pool_release(inv->pool_active);
+ inv->pool_active = NULL;
+
+ pj_atomic_destroy(inv->ref_cnt);
+ inv->ref_cnt = NULL;
+}
+
+/*
+ * Decrease INVITE session reference, destroy it when the reference count
+ * reaches zero.
+ */
+PJ_DEF(pj_status_t) pjsip_inv_dec_ref( pjsip_inv_session *inv )
+{
+ pj_atomic_value_t ref_cnt;
+
+ PJ_ASSERT_RETURN(inv && inv->ref_cnt, PJ_EINVAL);
+
+ ref_cnt = pj_atomic_dec_and_get(inv->ref_cnt);
+ pj_assert( ref_cnt >= 0);
+ if (ref_cnt == 0) {
+ inv_session_destroy(inv);
+ return PJ_EGONE;
+ }
+ return PJ_SUCCESS;
+}
+
+/*
* Set session state.
*/
static void inv_set_state(pjsip_inv_session *inv, pjsip_inv_state state,
@@ -261,27 +320,7 @@
if (inv->state == PJSIP_INV_STATE_DISCONNECTED &&
prev_state != PJSIP_INV_STATE_DISCONNECTED)
{
- if (inv->last_ack) {
- pjsip_tx_data_dec_ref(inv->last_ack);
- inv->last_ack = NULL;
- }
- if (inv->invite_req) {
- pjsip_tx_data_dec_ref(inv->invite_req);
- inv->invite_req = NULL;
- }
- if (inv->pending_bye) {
- pjsip_tx_data_dec_ref(inv->pending_bye);
- inv->pending_bye = NULL;
- }
- pjsip_100rel_end_session(inv);
- pjsip_timer_end_session(inv);
- pjsip_dlg_dec_session(inv->dlg, &mod_inv.mod);
-
- /* Release the flip-flop pools */
- pj_pool_release(inv->pool_prov);
- inv->pool_prov = NULL;
- pj_pool_release(inv->pool_active);
- inv->pool_active = NULL;
+ pjsip_inv_dec_ref(inv);
}
}
@@ -838,6 +877,12 @@
inv = PJ_POOL_ZALLOC_T(dlg->pool, pjsip_inv_session);
pj_assert(inv != NULL);
+ status = pj_atomic_create(dlg->pool, 0, &inv->ref_cnt);
+ if (status != PJ_SUCCESS) {
+ pjsip_dlg_dec_lock(dlg);
+ return status;
+ }
+
inv->pool = dlg->pool;
inv->role = PJSIP_ROLE_UAC;
inv->state = PJSIP_INV_STATE_NULL;
@@ -881,6 +926,7 @@
pjsip_100rel_attach(inv);
/* Done */
+ pjsip_inv_add_ref(inv);
*p_inv = inv;
pjsip_dlg_dec_lock(dlg);
@@ -1471,6 +1517,12 @@
inv = PJ_POOL_ZALLOC_T(dlg->pool, pjsip_inv_session);
pj_assert(inv != NULL);
+ status = pj_atomic_create(dlg->pool, 0, &inv->ref_cnt);
+ if (status != PJ_SUCCESS) {
+ pjsip_dlg_dec_lock(dlg);
+ return status;
+ }
+
inv->pool = dlg->pool;
inv->role = PJSIP_ROLE_UAS;
inv->state = PJSIP_INV_STATE_NULL;
@@ -1540,6 +1592,7 @@
}
/* Done */
+ pjsip_inv_add_ref(inv);
pjsip_dlg_dec_lock(dlg);
*p_inv = inv;
Loading…
Cancel
Save