Locks, FileInfoBlock and Examine

A lock is an access right on a file system object (a file or a directory). While a lock exists, the object cannot be deleted, and depending on the lock type it cannot be opened or locked by anyone else. Locks are also the way AmigaDOS names objects without paths: a lock on a directory plus a short relative name identifies an object at any nesting depth, which is how programs work around the 255-character path limit that most dos.library functions inherit from BSTRs.

This page covers the lock API of Chapter 6 of the RKM AmigaDOS manual (obtaining, duplicating, comparing and converting locks, opening files from locks, the FileLock structure) and the per-object inspection functions of §7.1 (Examine(), ExamineFH() and the FileInfoBlock). Directory iteration (ExNext(), ExAll()) is covered in Directory Scanning; Info() and volume-level operations in Volume Administration.

Shared and exclusive locks

Lock() takes one of four access modes from dos/dos.h; two of them are synonyms:

Mode Value Meaning
SHARED_LOCK 1.3+ −2 Any number of shared locks may coexist on the object
ACCESS_READ 1.3+ −2 Synonym of SHARED_LOCK
EXCLUSIVE_LOCK 1.3+ −1 Only this lock may exist on the object
ACCESS_WRITE 1.3+ −1 Synonym of EXCLUSIVE_LOCK

Rules:

  • An exclusive lock excludes every other lock. Locking an exclusively locked object fails with ERROR_OBJECT_IN_USE; so does an attempt to lock exclusively an object that already has a shared lock.
  • Prefer shared locks. Release every lock as early as possible, so that other processes can open, overwrite or delete the object.
  • A shared lock on a file does not protect its contents: it prevents the file from being replaced (MODE_NEWFILE), but the file can still be opened in any other mode and modified. An exclusively locked file cannot be opened by name at all, only through OpenFromLock() (packet level: only ACTION_FH_FROM_LOCK). An exclusively locked directory cannot be deleted, but objects inside it can still be created, deleted or changed (§14.2.1).
  • File systems should treat an invalid mode passed to Lock() as SHARED_LOCK, because some programs pass garbage. Since the root directory is also reachable as the ZERO lock, an exclusive lock on a volume root is "probably not very exclusive".
  • Links are never locked. Hard and soft links are resolved as the lock is acquired, so a lock always refers to the link target.
  • As long as at least one lock on a volume exists, its file system keeps the volume in the device list; Workbench therefore keeps showing the volume icon even after the medium is ejected.

The ZERO lock

The lock value ZERO (0) is special. In principle it stands for SYS:; in reality it is the root directory of the file system in pr_FileSystemTask of the calling process, which is usually, but not necessarily, the same volume as the SYS: assign. Many functions treat it inconsistently:

Function Behaviour with ZERO
DupLock(ZERO) Returns ZERO without touching IoErr(), indistinguishable from failure unless you clear IoErr() first
ParentDir(ZERO) Returns ZERO; IoErr() is 0 (no parent, not an error)
UnLock(ZERO) No-op
Examine(ZERO, fib) Examines the root of the default file system of the process
SameLock() 2.0+ Does not recognise ZERO as equal to a lock on SYS:; do not pass ZERO
NameFromLock(ZERO, …) Produces "SYS:", which is wrong if SYS: or pr_FileSystemTask was changed
ExNext() / ExAll() ZERO is not usable for scanning; see Directory Scanning

At the packet level, ZERO in a lock argument means "the root of the volume currently inserted in this handler", not the boot volume. In particular, the dvp_Lock returned by GetDeviceProc() may be ZERO and then refers to the root of the file system behind dvp_Port (see The AmigaDOS Device List).

Obtaining and releasing locks

BPTR Lock(STRPTR name, LONG accessMode);
BPTR DupLock(BPTR lock);
BPTR ParentDir(BPTR lock);
BPTR CreateDir(STRPTR name);
void UnLock(BPTR lock);
BPTR DupLockFromFH(BPTR fh);    /* V36 */
BPTR ParentOfFH(BPTR fh);       /* V36 */
BPTR OpenFromLock(BPTR lock);   /* V36 */
Function Returns Notes
Lock() 1.3+ Lock of the requested type, or ZERO Path is absolute or relative to the current directory
DupLock() 1.3+ New shared lock Only shared locks can be duplicated; an exclusive lock gives ERROR_OBJECT_IN_USE. Never copy a FileLock by copying its memory, which can crash the system
ParentDir() 1.3+ Shared lock on the parent directory (directory of a file) ZERO for the root or the ZERO lock (then IoErr() = 0) or on error (IoErr() ≠ 0, e.g. out of memory or parent locked exclusively). Does not release its argument
CreateDir() 1.3+ Exclusive lock on the new directory Creates only the last component, never intermediate directories. Existing object: ERROR_OBJECT_EXISTS (some file systems wrongly return ERROR_OBJECT_IN_USE if the existing directory is locked). Flat file systems do not support directories
UnLock() 1.3+ nothing ZERO is fine. Does not set IoErr(). Never unlock the lock installed as CurrentDir()
DupLockFromFH() 2.0+ Shared lock on the open file Fails for files opened with MODE_NEWFILE (implicit exclusive lock). The handle stays valid
ParentOfFH() 2.0+ Shared lock on the directory of the open file Works even for MODE_NEWFILE files; fails if the parent is exclusively locked
OpenFromLock() 2.0+ File handle See below

On success, IoErr() is set to an undefined value by all functions above except UnLock() (which never touches it) and DupLock(ZERO) (which leaves it alone). Only test IoErr() after a failure.

The claim in older documentation that CreateDir() deletes a conflicting file or directory is wrong; it fails instead.

Duplicating a lock that may be ZERO

#include <dos/dos.h>
#include <proto/dos.h>

/* Returns the copy. *error is 0 on success, including the case where
 * 'lock' was ZERO and the copy is therefore ZERO as well. */
BPTR DupLockZeroSafe(BPTR lock, LONG *error)
{
    BPTR copy;

    SetIoErr(0);               /* DupLock(ZERO) leaves IoErr() untouched */
    copy = DupLock(lock);
    *error = copy ? 0 : IoErr();
    return copy;
}

Telling the root from a ParentDir() failure

BPTR parent = ParentDir(lock);

if (parent == 0) {
    LONG err = IoErr();
    if (err == 0) {
        /* 'lock' is the root directory (or ZERO): no parent */
    } else {
        /* real failure: err holds the error code */
    }
}

Note that ParentDir() and locking the path "/" relative to a lock differ only at the root: the latter fails with ERROR_OBJECT_NOT_FOUND (§14.2.3).

Changing the mode of a lock or file handle

BOOL ChangeMode(ULONG type, BPTR object, ULONG newmode);   /* V36 */

type is CHANGE_LOCK (0) for a lock or CHANGE_FH (1) for a file handle; newmode is SHARED_LOCK or EXCLUSIVE_LOCK. An exclusive lock can always be downgraded to shared; a shared lock (or handle) can be upgraded to exclusive only if it is the only lock on the object.

Caveats:

  • The function is unreliable for file handles: the RAM-Handler does not interpret newmode correctly for CHANGE_FH.
  • At the packet level (ACTION_CHANGE_MODE, 1028), dp_Arg2 is the BPTR to the FileHandle itself, not its fh_Arg1 as with every other file packet. File systems should also accept ACTION_FINDINPUT/ACTION_FINDUPDATE as "shared" and ACTION_FINDOUTPUT as "exclusive", because callers pass those.
  • Passing the wrong type for the object may crash the system.

Note: the NDK 3.2 autodoc states that ChangeMode() did not work in V36 (2.02 and earlier, possibly crashing), and that CHANGE_FH on a handle opened on NIL: crashes up to dos.library 47.23 (fixed in 47.24).

Comparing locks

SameLock()

LONG SameLock(BPTR lock1, BPTR lock2);   /* V36 */
Result Value Meaning
LOCK_SAME 2.0+ 0 Same object
LOCK_SAME_VOLUME 2.0+ 1 Different objects on the same volume (LOCK_SAME_HANDLER is an obsolete alias)
LOCK_DIFFERENT 2.0+ −1 Objects on different volumes
  • IoErr() is not set consistently.
  • ZERO is not recognised as identical to a lock on SYS:; do not pass ZERO for either argument.
  • dos.library first checks whether both locks belong to the same file system; only then does it send ACTION_SAME_LOCK (40). If the file system does not implement that packet, dos.library falls back to comparing fl_Key, which is only meaningful for file systems where fl_Key uniquely identifies the object.

On V34 and earlier, where SameLock() does not exist, the manual offers this limited replacement. It is correct only for the ROM file system, the FFS and the RAM-Handler of V34 and below, because it relies on fl_Key:

#include <dos/dos.h>
#include <dos/dosextens.h>

LONG SameLockV34(BPTR lock1, BPTR lock2)
{
    struct FileLock *fl1 = BADDR(lock1);
    struct FileLock *fl2 = BADDR(lock2);

    if (fl1 == fl2)                        /* includes two ZERO locks */
        return LOCK_SAME;

    if (fl1 && fl2) {                      /* does not treat ZERO as SYS: */
        if (fl1->fl_Volume != fl2->fl_Volume) return LOCK_DIFFERENT;
        if (fl1->fl_Task   != fl2->fl_Task)   return LOCK_DIFFERENT;
        if (fl1->fl_Key    != fl2->fl_Key)    return LOCK_SAME_VOLUME;
        return LOCK_SAME;
    }
    return LOCK_DIFFERENT;                 /* not fully correct */
}

Note: the NDK 3.2 autodoc states that in V36 SameLock() returned LOCK_SAME_VOLUME for different volumes on the same handler.

SameDevice()

BOOL SameDevice(BPTR lock1, BPTR lock2);

Returns non-zero if the file systems owning the two locks run on the same unit of the same exec device, even if they are different partitions. Only device name and unit are compared, so it cannot tell whether two locks are on the same physical volume. It returns 0 if either file system or its device cannot be identified, and never sets IoErr(). The result can become stale: a floppy moved from DF0: to DF1: changes its device after the call. The intended use is copy tools deciding whether source and destination can be accessed in parallel.

Note: the NDK 3.2 autodoc marks SameDevice() as V37 and states that the V36 entry always returned 0.

Locks and files

A file handle is normally backed by an internal lock on the opened file. The lock type follows the open mode:

Open mode Implied lock
MODE_OLDFILE 1.3+ SHARED_LOCK
MODE_READWRITE 2.0+ SHARED_LOCK
MODE_NEWFILE 1.3+ EXCLUSIVE_LOCK

MODE_READWRITE mapping to a shared lock goes back to a V36 RAM-Handler defect that the FFS then copied. In V34 and earlier it implied an exclusive lock. On V36+ the way to get exclusive access to a file without truncating it is to lock it exclusively and then open it from the lock.

Opening a file from a lock

OpenFromLock() opens the locked object as a file. On success the lock is absorbed into the handle and released by Close(); do not UnLock() it. On failure it returns ZERO and the lock remains yours. It fails if the lock refers to a directory.

#include <dos/dos.h>
#include <proto/dos.h>

/* Open an existing file for exclusive access without truncating it.
 * Requires V37: OpenFromLock() does not work in V36. */
BPTR OpenExclusive(STRPTR name)
{
    BPTR lock, fh = 0;

    if ((lock = Lock(name, EXCLUSIVE_LOCK)) != 0) {
        if ((fh = OpenFromLock(lock)) == 0)
            UnLock(lock);              /* not absorbed on failure */
    }
    return fh;
}

OpenFromLock() does not work in V36, because of an incomplete implementation in the ROM file systems and defects in dos.library; it was fixed in V37. For V34, which lacks the function, the manual gives a workaround that only handles shared locks: make the lock the current directory and open the empty name, which every AmigaDOS file system interprets as "the object locked by the current directory", even if that object is a file.

BPTR OpenFromLockV34(BPTR lock)
{
    BPTR dir = CurrentDir(lock);       /* yes, a file as current directory */
    BPTR fh  = Open("", MODE_OLDFILE);

    if (fh)
        UnLock(lock);

    CurrentDir(dir);
    return fh;
}

The FileLock structure

Locks are BPTRs to FileLock structures (dos/dosextens.h):

struct FileLock {
    BPTR            fl_Link;    /* file system private: chain on dol_LockList */
    LONG            fl_Key;     /* file system private: object identifier */
    LONG            fl_Access;  /* SHARED_LOCK or EXCLUSIVE_LOCK */
    struct MsgPort *fl_Task;    /* port of the owning file system */
    BPTR            fl_Volume;  /* BPTR to the DLT_VOLUME DosList entry */
};

FileLocks are allocated and freed by the file system, never by dos.library, and may be larger than the public structure.

Field Meaning
fl_Link Only used while the volume is ejected: the file system chains all outstanding locks of the volume through fl_Link, starting at dol_LockList of the volume's DosList. When the volume reappears, possibly in another drive, the file system instance serving that drive picks the locks up and rewrites their fl_Task. This lets a lock taken in DF0: stay valid when the disk is reinserted into DF1:
fl_Key Opaque. The FFS stores the block number of the object's header block; other file systems may store a pointer. Do not interpret it
fl_Access The mode passed to Lock()
fl_Task Port that all requests on this lock go to. While the volume is ejected and unclaimed, it still points to the last file system that owned the lock
fl_Volume BPTR to the volume's DosList entry

Applications should treat fl_Link and fl_Key as private. fl_Task is the reliable way to find the handler behind a lock, for example for direct packet I/O. The migration mechanism covers locks only. File handles are not carried over by the FFS: reading from a file whose disk moved to another drive produces a requester asking for the original drive.

The FileInfoBlock

Examine(), ExamineFH() and ExNext() fill a FileInfoBlock (dos/dos.h):

struct FileInfoBlock {
    LONG   fib_DiskKey;
    LONG   fib_DirEntryType;
    char   fib_FileName[108];
    LONG   fib_Protection;
    LONG   fib_EntryType;
    LONG   fib_Size;
    LONG   fib_NumBlocks;
    struct DateStamp fib_Date;
    char   fib_Comment[80];
    UWORD  fib_OwnerUID;
    UWORD  fib_OwnerGID;
    char   fib_Reserved[32];
};

It must be longword-aligned. Do not declare it as a plain local variable, because compilers do not guarantee 4-byte alignment on the stack. Allocate it with AllocDosObject(DOS_FIB, NULL) (and free it with FreeDosObject(DOS_FIB, fib)), with AllocVec()/AllocMem(), or use the alignment macro of §2.4:

#define D_S(type,name) char a_##name[sizeof(type)+3]; \
                       type *name = (type *)((ULONG)(a_##name+3) & ~3UL)

D_S(struct FileInfoBlock, fib);   /* fib is an aligned pointer */

Field by field:

Field Meaning and caveats
fib_DiskKey File system private. The FFS stores the disk key, the RAM-Handler a pointer, some file systems nothing. Not a unique identifier, and not necessarily equal to fl_Key (for example when the entry is a link)
fib_DirEntryType Object type, see below
fib_FileName NUL-terminated name. FFS limit: 30, 54 or 106 characters depending on the flavour
fib_Protection Protection bits, see below
fib_EntryType Undocumented; may equal fib_DirEntryType. Do not use
fib_Size File size in bytes. Signed, so behaviour above 2 GB is unclear. Undefined for directories, hard links to directories and soft links
fib_NumBlocks Blocks occupied. Unreliable: the old BCPL ROM file system counted extension blocks too, the FFS (since Kickstart 1.3) only estimates from the size plus one header block, and OFS flavours underestimate. Not comparable across file systems, or even across identical files. Leave it alone
fib_Date Last modification (DateStamp). For directories, which operations update it is file-system dependent; the FFS does not update it for metadata changes (comment, protection)
fib_Comment NUL-terminated comment, at most 79 characters. Not all file systems support comments
fib_OwnerUID, fib_OwnerGID Opaque owner and group IDs used by network file systems such as Envoy. The FFS stores them but enforces nothing; AmigaDOS has no concept of users. Introduced in V43
fib_Reserved Do not access. Kickstart 1.3 BCPL commands such as Dir and List allocate a shortened FIB without it, so file systems must never write to it

Handlers fill fib_FileName and fib_Comment as BSTRs; dos.library converts them to C strings before returning.

fib_DirEntryType

Value (dos/dosextens.h) Meaning
ST_ROOT (1) Root directory
ST_USERDIR (2) Directory
ST_SOFTLINK (3) Soft link (target may be a file or a directory)
ST_LINKDIR (4) Hard link to a directory
ST_FILE (−3) File
ST_LINKFILE (−4) Hard link to a file
ST_PIPEFILE (−5) Pipe that supports ExamineFH()
any other > 0 Directory
any other < 0 File

Gotchas:

  • ST_SOFTLINK is positive. A naïve "> 0 means directory" test classifies every soft link as a directory, so test for ST_SOFTLINK first.
  • Lock() resolves links, so Examine() never reports a link type. Only ExNext() and ExAll() can, because they list directory entries without resolving them.
  • Only the FFS and the RAM-Handler give hard links a distinct type. On file systems that implement hard links as duplicate directory entries (all Linux ext variants, for example), a hard link looks like a regular entry. RAM-Handler external links (V47) also look like regular files or directories. Do not depend on being able to recognise links.

Protection bits

Bit (FIBB_) No. Meaning
FIBB_DELETE 1.3+ 0 0 = deletable. If set, DeleteFile() and replacing via MODE_NEWFILE fail with ERROR_DELETE_PROTECTED
FIBB_EXECUTE 1.3+ 1 0 = executable. Only interpreted by the Shell (V34 and up), which refuses to run the file if set; ignored for directories and by Workbench
FIBB_WRITE 1.3+ 2 0 = writable. If set, modifying or overwriting fails with ERROR_WRITE_PROTECTED; deleting is still allowed. Ignored by the V34 and older ROM file system (OFS); meaningless for directories
FIBB_READ 1.3+ 3 0 = readable. If set, opening for reading fails with ERROR_READ_PROTECTED. Ignored by the V34 and older OFS; meaningless for directories
FIBB_ARCHIVE 1.3+ 4 Cleared by the file system on every write, modification or creation; set by backup software
FIBB_PURE 1.3+ 5 1 = reentrant executable that Resident may keep loaded (Shell V34+). Nothing verifies it
FIBB_SCRIPT 1.3+ 6 1 = script; the Shell hands it to an interpreter (Execute, ARexx; V45+ also the interpreter named in the first line)
FIBB_HOLD 3.2+ 7 1 = make resident on first use (requires PURE too). Introduced in V36, dropped in V40 for ROM space, reintroduced in V45

The four low bits are inverted: the protection is active when the bit is 1, but Shell commands and Workbench display the letter when the bit is 0. dos/dos.h also defines group and "other" bits (FIBB_GRP_* bits 8–11, FIBB_OTR_* bits 12–15) for network file systems; AmigaDOS itself assigns them no meaning.

FIBB_ARCHIVE is not recursive: modifying a file clears the bit on its directory, but changes deeper in the tree do not propagate upwards. Backup software must therefore descend into directories whose archive bit is still set.

Whether a link has its own metadata is up to the file system. The ROM file systems behave as follows:

Metadata Hard link (FFS and RAM) Soft link, FFS Soft link, RAM-Handler
Protection bits Kept identical to the target Cannot be set on the link; setting them changes the target Separate bits
Modification date Kept identical to the target Separate date, but setting it updates the target Separate date
Comment Own comment Own comment Own comment

Separate link metadata has limited value, since Lock() always resolves the link and only ExNext()/ExAll() can see the link itself. Objects copied in through a RAM-Handler external link get their own protection bits, because the handler creates a copy on access (see Links).

Examine() and ExamineFH()

BOOL Examine(BPTR lock, struct FileInfoBlock *fib);
BOOL ExamineFH(BPTR fh, struct FileInfoBlock *fib);   /* V36 */

Examine() describes the locked object, never a link (see above). With ZERO it examines the root of the default file system of the process. ExamineFH() does the same for an open file, and also works with handlers that do not support locks. Both return non-zero on success; IoErr() is undefined on success and holds the error code on failure. Because files can change at any time, the result is a snapshot; the autodoc explicitly does not guarantee that fib_Size reflects changes made since the file was opened.

ExamineFH() crashes on a NIL: handle in every version. A NIL: handle has fh_Type == NULL, so check that first:

#include <dos/dos.h>
#include <dos/dosextens.h>
#include <proto/dos.h>

/* Print name and size of an open file; returns FALSE for NIL: or on error. */
BOOL ShowOpenFile(BPTR fh)
{
    struct FileHandle    *h = BADDR(fh);
    struct FileInfoBlock *fib;
    BOOL ok = FALSE;

    if (h == NULL || h->fh_Type == NULL)        /* NIL: has no handler */
        return FALSE;

    if ((fib = AllocDosObject(DOS_FIB, NULL)) != NULL) {
        if ((ok = ExamineFH(fh, fib)) != FALSE) {
            if (fib->fib_DirEntryType < 0)
                Printf("%s: %ld bytes\n", fib->fib_FileName, fib->fib_Size);
            else
                Printf("%s: not a file\n", fib->fib_FileName);
        }
        FreeDosObject(DOS_FIB, fib);
    }
    return ok;
}

Note: the NDK 3.2 autodoc for Examine() states that a local copy of a FileInfoBlock may be made as long as the copy is never passed to ExNext() (or back to the system).

Packets behind the functions

For handler writers and for direct packet I/O, each function maps to one packet (details in DOS Packet Reference):

Function Packet Notes
Lock() 1.3+ ACTION_LOCATE_OBJECT (8) Arg1 lock, Arg2 BSTR, Arg3 mode. An empty name locks the Arg1 object itself
DupLock() 1.3+ ACTION_COPY_DIR (19) Despite the name, copies locks on files too. With Arg1 = ZERO the handler must return a real shared lock on the root, not ZERO
ParentDir() 1.3+ ACTION_PARENT (29) Root or ZERO: Res1 = ZERO, Res2 = 0 (not an error)
DupLockFromFH() 2.0+ ACTION_COPY_DIR_FH (1030) Arg1 = fh_Arg1; fails on exclusively opened files
ParentOfFH() 2.0+ ACTION_PARENT_FH (1031) Arg1 = fh_Arg1; works for exclusive files
CreateDir() 1.3+ ACTION_CREATE_DIR (22) Not recursive
SameLock() 2.0+ ACTION_SAME_LOCK (40) Res1 DOSTRUE = same object; Res1 DOSFALSE with Res2 = 0 = different objects
ChangeMode() 2.0+ ACTION_CHANGE_MODE (1028) Arg2 is the FileHandle BPTR itself for CHANGE_FH
UnLock() 1.3+ ACTION_FREE_LOCK (15) Result ignored by UnLock(); handlers should clear fl_Task first, since dos.library ignores unlock requests for locks with a NULL fl_Task
OpenFromLock() 2.0+ ACTION_FH_FROM_LOCK (1026) Lock absorbed on success
Examine() 1.3+ ACTION_EXAMINE_OBJECT (23) Arg1 ZERO = root of the inserted volume; fib_FileName = volume name
ExamineFH() 2.0+ ACTION_EXAMINE_FH (1034) Arg1 = fh_Arg1

See Also


Sources: Thomas Richter, Amiga ROM Kernel Reference Manual: AmigaDOS (2024, v1.27), §2.4 (p. 7), ch. 6 (pp. 57–67), §7.1–7.1.2 (pp. 69–73), §7.2.4–7.2.5 (pp. 80–81), §14.2 (pp. 265–269), §14.3.1 and §14.3.5 (pp. 270–271, 275); NDK 3.2 R4 dos/dos.h, dos/dosextens.h, dos.doc (ChangeMode, SameLock, SameDevice, Examine, ExamineFH, OpenFromLock) Updated: 2026-11-13