Skip to content
NorscodeNorscode

Reading and writing files

ExampleBy the Norscode project

File handling with capabilities — and why your program cannot read what you have not given it.

File operations require capabilities. That is not in the way — it is the point.

bruk std.fil som fil

funksjon start() -> heltall {
    fil.skriv("notat.txt", "Første linje\
Andre linje\
")
    la innhald = fil.les("notat.txt")
    la linjer = builtin.split(innhald, "\
")
    skriv("Antall linjer: " + tekst(lengde(linjer) - 1))
    returner 0
}

What happens here

The write function creates the file if it does not exist, and overwrites if it does. The read function gives you the whole content as one text.

Notice that one is subtracted in the count. The file ends with a newline, so the split gives an empty element at the end. It is a classic off-by-one error that happily passes a test with two lines and breaks on you with a thousand. When you split text on newlines, always ask yourself whether the last element is real or just the tail after the last break.

Run with restricted access

NORSCODE_VM_CAPABILITIES="disk.read,disk.write" \\
NORSCODE_VM_DISK_ROOT="$PWD" \\
  nc run filer.no

Try to break out

Change the path to something outside NORSCODE_VM_DISK_ROOT — for example the system's password file — and run again. The program stops with manglar capability.

It is worth dwelling on why. There is no API for requesting more access while the program runs. The boundary is set by whoever starts the program, not by the program itself. A library you have not read thus cannot reach further than you have let it — not because it behaves nicely, but because the path does not exist.

Compare this with how it usually is: a program inherits the rights of the user who started it. A dependency five layers down the tree can read your key files and send them out onto the network, and you do not discover it without scrutinizing every line.

In practice

Set NORSCODE_VM_DISK_ROOT as narrowly as the job allows. If the service only needs to read configuration from one directory, give it only that directory — and only disk.read, not disk.write. It costs you one line in the startup command, and gives you a boundary that holds even if something further down the dependency tree should want otherwise.

Related

Back to the overview