FreeDOS INT 21h/AH=60h TrueName: The Path-Query Contract
Use FreeDOS TrueName safely: verify its registers and failure path, understand wildcard and redirector limits, and avoid treating paths as file identity.
INT 21h/AH=60h is the classic DOS TrueName service: it accepts a path string and returns a path result whose form depends on the local resolver or active redirector/provider. Local paths may be normalized, but callers must not assume every result passes through the same normalization. It is useful for diagnostics and for understanding how the active DOS implementation interprets a name. It is not an open call, a file handle, or proof that a later operation will reach the same object.
This guide describes the FreeDOS kernel source pinned to commit d6791add2043c9d7b584d840a8ffaf8829fd2bdc, not every DOS-compatible kernel or every released FreeDOS build. The pinned intfns.txt inventory identifies itself as FreeDOS build 2036 (May 2006), and labels function 60h undocumented while noting it as supported since DOS 3.00. That historical inventory is a compatibility note, not a portable specification for all the details below.
The classic call is not the later long-name service
For the classic call, the pinned FreeDOS dispatcher passes DS:SI as the source string and ES:DI as the destination to DosTruename. It expects NUL-terminated path text. The caller supplies no destination-capacity register, so the program must own a writable buffer large enough for the target kernel’s result. In this FreeDOS revision, the local path builder uses SFTMAX (128 bytes), but that constant is not a safe 128-character output guarantee: its append macro permits a character at offset 127, then the normalizer writes the NUL terminator separately at offset 128 without another bound check. The private path buffer is also 128 bytes, so a path that produces exactly 128 non-NUL characters crosses the internal buffer boundary. This is a source-level boundary defect, not a DOS-wide limit or a caller-buffer sizing recommendation; a larger caller buffer cannot repair the kernel-side write.
Do not conflate AH=60h with the later AX=7160h LFN family. In this pinned source, the AH=71h path exists under the WITHFAT32 compile-time guard and its LFN cases under WITHLFNAPI; AL=60h is labeled as Win95 long-filename canonicalization. The documented CL cases in this revision do not call a handler and fall through to the unsupported path. That is a limitation of this source revision and build configuration, not proof that no separately installed LFN provider can intercept or supply the service. Code that needs long-name behavior must follow the active provider’s contract rather than assuming classic TrueName preserves or returns long names.
The pinned INT 21h handler clears carry for a defined set of AH ranges, one of which includes 5Eh through 60h. For AH=60h it calls DosTruename, places its return value in AX, and uses the common result check. A negative internal error is converted to a positive AX error value and carry is set; a nonnegative return leaves carry clear. On success, AX is the helper’s return value, not a universal zero-only status. In particular, network and device paths have implementation-specific return flags, so branch on carry for success or failure rather than treating every nonzero AX as an error.
DosTruename builds the result in a kernel-owned buffer and copies it to the caller’s ES:DI buffer only when the helper returns success. Therefore, with this pinned implementation, a failed call leaves the caller’s destination unchanged. That is a useful property for writing a sentinel-based test, but do not assume another DOS clone has the same failure-buffer behavior without checking it.
What the local FreeDOS resolver normalizes
For an ordinary local path on a valid drive, an input with no drive prefix starts from the default drive. With an explicit drive prefix, the local resolver selects that drive’s Current Directory Structure entry. A drive-relative path such as C:REPORT.TXT therefore uses C’s remembered directory; C:\REPORT.TXT is rooted at C. This matches DOS’s per-drive current-directory model, but callers should avoid depending on mutable directory state when a fixed root is required.
The local mapper makes a fully qualified result and handles . and .. components. It consumes repeated slash separators, emits DOS backslashes, and rejects traversal above the drive root. Its path parser follows an 8.3-shaped representation. In that local path, * is filled into ? placeholders and wildcard state is accepted only in the terminal component; a wildcard in an earlier directory component makes a later component invalid. This produces a path pattern, not a list of matching files. Use FindFirst/FindNext or the provider’s documented search API when enumeration is the goal.
The implementation uppercases the user-provided portion as it builds this local 8.3 path. Do not read that as a rule that every returned character must be uppercase: the stored current-directory base is handled separately, and remote providers may return their own spelling. Nor is this a general Unicode or long-filename canonicalizer.
Network and UNC behavior is a boundary, not a detail
For a mapped network drive, the FreeDOS helper first offers the request to the redirector’s INT 2Fh/11h/23h Qualify Remote Filename interface. If the redirector supplies a result, the pinned branch marks it as a network result. The source comment intends to avoid this for device results such as Z:/NUL, but this revision tests the IS_DEVICE bit before it classifies the original drive result as a device: the initial value is a drive index (Z is 25, or 0x19), while IS_DEVICE is 0x20. For ordinary drive indices that check is false, so do not rely on the commented exception as implemented behavior. This is a source/comment discrepancy in the pinned revision, not a universal DOS rule. The helper otherwise falls back to its local mapper when the redirector does not supply a result. Source comments warn that older redirectors may lack this multiplex interface, even when they wrap DOS file calls; behavior depends on the installed redirector and cannot be inferred from the drive letter alone.
The QRemote debug check is not a capacity guard: it is compiled only with DEBUG_TRUENAME and checks the resulting string after the redirector has already written into the fixed internal path buffer. Treat it as a diagnostic assertion, not protection from an overlong redirector result.
Direct UNC input beginning with two backslashes is a separate path in this pinned implementation: if the NUL-terminated input fits the 128-byte internal buffer, it is copied unchanged and returned as a network path without running the local component normalizer. The bounded append rejects an overlong UNC input with a path error rather than reaching the local normalizer’s separate unchecked terminator write. Do not assume \\server\share\..\file will be collapsed like a local path merely because the API name says TrueName. Test UNC and mapped-drive behavior with the redirector and FreeDOS build actually deployed.
Use the result as a query, not an authorization check
The AH=60h call uses the wildcard-allowing mode in DosTruename. In the local resolver, dir_exists() is guarded by the separate CDS_MODE_CHECK_DEV_PATH mode and applies only to device paths; AH=60h does not use that mode. Successful local string normalization therefore does not establish that the requested final file exists. A network redirector may perform its own qualification, but that is provider-specific. Neither result is a durable identity: media, mappings, directory entries, or remote state can change before the next operation.
If an application needs to read or update a file, use the appropriate open operation and validate the returned handle and status. Do not compare a TrueName string and then assume a later open is still bound to that name. Do not use it as a security containment primitive, as proof that two spellings refer to one stable object, or as a substitute for handling open errors.
This MASM/TASM-style .COM sketch shows only the register setup and carry-based branch for a short, controlled local path. It is not assembled here. DS and ES point to the same segment so both buffers are addressable; the test buffer is initialized to a sentinel, but AH=60h supplies no destination-capacity argument and the kernel has the source-level boundary defect described above. Do not treat this as a production buffer-size recipe or send boundary-length input to an affected build.
org 100h
start:
mov ax, cs
mov ds, ax
mov es, ax
cld
mov di, offset output_path
mov cx, 512
mov al, 0A5h
rep stosb
mov si, offset input_path
mov di, offset output_path
mov ax, 6000h
int 21h
jc truename_failed
; output_path now contains this implementation's path result.
; Do not treat it as proof that a later open will reach the same file.
mov ax, 4C00h
int 21h
truename_failed:
; Preserve the error before replacing AX with the process exit code.
mov [failure_ax], ax
mov ax, 4C01h
int 21h
input_path db 'C:\DATA\*.TXT', 0
failure_ax dw 0
output_path db 512 dup (?)
Before relying on the call, test a disposable FreeDOS image with a known C: current directory and compare C:NAME, C:\NAME, a slash-separated path, ., .., a terminal wildcard, and a wildcard in a parent component. Initialize the output buffer with a visible sentinel and confirm it remains unchanged after a known failing input on the exact kernel build. Then repeat with a mapped network drive and any direct UNC names the application accepts. Record kernel revision, redirector, drive mappings, input bytes, carry, AX, and output string. Those observations define the environment’s actual compatibility contract.
The practical rule is simple: classic TrueName can explain how one DOS implementation resolved a string at one moment. Use it for controlled diagnostics, keep local and redirector cases separate, and let the actual open operation decide whether a file can be used.
Related:
- DOS Path Resolution: Current Drives, Per-Drive Directories, and Relative Names
- DOS INT 21h FindFirst and FindNext: The DTA Is Search State
Sources:
- FreeDOS kernel source at pinned revision: INT 21h carry macro, selected AH ranges, and carry initialization
- FreeDOS kernel source at pinned revision: AH=60h dispatch and branch to shared result check
- FreeDOS kernel source at pinned revision: separate LFN selector and unsupported fall-through
- FreeDOS kernel source at pinned revision: common negative-return, AX, and carry handling
- FreeDOS kernel source at pinned revision: caller-buffer copy only after success
- FreeDOS kernel source at pinned revision: MUX redirector design comment
- FreeDOS kernel source at pinned revision: drive-prefix and default-drive selection
- FreeDOS kernel source at pinned revision: UNC path handling
- FreeDOS kernel source at pinned revision: QRemote request, result classification, and post-write debug check
- FreeDOS kernel source at pinned revision: device-path test and older-redirector caveat
- FreeDOS kernel source at pinned revision: local current-directory and path-prefix setup
- FreeDOS kernel source at pinned revision: local absolute and drive-relative path setup
- FreeDOS kernel source at pinned revision: local component normalization and wildcard rules
- FreeDOS kernel source at pinned revision: local path buffer bound
- FreeDOS kernel source at pinned revision: local path append bound
- FreeDOS kernel source at pinned revision: separate unchecked NUL terminator write
- FreeDOS kernel source at pinned revision: 128-byte private path buffers
- FreeDOS kernel source at pinned revision: device/network flag definitions
- FreeDOS kernel INT 21h function inventory: AH=60h support note