ari@eleazar.dartmouth.edu (Ari Halberstadt) (01/11/91)
Summary A method to crash many application programs using the dialog manager is outlined, several solutions are discussed, and finally a code fragment illustrates a solution which may be incorporated into your own programs. The Problem Just when I thought my program was super-robust, I discovered an amazingly simple way to crash it. In fact, this is an amazingly simple way to crash many programs, including Stuffit 1.5.1 and Disinfectant. All you need is a Macintosh (preferably with a 68000 CPU, like a Plus or SE, but any Mac will do), practically any version of the System, though 6.0.5 is perfect, an application, a floppy disk, and some spare time. The method: 1. Copy program to floppy 2. Launch from floppy 3. Eject floppy 4. Do something that will pop up an alert or, preferably, a dialog. 5. When the Macintosh tells you to insert the disk containing the program, hit command-period several times, until the system gives up. 6. At this point, the program is likely to have crashed with an address error on 68000 machines, or in some other mysterious way on other Macintosh's. I advise doing this evil trick with a debugger installed so you won't have to reboot. I think you can get programs mounted on Apple share volumes to crash as well, for instance, by unplugging the connection to the server and then waiting for the connection to time out. In fact, any evil way to make the disk disappear (ie, go off line) may cause a similar crash. Actually, to be fair, Disinfectant 2.x doesn't crash, but it does put up an "Out of memory" alert and then exits to shell (the alert is incorrect: there's plenty of memory). The Explanation The DialogManager doesn't check the error returns and result codes returned by the Resource Manager. When the System requests the disk, and you refuse to insert it, it causes the File Manager to return an error to the Resource Manager, which in turn reports the failure to the caller by returning a NULL handle and setting an error code in the low-memory global ResErr. Most intelligent programmers check this return value (using routine ResError) and do something graceful if there's a problem. But Apple didn't do this in the DialogManager. If you could open up the ToolBox, you'd probably see some code like this (only it would be in assembly language): . . . Handle rsrc; Ptr data; rsrc = GetResource('DITL', 1234); HLock(rsrc); /* OH NO! Here we get to crash */ data = *rsrc; /* OK, if not there, then almost definitely here */ . . . One of the TechNotes (I think it's an old one, with a low number, like 15), says you should just ignore error returns from the dialog manager, since the OS itself ignores them. But this means that users can quite innocently crash your application. In fact, there is probably a simple workaround, which is discussed below. Possible Solutions Search for the resources needed by the dialog or alert before you call the dialog manager. This is tricky: you have to be sure to look for all the resources. For instance, if the dialog includes a control template, you have to make sure the control exists. If the dialog needs a picture, you have to make sure the picture exists, etc. etc. This information is stored in various resources, especially the 'DITL' for the dialog. You'll need to write code which knows all sorts of intimate details about the formats of resources. Programmers really shouldn't have to muck around with such things. Write your own dialog manager. Again, this is absurd: you are then responsible for many more aspects of the user interface, and it is difficult to do this correctly. Too much of a burden is placed upon each application developer. Make the alert or dialog, and all the resources it needs, unpurgeable. This may cause problems if memory becomes scarce, since the Memory Manager won't be able to purge the unneeded resources. And what if you're accessing resources in the System file (such as an icon), and the System file is also on the floppy drive? Store an error handler into the ResErrProc low-memory global. When using this method you have to be careful when calling a specific Menu Manager call; I can't remember which one, but it's documented in one of the TechNotes (I'd look it up, but my Mac is 6 miles from here). Most of the time, you won't need to use ResErrProc, but just before making a call to the DialogManager, such as Alert, NoteAlert, or GetNewDialog, you should store a procedure into ResErrProc. For instance, here's an outline of how to use ResErrProc to recover from otherwise fatal errors. The outline assumes THINK C, but similar methods will work with Pascal and MPW. One of the TechNotes even describes a method for executing non-local goto's (which are needed in my solution) from Pascal. During startup, load an error message alert and make it unpurgeable using CouldAlert. Write glue functions for calling all dialog manager functions which could read a dialog or alert template ('DLOG' or 'ALRT' resources) from disk. These include (but are not necessarily limited to) CouldAlert, CouldDialog, Alert, NoteAlert, StopAlert, CautionAlert, and GetNewDialog. Each of these glue functions should look something like the following (I'm not sure of the syntax, as I said, my Mac is 6 miles distant): /* jump buffer used for recovery from errors */ static jump_buf errjump; /* on entry, register D0 will contain the error number (I think) */ void DlgResErrProc(void) { short err; asm { move.w d0, err } longjmp(errjump, err); } /* glue for GetNewDialog */ OSErr DlgGetNew(short id, DialogPtr *dlgp) { long oldproc; /* original value of ResErrProc */ OSErr err; /* error code */ /* the require() macro is identical to assert(), and is used for preconditions; the ensure() macro is used for postcondtions */ require(dlgp != NULL); /* initialize default return values */ err = noErr; *dlgp = NULL; /* save old value so it may be restored when finished */ oldproc = ResErrProc; /* setup jump buffer for a non-local goto from the error procedure */ err = setjmp(errjump); /* if no error then this is the first attempt at loading the dialog */ if (! err) { /* set our error procedure */ ResErrProc = DlgResErrProc; /* call DialogManager to load and create the dialog */ *dlgp = GetNewDialog(id); } else *dlgp = NULL; /* restore ResErrProc to its original value */ ResErrProc = oldproc; ensure(! err ? *dlgp != NULL : *dlgp == NULL); return(err); } To report the error, you probably shouldn't use the dialog manager. Create a fake alert using the window manager, DrawControl, and TextBox to do the updating, and check events in the "OK" button yourself (this shouldn't be too hard, it's just a matter of calling WaitNextEvent and TrackControl). If you do use the dialog manager, be sure not to access it via the above glue functions, since it could then generate an infinite loop (an error occurs, so you call dialog manager to report it, which causes another error, etc. etc.). Also, be sure to use the alert you made unpurgeable during initialization. The above solution has not been tested yet. I was racking my brains earlier, trying to figure out a solution to the problem. Writing this message has really helped me solve this problem. Tomorrow, if I have the time, I may see about implementing the solution. I'd really appreciate hearing what other people have done about this problem, and any comments as to whether this solution will work. My rules of programming (in order of importance): It shall not crash. It shall fulfill its specification. It will try to reuse features and code found in other programs. It will try to have more features and be faster. I'll earn enough to own a villa. Apple makes it very hard to fulfill these rules. Assertions make it easier. TMON also makes it easier. TechNotes sometimes make it easier. Could Apple publish a version of its system software for developers which includes assertions (eg, require/ensure clauses in every ToolBox call)?
mandel@vax.anes.tulane.edu (Jeff E Mandel MD MS) (01/14/91)
In article <1991Jan11.002919.12966@dartvax.dartmouth.edu> ari@eleazar.dartmouth.edu (Ari Halberstadt) writes: >Summary > >A method to crash many application programs using the dialog manager >is outlined, several solutions are discussed, and finally a code >fragment illustrates a solution which may be incorporated into your >own programs. > [Much stuff] > >Possible Solutions > >Search for the resources needed by the dialog or alert before you call >the dialog manager. This is tricky: you have to be sure to look for >all the resources. For instance, if the dialog includes a control >template, you have to make sure the control exists. If the dialog >needs a picture, you have to make sure the picture exists, etc. etc. >This information is stored in various resources, especially the 'DITL' >for the dialog. You'll need to write code which knows all sorts of >intimate details about the formats of resources. Programmers really >shouldn't have to muck around with such things. > I work in C++, and have been writing a lot of XCMDs, where you have the added problem of being a guest in someone else's house. My solution is that I have a class for a dialog within an XCMD, and a generic dialog item class with subclasses for things like editable text, lists, popup menus, etc. The initializer (not the constructor, as it cannot report back failure) for each of these preflights for existence of the needed resources. It took me only a short time to write these routines, and now I just blithely write code knowing clever people cannot crash me. I know that writing robust code is not something most people start out doing, but the tools are there. I personally have written code for a real-time closed loop controller that administered a potentially lethal drug to human beings; you had better believe that I assured myself that some creative user couldn't pop out a floppy and crash that puppy!
upstill@pixar.uucp (It is not enough to win; others must lose. -- G. Vidal) (01/16/91)
If the problem is that the user refuses to offer up the disk with the dialog on it, then I don't see why it's necessary to load ALL the resources associated with the dialog (assuming that they're all on the same file). If I just get, say, the DLOG via the Resource Manager then IMMEDIATELY do the NewDialog call, the user has no chance to eject the disk and deny me access to the remaining resources, nes c'est pas? Sounds like a 90% solution to me. Steve
d88-jwa@nada.kth.se (Jon W{tte) (01/17/91)
In article <1991Jan15.191809.18212@pixar.uucp> upstill@pixar.uucp (It is not enough to win; others must lose. -- G. Vidal) writes: >the same file). If I just get, say, the DLOG via the Resource Manager >then IMMEDIATELY do the NewDialog call, the user has no chance to >eject the disk and deny me access to the remaining resources, >nes c'est pas? Sounds like a 90% solution to me. Plus the DITL, plus any dctb and cicn and ICON and PICT and ... that might be in the dialog. Don't forget the CDEFs that might be used. I've adopted the practice "If the user deletes resources, too bad." If the user doesn't insert the disk... well... don't everybody have hard disks these days ? :-) h+ -- Jon W{tte, Stockholm, Sweden, h+@nada.kth.se