Each directory entry has an attribute byte. INT 21h AH=43h with AL = 0 returns it in CX; with AL = 1 it writes CX into it. The file is found by name, like 41h and 56h, and does not need to be open.

BitValueMeaning
001hread-only: DOS refuses to write, truncate or delete the file (05h)
102hhidden: DIR does not list it, find first skips it unless asked
204hsystem: like hidden, for DOS's own files
308hvolume label: the entry is the disk's name, not a file
410hdirectory
520harchive: DOS sets it whenever the file is written; backup programs clear it

Set can change bits 0, 1, 2 and 5. The directory and volume-label bits say what the entry is, and trying to set them is error 05h. Set replaces the whole byte, so to change one bit, get the attributes first, change the bit with OR or AND, and set the result.

prints two lines: "new file: 20" and "after setting read-only: 21"
MOV AH, 3Ch ; DOS: create TEST.TXT
XOR CX, CX ; CX = 0: normal attributes
MOV DX, OFFSET FNAME
INT 21h
CALL CHECK ; stop if CF = 1
MOV BX, AX ; BX = the handle
MOV AH, 3Eh ; close it
INT 21h
MOV AX, 4300h ; DOS: get the attributes of TEST.TXT
MOV DX, OFFSET FNAME
INT 21h
CALL CHECK
MOV DX, OFFSET M1
MOV AH, 09h ; DOS: print the string at DS:DX
INT 21h
MOV AL, CL ; CX = the attributes
CALL PHEXB
MOV DX, OFFSET CRLF
MOV AH, 09h ; DOS: print the string at DS:DX
INT 21h
MOV AX, 4301h ; set: read-only + archive
MOV CX, 21h
MOV DX, OFFSET FNAME
INT 21h
CALL CHECK
MOV AX, 4300h ; get them again
MOV DX, OFFSET FNAME
INT 21h
CALL CHECK
MOV DX, OFFSET M2
MOV AH, 09h ; DOS: print the string at DS:DX
INT 21h
MOV AL, CL
CALL PHEXB
MOV DX, OFFSET CRLF
MOV AH, 09h ; DOS: print the string at DS:DX
INT 21h
MOV AX, 4301h ; back to 20h, or the delete fails with 05h
MOV CX, 20h
MOV DX, OFFSET FNAME
INT 21h
CALL CHECK
MOV AH, 41h ; delete it
MOV DX, OFFSET FNAME
INT 21h
FNAME DB "TEST.TXT", 0
M1 DB "new file: $"
M2 DB "after setting read-only: $"
CRLF DB 13, 10, "$"
CHECK PROC NEAR ; CF = 1: print the error code in AL and exit
JNC CHECK1
PUSH AX
MOV DX, OFFSET FAILMSG
MOV AH, 09h
INT 21h
POP AX
CALL PHEXB
MOV AX, 4C01h ; exit with code 1
INT 21h
CHECK1:
RET
CHECK ENDP
FAILMSG DB "failed: error $"
PHEXB PROC NEAR ; print AL as two hex digits
PUSH CX
PUSH DX
MOV DH, AL ; keep the byte
MOV CL, 4
SHR AL, CL ; the high digit first
MOV CX, 2
PHEXB1:
AND AL, 0Fh
ADD AL, "0"
CMP AL, "9"
JBE PHEXB2
ADD AL, 7 ; 10..15 -> "A".."F"
PHEXB2:
MOV DL, AL
MOV AH, 02h ; DOS: print DL
INT 21h
MOV AL, DH ; then the low digit
LOOP PHEXB1
POP DX
POP CX
RET
PHEXB ENDP

A new file comes with the archive bit set: it has changed since the last backup, which was never. Get works on directories too, returning 10h plus any other bits, which makes it a quick test whether a name is a directory.

On a host folder DOSBox-X maps read-only onto the host file's write permission and, on hosts other than Windows, keeps hidden and system in a small file of its own. How strictly read-only is enforced then depends on the host: a DOSBox-X running with administrator rights may still be able to write the file.