home *** CD-ROM | disk | FTP | other *** search
- Xref: sparky comp.sources.wanted:4940 comp.software-eng:4180 comp.sys.sun.apps:2393
- Newsgroups: comp.sources.wanted,comp.software-eng,comp.sys.sun.apps
- Path: sparky!uunet!nntp1.radiomail.net!fernwood!pure!hastings
- From: hastings@pure.com (Reed Hastings)
- Subject: Purify & Sentinel
- Message-ID: <1992Nov2.212558.28104@pure.com>
- Organization: Pure Software, Los Altos, CA
- Date: Mon, 2 Nov 1992 21:25:58 GMT
- Lines: 668
-
-
-
- In several recent postings, the developers of Sentinel compared their
- product with standard malloc debug and Purify.
-
- We realize that it is inappropriate for vendors to argue about their
- products on the net, and wish that Sentinel confined their postings to
- comp.newprod. Given that Sentinel has posted, though, to a wide
- number of groups with a distorted product comparison we have been left
- with no choice but to respond. Hopefully all further traffic will be
- from independent customers, and not from Sentinel or us.
-
- In the following message, we clarify a number of points about Purify
- and indicate some critical memory errors that Purify detects that
- Sentinel can not.
-
-
- ******** Purify and Sentinel *********
-
- Sentinel is a debugging version of malloc sold by Virtual
- Technologies. Sentinel is based on the public domain dbmalloc
- package, with the improvement of commercial support and the ability to
- read symbol tables to print function names. Standard malloc
- debuggers, which have been around for many years, print only the code
- addresses.
-
- Sentinel operates by providing their own version of 29 selected libc
- functions, such as strcpy, in which they perform their memory error
- checks. Sentinel's ONLY memory checking is through calls to these
- functions. Every other function in your application runs unchecked.
-
- Purify, in contrast, monitors and checks *every* memory access a
- program makes (such as those from statements like "int x = *y;";). It
- detects memory errors just *before* they are about to happen, and
- stops the program with an error message at that point.
-
- Purify's object code insertion is the key technology that allows
- Purify to check every memory access a program makes with very low
- overhead. This includes all memory accesses from libc, all accesses
- from your application code, all memory accesses from system calls, and
- all memory accesses from third party libraries such as database
- libraries and Xt libraries.
-
- In addition to checking every memory access, Purify's approach allows
- it to detect entire classes of serious errors that debugging mallocs
- such as Sentinel cannot detect.
-
-
- ******** Specific Functionality *********
-
- 1. Array bounds reads Purify yes Sentinel no
- 2. Array bounds writes Purify yes Sentinel partially
- 3. Free'd memory reads Purify yes Sentinel no
- 4. Free'd memory writes Purify yes Sentinel no
- 5. Uninitialized memory reads Purify yes Sentinel no
- 6. Memory leaks Purify yes Sentinel no
- 7. System call protection Purify yes Sentinel no
- 8. Shared Library support Purify yes Sentinel some
- 8. Stack corruption Purify no Sentinel partially
- 9. User memory manager support Purify yes Sentinel no
- 10.Watchpoints Purify yes Sentinel partially
-
- ******** Array Bounds Reads *********
-
- Unknowingly reading past the end of an array is a common cause of
- program crashes.
-
- Here is an example:
-
- main() {
- char* x = malloc(100);
- get_sorted_data(x); /* fill x with 100 small integers sorted */
- if (x[100] == MAX) { /* Is the biggest one too big? */
- abort();
- }
- }
-
-
- At the customer site the program crashes once every three days or so,
- because the random value at x[100] happens to be equal to the
- constant MAX.
-
- Running this example with Sentinel reports no errors of any kind.
-
- Running this example just once though Purify, it reports:
-
- ---- Purify'd a.out ----
- Purify (abr): array bounds read violation:
- * This is occurring while in:
- main (line 5 in foo.c)
- start (0x2064 in crt0.o)
- * Reading 1 byte from 0x45afc in the heap.
- 1 byte past end of a malloc'd block at 0x45a98 of 100 bytes.
- * This block was allocated by malloc called from:
- main (line 3 in foo.c)
- start (0x2064 in crt0.o)
-
-
- The example used above has
-
- if (x[100] == MAX) {
- abort();
- }
-
- A more realistic example, and much tougher to debug, is
-
- if (x[100] == MAX) {
- do_something_very_subtly_wrong()
- }
-
- Again, Purify detects this error immediately. Moreover, Purify
- stops the program just *before* the error occurred, allowing
- you to look around with a debugger.
-
-
-
- ******** Array Bounds Writes *********
-
- Sentinel detects some array bounds write violations by putting the
- bits (11110111) in the byte immediately after the array. At the
- end of the program Sentinel searches every memory block to see
- if any of the 11110111 patterns have changed.
-
- Here's an example:
-
- char* x;
-
- setup()
- {
- x = malloc(10);
- }
-
- do_work()
- {
- x[10] = 0; /* buggy, line 14 */
- }
-
- main()
- {
- setup();
- do_work();
- }
-
- Running this program with Sentinel, it reports:
-
- SENTINEL: Warning [23]: The program has modified the boundary area
- following the 10 byte data area that starts at location 0x20F80.
-
- Reading symbol table...Sun format.........Done
-
- This problem was detected at the following location:
-
- SeLeakCheckpoint()+0xA8 [leak.o]
- SeLeakShow()+0x4 [leak.o]
- _m_LeakReport()+0x24 [leak.o]
- exit()+0xB0 [exit.o]
- Segmentation fault
-
-
- Even in this simple example Sentinel can't identify that the error
- occured in do_work. Because Sentinel does not have object code
- insertion, it cannot detect errors as they occur. In this example,
- Sentinel itself gets a SEGV and dumps core trying to print its
- diagnostic output (this is a bug in Sentinel v1.3.1 that will likely
- get fixed.)
-
- Running this simple example with Purify, it reports:
-
- ---- Purify'd a.out ----
- Purify (abw): array bounds write violation:
- * This is occurring while in:
- do_work (line 14 in example.c)
- main (line 20 in example.c)
- start (0x2064 in crt0.o)
- * Writing 1 byte to 0x45a92 in the heap.
- 1 byte past end of a malloc'd block at 0x45a88 of 10 bytes.
- * This block was allocated by malloc called from:
- setup (line 9 in example.c)
- main(line 19 in example.c)
- start (0x2064 in crt0.o)
-
- Note that Purify correctly identified do_work, line 14, as the
- source of the error. Again, Purify detects this error immediately.
- Moreover, Purify stops the program just *before* the error occurred,
- allowing you to look around with a debugger.
-
- ******** Free'd memory reads *********
-
- As memory ownership protocols get more complicated, it becomes harder
- and harder to be sure that memory is not being freed too early.
- Using Purify, if you access memory *after* it has been freed you
- get an exact immediate diagnostic.
-
- Here is an example:
-
- main(){
- struct flags { int present; };
- struct flags* flags = malloc(sizeof(struct flags));
- flags->present = TRUE;
- /* ... */
- if (flags->present) {
- printf("present\n");
- }
- free(flags);
- /* ... */
- if (!flags->present) { /* line 11 */
- abort(); /* or something subtle */
- }
- }
-
- The subtle part of this example is that the code will run
- correctly for awhile since the memory flags *probably* has not been
- re-allocated and changed.
-
- Running this example with Sentinel reports no errors of any kind.
-
- The first time Purify is run on this code you get an exact diagnostic,
- telling you where the memory was freed, where it was allocated, and
- where the free'd memory is being used (line 11).
-
- ---- Purify'd a.out ----
- Purify (fmr): free memory read violation:
- * This is occurring while in:
- main (line 11 of fmr.c)
- start(0x2064 of crt0.o)
- * Reading 4 bytes from 0x39810 in the heap.
- at beginning of a freed block of 4 bytes.
- * This block was allocated by malloc called from:
- main (line 3 of fmr.c)
- start(0x2064 of crt0.o)
- * This block was freed 1 calls to free ago, from:
- main (line 9 of fmr.c)
- start(0x2064 of crt0.o)
-
-
-
- ******** Free'd memory writes *********
-
- Free'd memory writes are especially dangerous because if that memory
- gets reallocated then random memory from some other part of the
- program will get corrupted.
-
- Using Purify, if you write memory after it has been freed you
- get an exact immediate diagnostic just *before* it happens.
-
- Here is an example:
-
- main(){
- struct flags { int present; };
- struct flags* flags = malloc(sizeof(struct flags));
- flags->present = TRUE;
- /* ... */
- if (flags->present) {
- printf("present\n");
- }
- free(flags);
- /* ... */
- flags->present = FALSE; /* I wonder whose memory we are changing */
- }
-
- Running this example with Sentinel reports no errors of any kind.
-
- Running this example just once though Purify, it reports:
-
- ----Purify'd a.out ----
- Purify (fmw): free memory write violation:
- * This is occurring while in:
- main (line 11 of fmw.c)
- start(0x2064 of crt0.o)
- * Reading 4 bytes from 0x39810 in the heap.
- at beginning of a freed block of 4 bytes.
- * This block was allocated by malloc called from:
- main (line 3 of fmw.c)
- start(0x2064 of crt0.o)
- * This block was freed 1 calls to free ago, from:
- main (line 9 of fmw.c)
- start(0x2064 of crt0.o)
-
- ******** Uninitialized memory reads *********
-
- Uninitialized memory reads are another common case of mysterious
- and non-reproducible crashes. Reading uninitialized memory returns
- a random value, and the results are unpredictable.
-
- Here is an example:
-
-
- main(){
- int flag, i = 0;
- /* ... */
- while(i++ < 10 && flag) {
- update_db();
- }
- }
-
- Running this example with Sentinel reports no errors of any kind.
-
- Running this example though Purify, it reports:
-
-
- ----Purify'd umr.pure----
- Purify (umr): uninitialized memory read violation:
- * This is occurring while in:
- main(pc = 0x1ab44 on line 4 of umr.c)
- start(pc =0x2064 of crt0.o)
- * Reading 4 bytes from 0xf7fff9dc on the stack.
- This is local variable "flag" in function main.
-
- ******** Memory Leaks *********
-
- Memory leaks are allocated memory that should have been freed but were
- not. This is not the same as the memory allocated but not yet freed.
- Many programs and libraries allocate memory that is never meant to be
- freed. These might be symbol tables, contents of initialization
- files, etc. This memory is always in use.
-
- Sentinel does not report memory leaks, it reports memory in use,
- memory that has been allocated but not freed. Typically some small
- fraction of this memory is leaks, but Sentinel cannot and does not
- tell you which memory is still actively in use, and which memory is
- truly leaked.
-
- Purify can show you the memory in use (with purify_allallocated()), or
- the memory leaks (with purify_newleaks()). It can find the difference
- between these two groups by searching through all of memory tracing
- pointers. Sentinel does not do this.
-
- Being able to tell the difference between leaks and memory in use is
- critical, and only Purify is able to do it. In Xt applications we
- have studied there are an average of five thousand individual memory
- chunks active, even after closing windows. These hold things like the
- resource database. Sentinel (or Purify with purify_allallocated())
- prints for you a list of all 5 thousand memory chunks. It is up to
- you the developer to manually sort through this mess to find or guess
- what the 20 or so leaks are. Purify's purify_allleaks() quickly and
- precisely identifies those 20 leaks, and does not report falsely on
- the 4980 chunks that are still in use and are not leaks as Sentinel
- does.
-
- Here is a short example:
-
- int* table = 0;
-
- Malloc(x) {
- char* val = malloc(x);
- assert(val);
- return val;
- }
-
- example() {
- static int first = TRUE;
- char* local;
- if (first) { /* first time, build tables */
- int i;
- first = FALSE;
- table = Malloc(400);
- for(i = 1; i < 100; i++)
- table[i] = Malloc(i);
- }
- local = Malloc(43); /* oops, leak here */
- }
-
-
- In this small example table is a long-term data structure that is
- built when needed.
-
- Running this through Sentinel you get the following display, and you get
- to sort through this to attempt to find the one leak.
-
-
- *************** SENTINEL: LIST OF MEMORY LEAKS**************
-
- POINTER LOCATION ALLOC DATA
- TO DATA WHERE ALLOCATED FUNCT LENGTH
- -------- ------------------------------- -------------- --------
- 0x01E8E0 leak.c:6 Malloc() malloc(93) 90
- 0x01E988 leak.c:6 Malloc() malloc(92) 89
- 0x01EA20 leak.c:6 Malloc() malloc(91) 88
- 0x01EAB8 leak.c:6 Malloc() malloc(90) 87
- 0x01EB48 leak.c:6 Malloc() malloc(89) 86
- 0x01EBD8 leak.c:6 Malloc() malloc(88) 85
- 0x01EC68 leak.c:6 Malloc() malloc(87) 84
- 0x01ECF8 leak.c:6 Malloc() malloc(86) 83
- 0x01ED88 leak.c:6 Malloc() malloc(85) 82
- 0x01EE18 leak.c:6 Malloc() malloc(84) 81
- 0x01EEA8 leak.c:6 Malloc() malloc(83) 80
- 0x01EF38 leak.c:6 Malloc() malloc(82) 79
- 0x01EFC0 leak.c:6 Malloc() malloc(81) 78
- 0x01F048 leak.c:6 Malloc() malloc(80) 77
- 0x01F0D0 leak.c:6 Malloc() malloc(79) 76
- 0x01F158 leak.c:6 Malloc() malloc(78) 75
- 0x01F1E0 leak.c:6 Malloc() malloc(77) 74
- 0x01F268 leak.c:6 Malloc() malloc(76) 73
- 0x01F2F0 leak.c:6 Malloc() malloc(75) 72
- 0x01F378 leak.c:6 Malloc() malloc(74) 71
- 0x01F3F8 leak.c:6 Malloc() malloc(73) 70
- 0x01F478 leak.c:6 Malloc() malloc(72) 69
- 0x01F4F8 leak.c:6 Malloc() malloc(71) 68
- 0x01F578 leak.c:6 Malloc() malloc(70) 67
- 0x01F5F8 leak.c:6 Malloc() malloc(69) 66
- 0x01F678 leak.c:6 Malloc() malloc(68) 65
- 0x01F6F8 leak.c:6 Malloc() malloc(67) 64
- 0x01F778 leak.c:6 Malloc() malloc(66) 63
- 0x01F7F0 leak.c:6 Malloc() malloc(65) 62
- 0x01F868 leak.c:6 Malloc() malloc(64) 61
- 0x01F8E0 leak.c:6 Malloc() malloc(63) 60
- 0x01F958 leak.c:6 Malloc() malloc(62) 59
- 0x01F9D0 leak.c:6 Malloc() malloc(61) 58
- 0x01FA48 leak.c:6 Malloc() malloc(60) 57
- 0x01FAC0 leak.c:6 Malloc() malloc(59) 56
- 0x01FB38 leak.c:6 Malloc() malloc(58) 55
- 0x01FBA8 leak.c:6 Malloc() malloc(57) 54
- 0x01FC18 leak.c:6 Malloc() malloc(56) 53
- 0x01FC88 leak.c:6 Malloc() malloc(55) 52
- 0x01FCF8 leak.c:6 Malloc() malloc(54) 51
- 0x01FD68 leak.c:6 Malloc() malloc(53) 50
- 0x01FDD8 leak.c:6 Malloc() malloc(52) 49
- 0x01FE48 leak.c:6 Malloc() malloc(51) 48
- 0x01FEB8 leak.c:6 Malloc() malloc(50) 47
- 0x01FF20 leak.c:6 Malloc() malloc(49) 46
- 0x01FF88 leak.c:6 Malloc() malloc(48) 45
- 0x01FFF0 leak.c:6 Malloc() malloc(47) 44
- 0x020058 leak.c:6 Malloc() malloc(46) 43
- 0x0200C0 leak.c:6 Malloc() malloc(45) 42
- 0x020128 leak.c:6 Malloc() malloc(44) 41
- 0x020190 leak.c:6 Malloc() malloc(43) 40
- 0x0201F8 leak.c:6 Malloc() malloc(42) 39
- 0x020258 leak.c:6 Malloc() malloc(41) 38
- 0x0202B8 leak.c:6 Malloc() malloc(40) 37
- 0x020318 leak.c:6 Malloc() malloc(39) 36
- 0x020378 leak.c:6 Malloc() malloc(38) 35
- 0x0203D8 leak.c:6 Malloc() malloc(37) 34
- 0x020438 leak.c:6 Malloc() malloc(36) 33
- 0x020498 leak.c:6 Malloc() malloc(35) 32
- 0x0204F8 leak.c:6 Malloc() malloc(34) 31
- 0x020550 leak.c:6 Malloc() malloc(33) 30
- 0x0205A8 leak.c:6 Malloc() malloc(32) 29
- 0x020600 leak.c:6 Malloc() malloc(31) 28
- 0x020658 leak.c:6 Malloc() malloc(30) 27
- 0x0206B0 leak.c:6 Malloc() malloc(29) 26
- 0x020708 leak.c:6 Malloc() malloc(28) 25
- 0x020760 leak.c:6 Malloc() malloc(27) 24
- 0x0207B8 leak.c:6 Malloc() malloc(26) 23
- 0x020808 leak.c:6 Malloc() malloc(25) 22
- 0x020858 leak.c:6 Malloc() malloc(24) 21
- 0x0208A8 leak.c:6 Malloc() malloc(23) 20
- 0x0208F8 leak.c:6 Malloc() malloc(22) 19
- 0x020948 leak.c:6 Malloc() malloc(21) 18
- 0x020998 leak.c:6 Malloc() malloc(20) 17
- 0x0209E8 leak.c:6 Malloc() malloc(19) 16
- 0x020A38 leak.c:6 Malloc() malloc(18) 15
- 0x020A80 leak.c:6 Malloc() malloc(17) 14
- 0x020AC8 leak.c:6 Malloc() malloc(16) 13
- 0x020B10 leak.c:6 Malloc() malloc(15) 12
- 0x020B58 leak.c:6 Malloc() malloc(14) 11
- 0x020BA0 leak.c:6 Malloc() malloc(13) 10
- 0x020BE8 leak.c:6 Malloc() malloc(12) 9
- 0x020C30 leak.c:6 Malloc() malloc(11) 8
- 0x020C78 leak.c:6 Malloc() malloc(10) 7
- 0x020CB8 leak.c:6 Malloc() malloc(9) 6
- 0x020CF8 leak.c:6 Malloc() malloc(8) 5
- 0x020D38 leak.c:6 Malloc() malloc(7) 4
- 0x020D78 leak.c:6 Malloc() malloc(6) 3
- 0x020DB8 leak.c:6 Malloc() malloc(5) 2
- 0x020DF8 leak.c:6 Malloc() malloc(4) 1
- 0x020E38 leak.c:6 Malloc() malloc(1) 400
- 0x0274A0 leak.c:6 Malloc() malloc(103) 43
- 0x027508 leak.c:6 Malloc() malloc(102) 99
- 0x0275A8 leak.c:6 Malloc() malloc(101) 98
- 0x027648 leak.c:6 Malloc() malloc(100) 97
- 0x0276E8 leak.c:6 Malloc() malloc(99) 96
- 0x027788 leak.c:6 Malloc() malloc(98) 95
- 0x027820 leak.c:6 Malloc() malloc(97) 94
- 0x0278B8 leak.c:6 Malloc() malloc(96) 93
- 0x027950 leak.c:6 Malloc() malloc(95) 92
- 0x0279E8 leak.c:6 Malloc() malloc(94) 91
-
- Running this through Purify you get a single memory leak report and
- can fix the problem confident that you're not introducing another
- problem by freeing memory still in use.
-
- Purify: Searching for all memory leaks...
- 1 memory leak found.
-
- Report (mlk): 43 bytes at 0x3a570 lost, malloc called from:
- example (line 13 in ./leak.c)
- main (line 17 in ./leak.c)
- start (crt0.o)
-
-
- ******* System Call Protection ********
-
-
- Purify checks the arguments to the system calls which read or write memory.
- Here is an example:
-
- example() {
- struct stat* statp = malloc(sizeof(statp));
- stat(".cshrc", statp);
- }
-
- Running this example with Sentinel reports no errors of any kind.
-
- Running this example though Purify, it reports:
-
- Purify (abw): array bounds write violation:
- * This is occurring while in:
- stat(syscalls.o)
- main (line 19 in example.c)
- start(0x2064 in crt0.o)
- * Writing 64 bytes to 0x45a88 in the heap.
- at beginning of a malloc'd block of 4 bytes.
- * This block was allocated by malloc called from:
- main (line 18 in example.c)
- start(pc =0x2064 in crt0.o)
-
-
- A careful look at the malloc call reveals that sizeof(statp) should
- have been sizeof(struct stat).
-
-
-
- ******* Shared Library Support *********
-
- Sentinel does support programatic use of shared libraries, such as using
- dlopen.
-
- Consider the following example code placed in a shared library and opened with
- dlopen:
-
- printsomething()
- {
- doprint();
- }
-
- doprint()
- {
- char* y = malloc(5);
- strcpy(y, "Too long"); /* simple strcpy error */
- }
-
- Running this example with a simple main calling printsomething, Sentinel reports:
-
- SENTINEL: Warning [14]: An attempt was made to access data beyond the
- end of the allocated data section.The program attempted to write 9
- bytes to location 0x1ED78.That address is at offset 0 in the 5
- byte data area that starts at location 0x1ED78 (there is only
- room to write 5 bytes).
-
- This problem was detected at the following location:
-
- strcpy()+0x14C [string.o]
- etext()+0xF76B8FE8 [iob.o]
- etext()+0xF76B8FB0 [iob.o]
- main() [hello.c:31]
-
- This problem is *probably* associated with a 5 byte data area
- allocated on the 9th call to malloc() which returned 0x1ED78.
- The context of the call to malloc() was as follows:
-
- malloc()+0x20 [malloc.o]
- etext()+0xF76B8FD0 [iob.o]
- etext()+0xF76B8FB0 [iob.o]
- main() [hello.c:31]
-
- Note that Sentinel prints shared library function names as "etext()+0xF76B8FE8".
-
- Running the same example with Purify, it reports:
-
- ----Purify'd prog.pure ----
- Purify (abw): array bounds write violation:
- * This is occurring while in:
- strcpy (strings.o)
- doprint (line 12 of dlopen.c)
- printsomething (line 6 of dlopen.c)
- main (line 31 of hello.c)
- start(crt0.o)
- * Writing 9 bytes to 0x3bea0 in the heap.
- at beginning of a malloc'd block of 5 bytes.
- * This block was allocated by malloc called from:
- doprint (line 10 of dlopen.c)
- printsomething (line 6 of dlopen.c)
- main (line 31 of hello.c)
- start(crt0.o)
-
-
- ******* Stack Corruption *******
-
- Sentinel can be configured to walk up the stack every time it enters
- one of its 29 checking functions. It can detect cases of stack
- corruption, if the program doesn't crash between the time of the
- corruption and time of the call to the check function.
-
- Purify cannot directly help with stack corruption problems.
-
- ******* User Memory Manager support ********
-
-
- Sentinel does not support user-defined memory managers.
-
- Purify supports user memory managers layered on top of malloc with a
- simple extension language.
-
- ******* Watchpoints ********
-
- Sentinel's "watchpoints" check whether watched memory has changed
- whenever one of its libc function variants are entered. Sentinel
- tells you "it changed sometime in the past".
-
- Purify signals a message just before the exact instruction executes
- that is about to change the watched memory. Purify tells you the
- current value and the value it is about to change to. Purify allows
- the program to be stopped there, permitting you to look around with a
- debugger.
-
- ******** Pricing *********
-
- Sentinel is CPU-licensed, and costs $400 per cpu, which translates
- to $400 per user. VTI is willing to compromise signficantly on
- these prices.
-
- Purify is floating network licensed. Our customers buy and use
- approximately one license per 3 to 4 developers. Each license costs
- between $1950 and $2750 depending on volume. This results in per user
- costs of $500 to $700.
-
-
-
- ******** Summary *********
-
- Judy Grass from Bell Labs gets to the heart of the issue in
- IEEE Software, Aug 92, P125:
-
- "An entire month of working with a debugging
- malloc did not tell me as much about the
- program's problems as a single day spent
- with Purify"
-
- Sentinel is a good implementation of an relatively ineffective approach,
- It can be useful in situations in which there's no other choice, such
- as on platforms on which products like Purify or Centerline are not
- yet available.
-
- Unfortunately, VTI marketing is misleading, and its implications of
- matching Purify's functionality are unsubstantiated. VTI claimes that
- Sentinel offers most of the functionality of Purify for 20% of the
- price. In fact it offers a small fraction of the functionality for
- half of the price on a typical per-user basis.
-
- Purify's creators have previously written many debugging mallocs, as
- have many other developers. We grew tired of debugging mallocs'
- limited abilities and this is what drove us to do the hard work of
- object code insertion. It's the only way to get comprehensive
- checking. Purify will help you get from prototype to product faster
- than any other run-time error detection technology.
-
- Purify continues to be improved. We have always done our best to be
- completely honest about what it did and didn't do. We took pains in
- our manual to describe specifically what kind of errors Purify can not
- find. We don't abuse the net by advertising our products there,
- instead preferring to let our customers do the talking. We thank the
- thousands of Purify customers out there for helping us with
- the above examples.
-
- -Reed Hastings, President, Pure Software.
- hastings@pure.com
-
-
-