Thread: BUG #1723: array_cat() bug when passed empty array

BUG #1723: array_cat() bug when passed empty array

From
"Dave Chapeskie"
Date:
The following bug has been logged online:

Bug reference:      1723
Logged by:          Dave Chapeskie
Email address:      pgsql@ddm.wox.org
PostgreSQL version: 7.4.6
Operating system:   FreeBSD 5.x
Description:        array_cat() bug when passed empty array
Details:

I believe this bug still exists in the latest 8.0.x but
have not tested that version, just looked at the code.

array_cat() has a bug when passed an empty array.  The
code attempts to optimise/short-circuit this case and
returns a pointer to the non-empty argument.  This is
bad/wrong.  Especially when used in a construct like:
  foo := foo || <some_array>
since after array_cat() returns exec_assign_value()
will pfree() 'foo' and then attempt to assign the now
invalid result that points to 'foo'.

Turning on assertion checks (which turns on memory
checks) catches this.  Here is a simple example:

create or replace function array_test (text[],text[])
returns text[] as '
    declare
        tmp text[];
    begin
        tmp := $1;
        tmp := tmp || $2;
        return tmp;
    end'
    language plpgsql
    stable;
select array_test(array['foo','bar'],'{x}');
select array_test(array['foo','bar'],'{}');

The first call to array_test() returns
  {foo,bar,x}
as expected.  With debuging on the second call returns:
ERROR:  out of memory
DETAIL:  Failed on request of size 1065320319.
CONTEXT:  PL/pgSQL function "array_test" while casting return value to
function's return type
Note 1065320319 = 0x3F7F7F7F and with debugging pfree'd
memory is filled with 0x74 bytes.  The top 0x4000000
got cleared because the top bits of the length field
are flag bits.  I've also seen the error say
"Failed on request of size 2139062147.".

Re: BUG #1723: array_cat() bug when passed empty array

From
Tom Lane
Date:
"Dave Chapeskie" <pgsql@ddm.wox.org> writes:
> array_cat() has a bug when passed an empty array.  The
> code attempts to optimise/short-circuit this case and
> returns a pointer to the non-empty argument.  This is
> bad/wrong.  Especially when used in a construct like:
>   foo := foo || <some_array>
> since after array_cat() returns exec_assign_value()
> will pfree() 'foo' and then attempt to assign the now
> invalid result that points to 'foo'.

Actually, I would say the bug is exec_assign_value's.  There is nothing
at all wrong with a function returning one of its input values; for
example the smaller/larger functions all do that.  Let's see...

regression=# create or replace function smal(text,text) returns text as $$
regression$# declare tmp text;
regression$# begin
regression$#   tmp := $1;
regression$#   tmp := text_smaller(tmp, $2);
regression$#   return tmp;
regression$# end$$ language plpgsql stable;
CREATE FUNCTION
regression=# select smal('abc', '123');
 smal
------
 123
(1 row)

regression=# select smal('123', 'abc');
ERROR:  out of memory
DETAIL:  Failed on request of size 1065320319.
CONTEXT:  PL/pgSQL function "smal" line 4 at assignment
regression=#

It's very surprising no one noticed this before.  Thanks for the report!

            regards, tom lane

Re: BUG #1723: array_cat() bug when passed empty array

From
Tom Lane
Date:
I wrote:
> Actually, I would say the bug is exec_assign_value's.  There is nothing
> at all wrong with a function returning one of its input values; for
> example the smaller/larger functions all do that.

For that matter, you don't need a function at all:

regression=# create or replace function copyit(text) returns text as $$
regression$# declare tmp text;
regression$# begin
regression$#   tmp := $1;
regression$#   tmp := tmp;
regression$#   return tmp;
regression$# end$$ language plpgsql stable;
CREATE FUNCTION
regression=# select copyit('foo');
ERROR:  out of memory
DETAIL:  Failed on request of size 1065320319.
CONTEXT:  PL/pgSQL function "copyit" line 4 at assignment
regression=#

This makes it perfectly clear that the problem is that exec_assign_value
must copy the given value before it frees the old, just in case they're
the same.  (Hmm, I wonder if we can shortcircuit the whole thing ...)

            regards, tom lane

Re: BUG #1723: array_cat() bug when passed empty array

From
Tom Lane
Date:
Dave Chapeskie <dchapeskie@sandvine.com> writes:
> On Mon, Jun 20, 2005 at 03:44:24PM -0400, Tom Lane wrote:
>> This makes it perfectly clear that the problem is that exec_assign_value
>> must copy the given value before it frees the old, just in case they're
>> the same.  (Hmm, I wonder if we can shortcircuit the whole thing ...)

> Anyone working on fixing this?  I'd have tried but I didn't want to dive
> into exec_assign_value() and exec_cast_value().

Done some time ago, I think [ digs in CVS log... ]  yeah, here it is:

2005-06-20 16:44  tgl

    * src/pl/plpgsql/src/: pl_exec.c (REL7_3_STABLE), pl_exec.c
    (REL7_4_STABLE), pl_exec.c (REL7_2_STABLE), pl_exec.c
    (REL8_0_STABLE), pl_exec.c: plpgsql's exec_assign_value() freed the
    old value of a variable before copying/converting the new value,
    which meant that it failed badly on "var := var" if var is of
    pass-by-reference type.  Fix this and a similar hazard in
    exec_move_row(); not sure that the latter can manifest before 8.0,
    but patch it all the way back anyway.  Per report from Dave
    Chapeskie.

You can get the patches from our CVS server.
http://developer.postgresql.org/cvsweb.cgi/pgsql/src/pl/plpgsql/src/pl_exec.c

            regards, tom lane

Re: BUG #1723: array_cat() bug when passed empty array

From
Dave Chapeskie
Date:
On Mon, Jun 20, 2005 at 03:44:24PM -0400, Tom Lane wrote:

> I [Tom Lane] wrote:
> > Actually, I would say the bug is exec_assign_value's.  There is nothing
> > at all wrong with a function returning one of its input values; for
> > example the smaller/larger functions all do that.
>
> For that matter, you don't need a function at all:
>
> regression=# create or replace function copyit(text) returns text as $$
> regression$# declare tmp text;
> regression$# begin
> regression$#   tmp := $1;
> regression$#   tmp := tmp;
> regression$#   return tmp;
> regression$# end$$ language plpgsql stable;
> CREATE FUNCTION
> regression=# select copyit('foo');
> ERROR:  out of memory
> DETAIL:  Failed on request of size 1065320319.
> CONTEXT:  PL/pgSQL function "copyit" line 4 at assignment
> regression=#
>
> This makes it perfectly clear that the problem is that exec_assign_value
> must copy the given value before it frees the old, just in case they're
> the same.  (Hmm, I wonder if we can shortcircuit the whole thing ...)

Anyone working on fixing this?  I'd have tried but I didn't want to dive
into exec_assign_value() and exec_cast_value().

Given that this occurs for almost any assignment of the form
"x := any expression with x" it's hard to avoid without rewritting a lot
of basic assignments with "tmp:=x; x:=foo(tmp)" which is ugly.

We can't use memory checking with this and even without memory checking
I don't trust that memory isn't getting spamed in some situations.
After all it's referencing free'd memory.

--
Dave Chapeskie