Bug 78969 - bogus snprintf truncation warning due to missing range info
Summary: bogus snprintf truncation warning due to missing range info
Status: NEW
Alias: None
Product: gcc
Classification: Unclassified
Component: tree-optimization (show other bugs)
Version: 7.0
: P3 normal
Target Milestone: ---
Assignee: Not yet assigned to anyone
URL:
Keywords: diagnostic, missed-optimization
: 80924 (view as bug list)
Depends on:
Blocks: 81592
  Show dependency treegraph
 
Reported: 2017-01-02 18:55 UTC by Martin Sebor
Modified: 2017-07-27 20:28 UTC (History)
6 users (show)

See Also:
Host:
Target:
Build:
Known to work:
Known to fail: 7.1.0
Last reconfirmed: 2017-05-23 00:00:00


Attachments

Note You need to log in before you can comment on or make changes to this bug.
Description Martin Sebor 2017-01-02 18:55:44 UTC
In both functions in the following test case the %3u argument to snprintf is in the range [0, 999] and so the directive writes exactly 3 bytes into the destination buffer of size 4.  As the VRP dump shows, GCC exposes that range to the -Wformat-length checker via the get_range_info() function in the call in function f() allowing it to avoid a warning.  But in the call in function g(), even though the same range is also known, it's not made available for the argument to the directive, resulting in a false positive.

$ cat t.c && gcc -O2 -S -Wall -Wformat-length=2 -fdump-tree-vrp=/dev/stdout t.c 
void f (unsigned j, char *p)
{
  if (j > 999)
    j = 0;

  __builtin_snprintf (p, 4, "%3u", j);
}

void g (unsigned j, char *p)
{
  if (j > 999)
    return;

  __builtin_snprintf (p, 4, "%3u", j);
}




;; Function f (f, funcdef_no=0, decl_uid=1796, cgraph_uid=0, symbol_order=0)

;; 1 loops found
;;
;; Loop 0
;;  header 0, latch 1
;;  depth 0, outer -1
;;  nodes: 0 1 2 3 4
;; 2 succs { 3 4 }
;; 3 succs { 4 }
;; 4 succs { 1 }

SSA replacement table
N_i -> { O_1 ... O_j } means that N_i replaces O_1, ..., O_j

j_6 -> { j_2(D) }
j_7 -> { j_2(D) }
Incremental SSA update started at block: 2
Number of blocks in CFG: 6
Number of blocks to update: 4 ( 67%)



Value ranges after VRP:

j_1: [0, 999]  EQUIVALENCES: { } (0 elements)
j_2(D): VARYING
j_6: [1000, +INF]  EQUIVALENCES: { j_2(D) } (1 elements)
j_7: [0, 999]  EQUIVALENCES: { j_2(D) } (1 elements)


Removing basic block 3
f (unsigned int j, char * p)
{
  <bb 2> [100.00%]:
  if (j_2(D) > 999)
    goto <bb 4>; [54.00%]
  else
    goto <bb 3>; [46.00%]

  <bb 3> [46.00%]:

  <bb 4> [100.00%]:
  # j_1 = PHI <j_2(D)(3), 0(2)>
  __builtin_snprintf (p_4(D), 4, "%3u", j_1);
  return;

}



;; Function f (f, funcdef_no=0, decl_uid=1796, cgraph_uid=0, symbol_order=0)

;; 1 loops found
;;
;; Loop 0
;;  header 0, latch 1
;;  depth 0, outer -1
;;  nodes: 0 1 2 3 4
;; 2 succs { 3 4 }
;; 3 succs { 4 }
;; 4 succs { 1 }

SSA replacement table
N_i -> { O_1 ... O_j } means that N_i replaces O_1, ..., O_j

j_6 -> { j_2(D) }
j_7 -> { j_2(D) }
Incremental SSA update started at block: 2
Number of blocks in CFG: 6
Number of blocks to update: 4 ( 67%)



Value ranges after VRP:

j_1: [0, 999]  EQUIVALENCES: { } (0 elements)
j_2(D): VARYING
j_6: [1000, +INF]  EQUIVALENCES: { j_2(D) } (1 elements)
j_7: [0, 999]  EQUIVALENCES: { j_2(D) } (1 elements)


Removing basic block 3
f (unsigned int j, char * p)
{
  <bb 2> [100.00%]:
  if (j_2(D) > 999)
    goto <bb 4>; [54.00%]
  else
    goto <bb 3>; [46.00%]

  <bb 3> [46.00%]:

  <bb 4> [100.00%]:
  # j_1 = PHI <j_2(D)(3), 0(2)>
  __builtin_snprintf (p_4(D), 4, "%3u", j_1);
  return;

}



;; Function g (g, funcdef_no=1, decl_uid=1800, cgraph_uid=1, symbol_order=1)

;; 1 loops found
;;
;; Loop 0
;;  header 0, latch 1
;;  depth 0, outer -1
;;  nodes: 0 1 2 3 4
;; 2 succs { 4 3 }
;; 3 succs { 4 }
;; 4 succs { 1 }

SSA replacement table
N_i -> { O_1 ... O_j } means that N_i replaces O_1, ..., O_j

j_6 -> { j_2(D) }
Incremental SSA update started at block: 2
Number of blocks in CFG: 5
Number of blocks to update: 2 ( 40%)



Value ranges after VRP:

.MEM_1: VARYING
j_2(D): VARYING
j_6: [0, 999]  EQUIVALENCES: { j_2(D) } (1 elements)


g (unsigned int j, char * p)
{
  <bb 2> [100.00%]:
  if (j_2(D) > 999)
    goto <bb 4>; [51.01%]
  else
    goto <bb 3>; [48.99%]

  <bb 3> [48.99%]:
  __builtin_snprintf (p_4(D), 4, "%3u", j_2(D));

  <bb 4> [100.00%]:
  return;

}


t.c: In function ‘g’:
t.c:14:30: warning: ‘%3u’ directive output may be truncated writing between 3 and 10 bytes into a region of size 4 [-Wformat-length=]
   __builtin_snprintf (p, 4, "%3u", j);
                              ^~~
t.c:14:29: note: using the range [1, 4294967295] for directive argument
   __builtin_snprintf (p, 4, "%3u", j);
                             ^~~~~
t.c:14:3: note: format output between 4 and 11 bytes into a destination of size 4
   __builtin_snprintf (p, 4, "%3u", j);
   ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

;; Function g (g, funcdef_no=1, decl_uid=1800, cgraph_uid=1, symbol_order=1)

;; 1 loops found
;;
;; Loop 0
;;  header 0, latch 1
;;  depth 0, outer -1
;;  nodes: 0 1 2 3 4
;; 2 succs { 4 3 }
;; 3 succs { 4 }
;; 4 succs { 1 }

SSA replacement table
N_i -> { O_1 ... O_j } means that N_i replaces O_1, ..., O_j

j_6 -> { j_2(D) }
Incremental SSA update started at block: 2
Number of blocks in CFG: 5
Number of blocks to update: 2 ( 40%)



Value ranges after VRP:

.MEM_1: VARYING
j_2(D): VARYING
j_6: [0, 999]  EQUIVALENCES: { j_2(D) } (1 elements)


g (unsigned int j, char * p)
{
  <bb 2> [100.00%]:
  if (j_2(D) > 999)
    goto <bb 4>; [51.01%]
  else
    goto <bb 3>; [48.99%]

  <bb 3> [48.99%]:
  __builtin_snprintf (p_4(D), 4, "%3u", j_2(D));

  <bb 4> [100.00%]:
  return;

}
Comment 1 Martin Sebor 2017-01-02 19:32:29 UTC
The same underlying problem (lack of range info) can be seen in the VRP dump for the following test case.  The -Walloca-larger-than option is interesting because the alloca pass that implements it tries to compensate for the missing range info by deriving it from conditions the alloca() argument is subjected to (see the alloca_call_type_by_arg function).  Although the logic it uses is quite simple in this case it manages to successfully determine the range on its own and avoids the warning.

$ cat t.c && gcc -O2 -S -Wall -Walloca-larger-than=255  -fdump-tree-vrp=/dev/stdout t.c 
void foo (void*);

void f (unsigned long j)
{
  if (j / 256)
    return;

  foo (__builtin_alloca (j));
}




;; Function f (f, funcdef_no=0, decl_uid=1797, cgraph_uid=0, symbol_order=0)

;; 1 loops found
;;
;; Loop 0
;;  header 0, latch 1
;;  depth 0, outer -1
;;  nodes: 0 1 2 3 4
;; 2 succs { 4 3 }
;; 3 succs { 4 }
;; 4 succs { 1 }

SSA replacement table
N_i -> { O_1 ... O_j } means that N_i replaces O_1, ..., O_j

j_7 -> { j_3(D) }
Incremental SSA update started at block: 2
Number of blocks in CFG: 5
Number of blocks to update: 2 ( 40%)



Value ranges after VRP:

_1: ~[0B, 0B]
.MEM_2: VARYING
j_3(D): VARYING
j_7: [0, 255]  EQUIVALENCES: { j_3(D) } (1 elements)


f (long unsigned int j)
{
  void * _1;

  <bb 2> [100.00%]:
  if (j_3(D) > 255)
    goto <bb 4>; [51.01%]
  else
    goto <bb 3>; [48.99%]

  <bb 3> [48.99%]:
  _1 = __builtin_alloca (j_3(D));
  foo (_1);

  <bb 4> [100.00%]:
  return;

}



;; Function f (f, funcdef_no=0, decl_uid=1797, cgraph_uid=0, symbol_order=0)

;; 1 loops found
;;
;; Loop 0
;;  header 0, latch 1
;;  depth 0, outer -1
;;  nodes: 0 1 2 3 4
;; 2 succs { 4 3 }
;; 3 succs { 4 }
;; 4 succs { 1 }

SSA replacement table
N_i -> { O_1 ... O_j } means that N_i replaces O_1, ..., O_j

j_7 -> { j_3(D) }
Incremental SSA update started at block: 2
Number of blocks in CFG: 5
Number of blocks to update: 2 ( 40%)



Value ranges after VRP:

_1: ~[0B, 0B]
.MEM_2: VARYING
j_3(D): VARYING
j_7: [0, 255]  EQUIVALENCES: { j_3(D) } (1 elements)


f (long unsigned int j)
{
  void * _1;

  <bb 2> [100.00%]:
  if (j_3(D) > 255)
    goto <bb 4>; [51.01%]
  else
    goto <bb 3>; [48.99%]

  <bb 3> [48.99%]:
  _1 = __builtin_alloca (j_3(D));
  foo (_1);

  <bb 4> [100.00%]:
  return;

}
Comment 2 Andrew Pinski 2017-01-03 06:48:10 UTC
I think there might be a dup of this bug already.
basically VRP does a copy prop of where the assert was.
get_range_info is not position sensitive so the range is gone after VRP.
Comment 3 Martin Sebor 2017-01-08 23:42:44 UTC
Author: msebor
Date: Sun Jan  8 23:42:09 2017
New Revision: 244210

URL: https://gcc.gnu.org/viewcvs?rev=244210&root=gcc&view=rev
Log:
PR tree-optimization/78913 - Probably misleading error reported by -Wformat-length
PR middle-end/77708 - -Wformat-length %s warns for snprintf

gcc/ChangeLog:

	PR middle-end/77708
	* doc/invoke.texi (Warning Options): Document -Wformat-truncation.
	* gimple-ssa-sprintf.c (call_info::reval_used, call_info::warnopt):
	New member functions.
	(format_directive): Used them.
	(add_bytes): Same.
	(pass_sprintf_length::handle_gimple_call): Same.
	* graphite-sese-to-poly.c (tree_int_to_gmp): Increase buffer size
	to avoid truncation for any argument.
	(extract_affine_mul): Same.
	* tree.c (get_file_function_name): Same.

gcc/c-family/ChangeLog:

	PR middle-end/77708
	* c.opt (-Wformat-truncation): New option.

gcc/fortran/ChangeLog:

	PR tree-optimization/78913
	PR middle-end/77708
	* trans-common.c (build_equiv_decl): Increase buffer size to avoid
	truncation for any argument.
	* trans-types.c (gfc_build_logical_type): Same.

gcc/testsuite/ChangeLog:

	PR middle-end/77708
	* gcc.dg/tree-ssa/builtin-snprintf-warn-1.c: New test.
	* gcc.dg/tree-ssa/builtin-snprintf-warn-2.c: New test.
	* gcc.dg/tree-ssa/builtin-sprintf-warn-6.c: XFAIL test cases failing
	due to bug 78969.
	* gcc.dg/format/pr78569.c: Adjust.


Added:
    trunk/gcc/testsuite/gcc.dg/tree-ssa/builtin-snprintf-warn-1.c
    trunk/gcc/testsuite/gcc.dg/tree-ssa/builtin-snprintf-warn-2.c
Modified:
    trunk/gcc/ChangeLog
    trunk/gcc/c-family/ChangeLog
    trunk/gcc/c-family/c.opt
    trunk/gcc/doc/invoke.texi
    trunk/gcc/fortran/ChangeLog
    trunk/gcc/fortran/trans-common.c
    trunk/gcc/fortran/trans-types.c
    trunk/gcc/gimple-ssa-sprintf.c
    trunk/gcc/graphite-sese-to-poly.c
    trunk/gcc/testsuite/ChangeLog
    trunk/gcc/testsuite/gcc.dg/format/pr78569.c
    trunk/gcc/testsuite/gcc.dg/tree-ssa/builtin-sprintf-warn-6.c
    trunk/gcc/tree.c
Comment 4 Sergei Trofimovich 2017-05-23 21:28:22 UTC
Found similar false positive on lxc project.

Original snippet of code: https://github.com/lxc/lxc/blob/5059aae90584d7d80b3494088920da4ba73e2b2a/src/lxc/cgroups/cgfsng.c#L1379-L1395

Simplified version:

$ cat a.c

#include <stdio.h>

void f(char * p /* NNN\0" */) {
    for (int idx = 0; idx < 1000; idx++) {
        // guaranteed to be in [0-999] range
        snprintf (p, 4, "%d", idx);
    }
}

$ gcc -O2 -c a.c -Wall
a.c: In function 'f':
a.c:6:25: warning: '__builtin___snprintf_chk' output may be truncated before the last format character [-Wformat-truncation=]
         snprintf (p, 4, "%d", idx);
                         ^~~~
/usr/include/bits/stdio2.h:64:10: note: '__builtin___snprintf_chk' output between 2 and 5 bytes into a destination of size 4

If I change 1000 to 999
    for (int idx = 0; idx < 999; idx++) {
no warning will be issued.

Looks like what happens here is that gcc does not distinct between
idx in the for loop itself that has range of [0-999]
and idx outside for loop, which has value range of [1000-1000].
Comment 5 Martin Sebor 2017-05-23 22:01:21 UTC
(In reply to Sergei Trofimovich from comment #4)
> Found similar false positive on lxc project.

Thanks for the test case.  The VRP pass computes the correct range information for the argument but the range made available outside it via the get_range_info() function is that of idx_6, not idx_10 below (compile with -fdump-tree-vrp=/dev/stdout to see the output).  It's possible that this problem is caused by the same underlying limitation as the one in comment #0.  Let me confirm this bug on that basis.

Value ranges after VRP:

idx_1: [1, 999]  EQUIVALENCES: { idx_6 } (1 elements)
idx_6: [1, 1000]
idx_10: [0, 999]
.MEM_11: VARYING


Removing basic block 5
f (char * p)
{
  int idx;

  <bb 2> [1.00%]:

  <bb 3> [99.00%]:
  # idx_10 = PHI <idx_6(3), 0(2)>
  __builtin_snprintf (p_4(D), 4, "%d", idx_10);
  idx_6 = idx_10 + 1;
  if (idx_6 != 1000)
    goto <bb 3>; [98.99%]
  else
    goto <bb 4>; [1.01%]

  <bb 4> [1.00%]:
  return;

}
Comment 6 Martin Sebor 2017-05-24 02:29:01 UTC
I also found the following discussion about what looks like the same problem:

  https://patchwork.ffmpeg.org/patch/3630
Comment 7 Jakub Jelinek 2017-05-24 07:10:38 UTC
This is because the PHI at that point is only created during CFG changes at the end of VRP1 pass.
After creating ASSERT_EXPRs, we still have:
  <bb 2> [1.00%]:
  goto <bb 4>; [100.00%]

  <bb 3> [99.00%]:
  # RANGE [0, 1000] NONZERO 1023
  idx_7 = ASSERT_EXPR <idx_1, idx_1 <= 999>;
  __builtin_snprintf (p_4(D), 4, "%d", idx_7);
  # RANGE [1, 1000] NONZERO 1023
  idx_6 = idx_7 + 1;

  <bb 4> [100.00%]:
  # RANGE [0, 1000] NONZERO 1023
  # idx_1 = PHI <0(2), idx_6(3)>
  if (idx_1 <= 999)
    goto <bb 3>; [99.00%]
  else
    goto <bb 5>; [1.00%]

  <bb 5> [1.00%]:
  return;
Then VRP1 correctly determines:
idx_1: [0, 1000]
.MEM_2: VARYING
idx_6: [1, 1000]
idx_7: [0, 999]  EQUIVALENCES: { idx_1 } (1 elements)
then the ASSERT_EXPRs are removed, which means whenever idx_7 is used we now use idx_1, and finally the loop is changed, idx_8 and idx_10 is created:
  <bb 2> [1.00%]:
  goto <bb 6>; [100.00%]

  <bb 3> [99.00%]:
  # RANGE [0, 1000] NONZERO 1023
  # idx_10 = PHI <idx_1(4), idx_8(6)>
  __builtin_snprintf (p_4(D), 4, "%d", idx_10);
  # RANGE [1, 1000] NONZERO 1023
  idx_6 = idx_10 + 1;

  <bb 4> [99.00%]:
  # RANGE [0, 1000] NONZERO 1023
  # idx_1 = PHI <idx_6(3)>
  if (idx_1 != 1000)
    goto <bb 3>; [98.99%]
  else
    goto <bb 5>; [1.01%]

  <bb 5> [1.00%]:
  return;

  <bb 6> [1.00%]:
  # RANGE [0, 1000] NONZERO 1023
  # idx_8 = PHI <0(2)>
  goto <bb 3>; [100.00%]

But there is no obvious connection between idx_10 (which indeed nicely could hold the [0, 999] range) and idx_7 (the already removed ASSERT_EXPR); similarly, idx_8 could very well have RANGE [0, 0] but doesn't (though in that case it isn't that big a deal, because it is going to be removed immediately afterwards).
Comment 8 Jakub Jelinek 2017-05-24 07:22:40 UTC
idx_10 addition is a consequence of TODO_update_ssa in vrp1's todo_flags, triggered by jump threading creating the bb6.
Comment 9 Martin Sebor 2017-05-31 17:31:27 UTC
*** Bug 80924 has been marked as a duplicate of this bug. ***