Skip to content

missing check for truncation of numeric data lead to "double free" or other memory errors #72

Open

Description

@GitMensch

This is related to the "in progress" issue #31 and should ideally be solved together with that.

Given the code in

Open-COBOL-ESQL/dblib/ocesql.c

Lines 2789 to 3172 in 1b272b8

case OCDB_TYPE_UNSIGNED_NUMBER:
{
char *ptr;
int fillzero;
int zcount;
char *final;
int finalbuflen;
// fill zero
finalbuflen = sv->length + TERMINAL_LENGTH;
final = (char *)calloc(finalbuflen, sizeof(char));
// before decimal point
int beforedp = 0;
for(ptr = retstr; *ptr != '\0'; ptr++){
if(*ptr == '.'){
break;
} else {
beforedp++;
}
}
fillzero = sv->length - beforedp + sv->power;
for(zcount = 0; zcount < fillzero; zcount++){
final[zcount] = '0';
}
memcpy(final + fillzero, retstr, beforedp);
if(sv->power < 0){
int afterdp = 0;
if(*ptr != '\0'){
ptr++;
// after decimal point
for(; *ptr != '\0'; ptr++){
afterdp++;
}
// fill zero
memcpy(final + fillzero + beforedp,
retstr + beforedp + DECIMAL_LENGTH, afterdp);
}
fillzero = - sv->power - afterdp;
for(zcount = 0; zcount < fillzero; zcount++){
final[zcount] = '0';
}
}
memcpy(addr, final, sv->length);
free(final);
break;
}
case OCDB_TYPE_SIGNED_NUMBER_TC:
{
char *value;
char *ptr;
int is_negative = false;
int fillzero;
int zcount;
char *final;
int finalbuflen;
int final_length;
// fill zero
finalbuflen = sv->length;
final = (char *)calloc(finalbuflen, sizeof(char));
if(retstr[0] == '-'){
is_negative = true;
value = retstr + 1;
} else {
value = retstr;
}
// before decimal point
int beforedp = 0;
for(ptr = value; *ptr != '\0'; ptr++){
if(*ptr == '.'){
break;
} else {
beforedp++;
}
}
fillzero = sv->length - beforedp + sv->power;
for(zcount = 0; zcount < fillzero; zcount++){
final[zcount] = '0';
}
memcpy(final + fillzero, value, beforedp);
if(sv->power < 0){
int afterdp = 0;
if(*ptr != '\0'){
ptr++;
// after decimal point
for(; *ptr != '\0'; ptr++){
afterdp++;
}
memcpy(final + fillzero + beforedp, value +
beforedp + DECIMAL_LENGTH, afterdp);
}
// fill zero
fillzero = - sv->power - afterdp;
for(zcount = 0; zcount < fillzero; zcount++){
final[zcount] = '0';
}
}
final_length = strlen(final);
if(is_negative){
int index = *(final + (final_length - 1)) - '0';
final[final_length - 1] = type_tc_negative_final_number[index];
}
memcpy(addr, final, sv->length);
free(final);
break;
}
case OCDB_TYPE_SIGNED_NUMBER_LS:
{
char *value;
char *ptr;
int fillzero;
int zcount;
char *final;
int finalbuflen;
// fill zero
finalbuflen = SIGN_LENGTH + sv->length + TERMINAL_LENGTH;
final = (char *)calloc(finalbuflen, sizeof(char));
if(retstr[0] == '-'){
final[0] = '-';
value = retstr + 1;
} else {
final[0] = '+';
value = retstr;
}
// before decimal point
int beforedp = 0;
for(ptr = value; *ptr != '\0'; ptr++){
if(*ptr == '.'){
break;
} else {
beforedp++;
}
}
fillzero = sv->length - beforedp + sv->power;
for(zcount = 0; zcount < fillzero; zcount++){
final[zcount] = '0';
}
memcpy(final + SIGN_LENGTH + fillzero, value, beforedp);
if(sv->power < 0){
int afterdp = 0;
if(*ptr != '\0'){
ptr++;
// after decimal point
for(; *ptr != '\0'; ptr++){
afterdp++;
}
// fill zero
memcpy(final + SIGN_LENGTH + fillzero + beforedp,
value + beforedp + DECIMAL_LENGTH, afterdp);
}
fillzero = - sv->power - afterdp;
for(zcount = 0; zcount < fillzero; zcount++){
final[zcount] = '0';
}
}
memcpy(addr, final, sv->length + SIGN_LENGTH);
free(final);
break;
}
case OCDB_TYPE_UNSIGNED_NUMBER_PD:
{
char *value = retstr;
char *ptr;
int is_negative = false;
int fillzero;
int zcount;
char *pre_final;
int pre_final_len;
char *final;
double dlength;
int skip_first;
int i;
unsigned char ubit = 0xF0;
unsigned char lbit = 0x0F;
dlength = ceil(((double)sv->length + 1)/2);
skip_first = (sv->length + 1) % 2; // 1 -> skip first 4 bits
pre_final_len = sv->length + TERMINAL_LENGTH;
pre_final = (char *)calloc(pre_final_len, sizeof(char));
// before decimal point
int beforedp = 0;
for(ptr = value; *ptr != '\0'; ptr++){
if(*ptr == '.'){
break;
} else {
beforedp++;
}
}
fillzero = sv->length - beforedp + sv->power;
for(zcount = 0; zcount < fillzero; zcount++){
pre_final[zcount] = '0';
}
memcpy(pre_final + fillzero, value, beforedp);
if(sv->power < 0){
int afterdp = 0;
if(*ptr != '\0'){
ptr++;
// after decimal point
for(; *ptr != '\0'; ptr++){
afterdp++;
}
memcpy(pre_final + fillzero + beforedp,
value + beforedp + DECIMAL_LENGTH, afterdp);
}
// fill zero
fillzero = - sv->power - afterdp;
for(zcount = 0; zcount < fillzero; zcount++){
pre_final[zcount] = '0';
}
}
// format setting
final = (char *)calloc((int)dlength + TERMINAL_LENGTH, sizeof(char));
ptr = pre_final;
for(i=0; i<dlength; i++){
unsigned char vubit = 0x00;
unsigned char vlbit = 0x00;
if(i == 0 && skip_first){
vubit = 0x00;
} else {
vubit = (*ptr) << 4;
vubit = vubit & ubit;
ptr++;
}
if(i != dlength - 1){
vlbit = *ptr;
vlbit = vlbit & lbit;
ptr++;
} else {
vlbit = 0x0F;
}
final[i] = vubit | vlbit;
}
memcpy(addr, final, (int)dlength);
free(pre_final);
free(final);
break;
}
case OCDB_TYPE_SIGNED_NUMBER_PD:
{
char *value;
char *ptr;
int is_negative = false;
int fillzero;
int zcount;
char *pre_final;
int pre_final_len;
char *final;
double dlength;
int skip_first;
int i;
unsigned char ubit = 0xF0;
unsigned char lbit = 0x0F;
dlength = ceil((double)(sv->length + 1)/2);
skip_first = (sv->length + 1) % 2; // 1 -> skip first 4 bits
if(retstr[0] == '-'){
is_negative = true;
value = retstr + 1;
} else {
value = retstr;
}
pre_final_len = (int)dlength + TERMINAL_LENGTH;
pre_final = (char *)calloc(pre_final_len, sizeof(char));
// before decimal point
int beforedp = 0;
for(ptr = value; *ptr != '\0'; ptr++){
if(*ptr == '.'){
break;
} else {
beforedp++;
}
}
fillzero = sv->length - beforedp + sv->power;
for(zcount = 0; zcount < fillzero; zcount++){
pre_final[zcount] = '0';
}
memcpy(pre_final + fillzero, value, beforedp);
if(sv->power < 0){
int afterdp = 0;
if(*ptr != '\0'){
ptr++;
// after decimal point
for(; *ptr != '\0'; ptr++){
afterdp++;
}
memcpy(pre_final + fillzero + beforedp,
value + beforedp + DECIMAL_LENGTH, afterdp);
}
// fill zero
fillzero = - sv->power - afterdp;
for(zcount = 0; zcount < fillzero; zcount++){
pre_final[zcount] = '0';
}
}
// format setting
final = (char *)calloc((int)dlength + TERMINAL_LENGTH, sizeof(char));
ptr = pre_final;
for(i=0; i<dlength; i++){
unsigned char vubit = 0x00;
unsigned char vlbit = 0x00;
if(i == 0 && skip_first){
vubit = 0x00;
} else {
vubit = (*ptr) << 4;
vubit = vubit & ubit;
ptr++;
}
if(i != dlength - 1){
vlbit = *ptr;
vlbit = vlbit & lbit;
ptr++;
} else {
if(is_negative){
vlbit = 0x0D;
} else {
vlbit = 0x0C;
}
}
final[i] = vubit | vlbit;
}
memcpy(addr, final, (int)dlength);
free(pre_final);
free(final);
break;
}

  • the driver queries the data, for example with an integer type in the DB you may get 2147483647
  • it translates the data to its string value, so for our example "2147483647"
  • the library frontend takes this value and sets the COBOL data

... so far: totally correct.

The issue is that in all those places the local allocated buffer that is used to write the data to is allocated based on dlength - the length of the COBOL field, but the memcpy is based on the strlen(data), which can be bigger.

In the example above, given a COBOL definition of S9(8) we allocate 8 (+1 for strlen) bytes, but create_coboldata copies the 10 bytes from the data size (*ptr != '\0' / strlen()). In the case of OCDB_TYPE_SIGNED_NUMBER_TC:

Open-COBOL-ESQL/dblib/ocesql.c

Lines 2867 to 2881 in 1b272b8

// before decimal point
int beforedp = 0;
for(ptr = value; *ptr != '\0'; ptr++){
if(*ptr == '.'){
break;
} else {
beforedp++;
}
}
fillzero = sv->length - beforedp + sv->power;
for(zcount = 0; zcount < fillzero; zcount++){
final[zcount] = '0';
}
memcpy(final + fillzero, value, beforedp);

we compute fillzero which will end up with -2 and then copy the data "two bytes before the allocated buffer".

If this does not crash the memory layout, then the end result looks correct (left-truncation).

As seen above the truncation must be explicit checked, both to return the correct values and even more important to don't trash the memory.

And very likely after the data is stored with COBOL correct truncation an sqlcode for "field truncated" should be set.

Activity

  1. GitMensch commented on Jan 11, 2023

    @GitMensch
    ContributorAuthor

    I'm going to send in a PR for both this issue and #31 in the next days.

  2. phodal commented on Oct 20, 2026

    @phodal

    Still reproducible on Open-COBOL-ESQL 1.4.0 (git e116b6a, 2026-03-06), here with a COMP-3 host variable — the same truncation path as described above, in create_coboldata() case OCDB_TYPE_SIGNED_NUMBER_PD.

    Environment: Debian trixie x86_64, GnuCOBOL 3.2.0 (Debian 3.2-3), PostgreSQL 17.11, libpq 17.

           IDENTIFICATION DIVISION.
           PROGRAM-ID. C3OVER.
           DATA DIVISION.
           WORKING-STORAGE SECTION.
           01  D-P      PIC -(7)9.99.
           EXEC SQL BEGIN DECLARE SECTION END-EXEC.
           01  DBNAME   PIC X(30) VALUE "bugrepro@127.0.0.1:55432".
           01  USERNAME PIC X(30) VALUE "point".
           01  PASSWD   PIC X(10) VALUE SPACE.
           01  H-P      PIC S9(7)V99 COMP-3.
           EXEC SQL END DECLARE SECTION END-EXEC.
           EXEC SQL INCLUDE SQLCA END-EXEC.
          *
           PROCEDURE DIVISION.
           MAIN-RTN.
               EXEC SQL
                   CONNECT :USERNAME IDENTIFIED BY :PASSWD USING :DBNAME
               END-EXEC.
               DISPLAY "CONNECT SQLCODE=" SQLCODE.
          *    12345678.5 needs 8 integer digits; H-P holds 7.
               EXEC SQL
                   SELECT CAST(12345678.5 AS NUMERIC(10,1)) INTO :H-P
               END-EXEC.
               DISPLAY "SELECT SQLCODE=" SQLCODE " SQLSTATE=" SQLSTATE.
               MOVE H-P TO D-P.
               DISPLAY "H-P=" D-P.
               EXEC SQL DISCONNECT ALL END-EXEC.
               STOP RUN.
    ocesql C3OVER.cbl C3OVER.pre
    cobc -x -I $PREFIX/share/open-cobol-esql/copy -L$PREFIX/lib -locesql -o c3over C3OVER.pre
    ./c3over

    Actual

    CONNECT SQLCODE=+0000000000
    double free or corruption (out)
    Aborted   (exit status 134)
    

    Expected: no memory corruption. Either SQLCODE < 0 with a truncation/overflow error, or (if left truncation is intended) H-P = 2345678.50 with a warning (e.g. SQLWARN1 = 'W'). For comparison, opensource COBOL 4J 2.1.0-hotfix1 (built-in ESQL) does not crash on the same program (it silently stores the truncated value with SQLCODE 0).

    Cause (as in this issue): fillzero = sv->length - beforedp + sv->power is 9 - 8 - 2 = -1, so memcpy(pre_final + fillzero, ...) writes one byte before the calloc'd buffer; the following free() aborts. The same pattern exists in the UNSIGNED_PD and DISPLAY (TC) cases. A check if (fillzero < 0) that sets an error (or truncates explicitly from the left) before the memcpy would avoid the corruption.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions