OP commented "a large number of buffers are dynamically allocated, ... ".  The malloc(), realloc(), calloc(), free(), etc could be re-written with a wrapper function that stores the size.
typedef union {
  max_align_t align;
  size_t sz;
} my_header;
void* my_malloc(size_t size) {
  my_header *p = malloc(sizeof *p + size);
  if (p) {
    p->sz = size;
    p++;
  }
  return p;
}
size_t my_size(const void *p) {
  if (p) {
    const my_header *head = p;
    return head[-1].sz;
  }
  return 0;
}
void my_free(void *p) {
  if (p) {
    my_header *head = p;
    free(--head);
  }
}
All other *.c files call some *.h file with
#define malloc my_malloc
#define free my_free
void *my_malloc(size_t size);
void my_free(void *p);
size_t my_size(const void *p);
Now when my_sprintf() is called with an allocated pointer ...
int my_sprintf (char *dest,char *format,...) {
  va_list va;
  va_start(va,format);
  size_t n = my_size(dest);
  return vsnprintf(dest,n,format,va);
}
Further, a magic number could also be pre-pended to help identify if the pointer passed is truly a my_allcoated() one.
Wrapping allocation functions is also a way to determine various allocation concerns: double free, max usage, all pointers free'd, ...    
[Edit] after 5 years.
Code needs to ensure alignment - code re-worked.
With pre-C11 use a union of wide types in lieu of max_align_t.
typedef union {
  double d;
  long l;
  void *p;
  void (*fp)();
  // With C99
  complex long double cld;
  long long ll;
  size_t sz;
} my_header;