Landin library reference discuss source

platform/linux/vdso

Target scope is enforced by the module assertion below.

The vDSO, the small shared object Linux maps into every process, and the functions found in it, raw.

A submodule of platform/linux, so a program that never reads a clock this way does not carry it; it declares nothing on Darwin and FreeBSD (D346). The vDSO the kernel maps into every process is found through the auxiliary vector's AT_SYSINFO_EHDR: vdso.kernel_image is its bytes as far as its first load segment reaches, the image the kernel mapped, vdso.find finds a global or weak function by name and version in any image of a 64-bit little-endian ELF object, through its program headers, dynamic table, DT_HASH or DT_GNU_HASH, symbols and version definitions, and vdso.symbol gives the function's address. vdso.clock_symbol and vdso.clock_version name its clock_gettime, __vdso_clock_gettime of LINUX_2.6 on x86-64, __kernel_clock_gettime of LINUX_2.6.39 on arm64 and __vdso_clock_gettime of LINUX_4.15 on RV64, and vdso.clock_gettime is its C type. Every offset is held to lie within the image before a byte is read, so a malformed, truncated or foreign image finds nothing and never traps; runtime/linux-vdso damages a copy of the real one byte by byte to show it.

compiler.assert(compiler.os == linux or compiler.os == darwin
                or compiler.os == freebsd)

Items

Executable example

This complete program is maintained in the repository runtime tests. View source.

--  The vDSO the auxiliary vector names: platform/linux finds its
--  `clock_gettime` by name and version, which reads the same clocks the
--  system call does, and a copy of the image, damaged one byte at a time
--  or cut short anywhere, finds nothing or something inside it and never
--  traps, so a malformed vDSO falls back to the system call.  Without its
--  version tables the name alone decides, and without DT_HASH the GNU
--  hash table counts the symbols.  Each check that fails returns its own
--  code.
import platform/linux
import platform/linux/vdso

--  Room for a copy of the image: the kernel's is two pages or so.
mut copy: [65536]u8

--  The image's length in the copy.
mut copied: usize

--  The clock `clock` read through the system call, in nanoseconds.
by_system_call: (clock: i32) -> (at: i64) =
    mut record: linux.timespec = zeroed
    at = -1
    return when linux.clock_gettime(clock, addr record) <> 0
    at = record.seconds * 1000000000 + record.nanoseconds
end by_system_call

--  The clock `clock` read through the vDSO function at ADDRESS.
by_vdso: (address: usize, clock: i32) -> (at: i64) =
    function: vdso.clock_gettime = vdso.clock_gettime(address)
    mut record: linux.timespec = zeroed
    at = -1
    return when function(clock, addr record) <> 0
    at = record.seconds * 1000000000 + record.nanoseconds
end by_vdso

--  Readings through the vDSO fall between system calls made before and
--  after them, on the monotonic clock, and the realtime clocks agree
--  within a second.
agrees: (address: usize) -> (yes: bool) =
    yes = false
    for round in usize(0)..<100 do
        _ = round
        before: i64 = by_system_call(linux.clock_monotonic)
        between: i64 = by_vdso(address, linux.clock_monotonic)
        after: i64 = by_system_call(linux.clock_monotonic)
        return when before < 0 or between < before or after < between
    end for
    wall: i64 = by_vdso(address, linux.clock_realtime)
    kernel: i64 = by_system_call(linux.clock_realtime)
    return when wall < 0 or kernel < wall or kernel - wall > 1000000000
    yes = true
end agrees

--  Whether a lookup in the first LENGTH bytes of the copy returns without
--  trapping and, when it finds the clock, finds it inside them.
holds: (length: usize) -> (yes: bool) =
    offset: usize = found_at(copy[0..<length], vdso.clock_symbol,
                             vdso.clock_version)
    yes = offset == no_offset or offset < length
end holds

--  Every byte of the copy set to zero, to every bit, and to itself with
--  its top bit flipped, one at a time and put back after.
damaged: () -> (yes: bool) =
    yes = false
    for at in usize(0)..<copied do
        kept: u8 = copy[at]
        copy[at] = 0
        return when not holds(copied)
        copy[at] = 255
        return when not holds(copied)
        copy[at] = kept ^ 128
        return when not holds(copied)
        copy[at] = kept
    end for
    yes = true
end damaged

--  The copy cut short at every length.
truncated: () -> (yes: bool) =
    yes = false
    for length in usize(0)..<copied do
        return when not holds(length)
    end for
    yes = true
end truncated

--  The little-endian u64 at AT in the copy, and writing one.
read_double: (at: usize) -> (value: u64) =
    value = 0
    for index in usize(0)..<8 do
        value = value | (u64(copy[at + index]) << u64(8 * index))
    end for
end read_double

write_double: (at: usize, value: u64) -> none =
    for index in usize(0)..<8 do
        copy[at + index] = u8((value >> u64(8 * index)) & 255)
    end for
end write_double

--  Where in the copy the dynamic entry tagged TAG lies, or zero.
entry_of: (tag: u64) -> (at: usize) =
    at = 0
    headers: usize = usize(read_double(32))
    count: usize = usize(copy[56]) | (usize(copy[57]) << 8)
    for index in usize(0)..<count do
        header: usize = headers + index * 56
        if copy[header] == 2 and copy[header + 1] == 0 then
            dynamic: usize = usize(read_double(header + 8))
            mut entry: usize = dynamic
            loop do
                found: u64 = read_double(entry)
                return when found == 0
                if found == tag then
                    at = entry
                    return
                end if
                entry += 16
            end loop
        end if
    end for
end entry_of

--  A tag no dynamic loader reads, which hides the entry it replaces.
hidden: u64 = 0x6fffff00

--  Where in BYTES the function NAME of VERSION is, or no_offset.
no_offset: usize = 0 -% 1

found_at: (bytes: []u8, name: []u8, version: []u8) -> (offset: usize) =
    (found, offset: at) := vdso.find(bytes, name, version)
    offset = if found then at else no_offset end if
end found_at

--  Where the auxiliary vector says the vDSO is.
vdso_base: () -> (base: usize) =
    (found: _, value) := linux.auxiliary(vdso.at_sysinfo_ehdr)
    base = usize(value)
end vdso_base

--  The vDSO's function NAME of VERSION's address, or zero.
address_of: (name: []u8, version: []u8) -> (address: usize) =
    (found, address: at) := vdso.symbol(name, version)
    address = if found then at else 0 end if
end address_of

public main: () -> (code: i32) =
    (found: present, image) := vdso.kernel_image()
    code = 1
    return when not present or lenof image < 64 or lenof image > 65536
    code = 2
    name: []u8 = vdso.clock_symbol
    version: []u8 = vdso.clock_version
    offset: usize = found_at(image, name, version)
    return when offset >= lenof image
    address: usize = address_of(name, version)
    return when address <> vdso_base() + offset
    code = 3
    return when not agrees(address)
    code = 4
    return when found_at(image, name, "LINUX_0.1") <> no_offset
      or found_at(image, "__vdso_no_such_function", version) <> no_offset
    code = 5
    copied = lenof image
    for at in usize(0)..<copied do
        copy[at] = image[at]
    end for
    return when not damaged()
    code = 6
    return when not truncated()
    code = 7
    --  Without DT_HASH the GNU hash table counts the symbols, and the same
    --  function is found where it was.
    hash: usize = entry_of(4)
    if hash <> 0 and entry_of(0x6ffffef5) <> 0 then
        write_double(hash, hidden)
        return when found_at(copy[0..<copied], name, version) <> offset
        write_double(hash, 4)
    end if
    code = 8
    --  Without version definitions, the name alone decides.
    definitions: usize = entry_of(0x6ffffffc)
    return when definitions == 0
    write_double(definitions, hidden)
    return when found_at(copy[0..<copied], name, "LINUX_0.1") <> offset
    code = 42
end main

clock_gettime type

When compiler.os == linux

public clock_gettime: type = extern(c) (clock: i32, into: ptr mut linux.timespec) -> (result: i32)

platform/linux/vdso/vdso.ldn:41

The vDSO's clock_gettime, a C function: it fills into with the clock clock's time and returns zero, or minus the errno, making the system call itself for a clock it cannot read.

at_sysinfo_ehdr value

When compiler.os == linux

public at_sysinfo_ehdr: u64 = 33

platform/linux/vdso/vdso.ldn:19

The auxiliary-vector tag of the vDSO's address, AT_SYSINFO_EHDR.

clock_symbol value

When compiler.os == linux; compiler.arch == x86_64

public clock_symbol: []u8 = "__vdso_clock_gettime"

platform/linux/vdso/vdso.ldn:23

The name the vDSO gives clock_gettime on this architecture.

When compiler.os == linux; not (compiler.arch == x86_64) and (compiler.arch == arm64)

public clock_symbol: []u8 = "__kernel_clock_gettime"

platform/linux/vdso/vdso.ldn:28

The name the vDSO gives clock_gettime on this architecture.

When compiler.os == linux; not (compiler.arch == x86_64 or compiler.arch == arm64)

public clock_symbol: []u8 = "__vdso_clock_gettime"

platform/linux/vdso/vdso.ldn:33

The name the vDSO gives clock_gettime on this architecture.

clock_version value

When compiler.os == linux; compiler.arch == x86_64

public clock_version: []u8 = "LINUX_2.6"

platform/linux/vdso/vdso.ldn:25

The symbol version it has: x86-64's is LINUX_2.6.

When compiler.os == linux; not (compiler.arch == x86_64) and (compiler.arch == arm64)

public clock_version: []u8 = "LINUX_2.6.39"

platform/linux/vdso/vdso.ldn:30

The symbol version it has: arm64's is LINUX_2.6.39.

When compiler.os == linux; not (compiler.arch == x86_64 or compiler.arch == arm64)

public clock_version: []u8 = "LINUX_4.15"

platform/linux/vdso/vdso.ldn:35

The symbol version it has: RV64's is LINUX_4.15.

largest value

When compiler.os == linux

public largest: usize = 1048576

platform/linux/vdso/vdso.ldn:45

The most of an image symbol believes: the kernel's vDSO is a few pages, and a load segment claiming more is not one.

find function

When compiler.os == linux

public link(symbol: "_landin_linux_vdso_find") find:
    (image: []u8, name: []u8, version: []u8)
    -> (found: bool, offset: usize)

platform/linux/vdso/vdso.ldn:229

Find the function name of version version in image, the bytes of a shared object as it is mapped: whether it is there, and its offset from the image's start. A global or weak function defined in the image counts; where the image has no version tables, the name alone decides, as a dynamic loader decides it. Every offset is held to lie within the image before it is read, so an image that is not a 64-bit little-endian ELF object, is truncated or contradicts itself finds nothing and never traps.

kernel_image function

When compiler.os == linux

public link(symbol: "_landin_linux_vdso_image") kernel_image:
    () -> (found: bool, image: []u8)

platform/linux/vdso/vdso.ldn:317

The vDSO as the kernel mapped it: whether the auxiliary vector names one, and its bytes, as far as its first load segment reaches and no further than largest. There is none when auxiliary found no auxiliary vector, as when a C main built the tables it passed the argument initializer itself, or when the image's first page does not describe a load segment.

symbol function

When compiler.os == linux

public link(symbol: "_landin_linux_vdso_symbol") symbol:
    (name: []u8, version: []u8) -> (found: bool, address: usize)

platform/linux/vdso/vdso.ldn:337

The address of the vDSO's function name of version version, found by find in kernel_image: whether it is there, and where.