Skip to content
26 changes: 18 additions & 8 deletions src/odbc/odbc.c
Original file line number Diff line number Diff line change
Expand Up @@ -872,7 +872,13 @@ odbc_lock_statement(TDS_STMT* stmt)

/* try with MARS */
if (!tds)
{
tds = tds_alloc_additional_socket(dbc_tds->conn);
if (tds)
{
tdsdump_log(TDS_DBG_INFO1, "MARS SID %d allocated new TDSSOCKET\n", tds->sid);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe MARS SID %d allocated for new TDSSOCKET\n ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well, both of them are allocated really (the SID and the TDSSOCKET); how about with instead of for

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It sounds good to me

}
}
}
if (tds) {
tds->query_timeout = (stmt->attr.query_timeout != DEFAULT_QUERY_TIMEOUT) ?
Expand Down Expand Up @@ -922,15 +928,8 @@ odbc_unlock_statement(TDS_STMT* stmt)
tds_set_parent(tds, stmt->dbc);
stmt->tds = NULL;
}
#if ENABLE_ODBC_MARS
} else if (tds) {
if (tds->state == TDS_IDLE || tds->state == TDS_DEAD) {
assert(tds != stmt->dbc->tds_socket);
tds_free_socket(tds);
stmt->tds = NULL;
}
#endif
}
/* NOTE: MARS socket now released when statement freed. */
tds_mutex_unlock(&stmt->dbc->mtx);
}

Expand Down Expand Up @@ -4464,6 +4463,17 @@ odbc_SQLFreeStmt(SQLHSTMT hstmt, SQLUSMALLINT fOption, int force)
tds_free_param_results(stmt->params);
odbc_errs_reset(&stmt->errs);
odbc_unlock_statement(stmt);
#if ENABLE_ODBC_MARS
if ( stmt->tds && stmt->tds != stmt->dbc->tds_socket )
{
tds_free_socket(tds);
stmt->tds = NULL;
tdsdump_log(TDS_DBG_INFO1, "MARS SID %d socket freed\n", tds->sid);
if (!(tds->state == TDS_IDLE || tds->state == TDS_DEAD)) {
tdsdump_log(TDS_DBG_WARN, "MARS SID %d was not idle/dead\n", tds->sid);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These looks like an use after free.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes you're right; will have to reorder those statements (and re-word the messages, or remember the variables being logged)

}
#endif
tds_dstr_free(&stmt->cursor_name);
tds_dstr_free(&stmt->attr.qn_msgtext);
tds_dstr_free(&stmt->attr.qn_options);
Expand Down
6 changes: 5 additions & 1 deletion src/odbc/unittests/common.c
Original file line number Diff line number Diff line change
Expand Up @@ -632,7 +632,11 @@ odbc_command_proc(HSTMT stmt, const char *command, const char *file, int line, c
SQLRETURN ret;
ODBC_BUF *odbc_buf = NULL;

printf("%s\n", command);
if (command[0] == '!')
++command;
else
printf("%s\n", command);

ret = odbc_check_res(file, line, SQLExecDirect(stmt, T(command), SQL_NTS), SQL_HANDLE_STMT, stmt, "odbc_command", res);
ODBC_FREE();
return ret;
Expand Down
61 changes: 49 additions & 12 deletions src/odbc/unittests/mars1.c
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ my_attrs(void)
TEST_MAIN()
{
SQLINTEGER len, out;
int i;
int i, j;
SQLHSTMT stmt1, stmt2;
SQLHSTMT *pcur_stmt = NULL;

Expand Down Expand Up @@ -77,22 +77,61 @@ TEST_MAIN()
AutoCommit(SQL_AUTOCOMMIT_OFF);

/* try to do a select which return a lot of data (to test server didn't cache everything) */
odbc_command("select a.n, b.n, a.v from #mars1 a, #mars1 b order by a.n, b.n");
odbc_command("select a.n, b.n, c.n, a.v from #mars1 a, #mars1 b, #mars1 c order by a.n, b.n, c.n");
CHKFetch("S");

/* try to do other commands */
CHKAllocStmt(&stmt2, "S");
SET_STMT(stmt2);
odbc_command("insert into #mars2 values(1, 'foo')");
SET_STMT(stmt1);

CHKFetch("S");

/* Use a parameterized insert. This causes DONEINPROC to be returned by SQL Server,
* leading to result_type==TDS_CMD_DONE when it's complete. Without the parameter,
* result_type == TDS_DONE_RESULT.
* And in odbc_SQLExecute(), it calls odbc_unlock_statement() for TDS_CMD_DONE, but
* not for TDS_DONE_RESULT. (We don't know why...)
* This means that the stmt->tds TDSSOCKET struct is completely freed after every
* iteration of the insert if and only if it was a parameterized insert. So we need
* to test both parameterized and non-parameterized inserts.
*/
long bind1;
Copy link
Contributor

@freddy77 freddy77 Dec 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The correct type to bind a SQL_C_SLONG is SQLINTEGER. Yes, surely confusing, but I think initial ODBC specifications were written with 16 bits in mind.

char bind2[20] = "parameters";
SQLBindParameter(stmt2, 1, SQL_PARAM_INPUT, SQL_C_SLONG, SQL_INTEGER, 0, 0, &bind1, 0, NULL);
SQLBindParameter(stmt2, 2, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_CHAR, 0, 0, &bind2, 20, NULL);

/* adjust these parameters for memory leak testing */
/* TODO a way for this test to detect memory leak here. */
const int n_iterations = 20; /* E.g. 200000 */
const int freq_parameterized = 2; /* set 1 to parameterize all, INT_MAX for none */

for (i= 1; i <= n_iterations; ++i)
{
SET_STMT(stmt2);

// Test option - force reallocation of socket
// odbc_reset_statement();

if (i % freq_parameterized == 0)
{
bind1 = i;
odbc_command("!insert into #mars2 values(?, ?)");
}
else
odbc_command("!insert into #mars2 values(1, 'foo')");

// Perform several fetches for each insert, so we also test continuing to draw
// further packets of the fetch
SET_STMT(stmt1);
for (j = 0; j < 10; ++j)
{
CHKFetch("S");
}
}
printf("Performed %d inserts while fetching.\n", i - 1);

/* reset statements */
SET_STMT(stmt1);
odbc_reset_statement();
SET_STMT(stmt2);
odbc_reset_statement();

goto done;
/* now to 2 select with prepare/execute */
CHKPrepare(T("select a.n, b.n, a.v from #mars1 a, #mars1 b order by a.n, b.n"), SQL_NTS, "S");
SET_STMT(stmt1);
Expand All @@ -111,11 +150,9 @@ TEST_MAIN()
SET_STMT(stmt1);
CHKFetch("S");
odbc_reset_statement();

done:
EndTransaction(SQL_COMMIT);

/* TODO test receiving large data should not take much memory */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I cannot see the implementation of this in the test.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I replaced that comment with /* TODO a way for this test to detect memory leak here. */ a bit further up -- unless you meant something else? Receiving one row of large data would take as much memory as it takes to process that data, so I took your comment to mean that repeated iterations of the test should not keep increasing memory.


odbc_disconnect();
return 0;
}
Expand Down
15 changes: 12 additions & 3 deletions src/tds/packet.c
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,7 @@ int
tds_read_packet(TDSSOCKET * tds)
{
#if ENABLE_ODBC_MARS
TDS_UINT this_seq;
TDSCONNECTION *conn = tds->conn;

tds_mutex_lock(&conn->list_mtx);
Expand Down Expand Up @@ -554,13 +555,21 @@ tds_read_packet(TDSSOCKET * tds)
tds->in_pos = 8;
tds->in_flag = tds->in_buf[0];

/* send acknowledge if needed */
if ((int32_t) (tds->recv_seq + 2 - tds->recv_wnd) >= 0)
tds_update_recv_wnd(tds, tds->recv_seq + 4);
/* Look ahead by up to 4 packets */
memcpy(&this_seq, ((char const *)packet->buf) + offsetof(TDS72_SMP_HEADER, seq), sizeof this_seq);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would use

this_seq = TDS_GET_A4LE(&((const TDS72_SMP_HEADER *) packet->buf)->seq);

also, consider that other code will still use tds->recv_seq, perhaps it would be better to update that in a different way.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree about using TDS_GET_A4LE ; but is packet guaranteed to be correctly aligned for that cast?

The code in tds_connection_put_packet makes the same cast, so I guess it's not a problem. The code in tds_packet_read() uses memcpy(&mars_header, packet->buf, sizeof(mars_header));.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I though I replied to this. Packet is an allocated buffer so it's surely aligned to 32 bit.

if (this_seq + 2 >= tds->recv_wnd)
Copy link
Contributor

@freddy77 freddy77 Dec 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This formulae is wrong if you have wrapping, the old one was correct.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, you're right about the wrapping issue. I didn't like the old one as the C Standard doesn't define the behaviour of converting out-of-range unsigned values to signed integer, but FreeTDS is unlikely to be used on any system that doesn't behave as expected here.

Perhaps we could improve readability by borrowing TCP sequence macros, e.g. from BSD but with TDS_INT as the cast type (it should be the signed counterpart of TDS_UINT).

tds_update_recv_wnd(tds, this_seq + 4);

return tds->in_len;
}

#if ENABLE_EXTRA_CHECKS
{
TDS_UINT np = 0;
for (p_packet = &conn->packets; *p_packet; p_packet = &(*p_packet)->next) ++np;
tdsdump_log(TDS_DBG_NETWORK, "MARS SID %d queued packets %u\n", tds->sid, np);
}
#endif
/* network ok ? process network */
if (!conn->in_net_tds) {
tds_connection_network(conn, tds, 0);
Expand Down
Loading