[comp.sys.mac.programmer] Common

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