Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I ensure that a function takes a certain amount of time in Go?

Tags:

python

time

go

sqrl

I am implementing EnScrypt for an SQRL client in Go. The function needs to run until it has used a minimum amount of CPU time. My Python code looks like this:

def enscrypt_time(salt, password, seconds, n=9, r=256):
    N = 1 << n
    start = time.process_time()
    end = start + seconds
    data = acc = scrypt.hash(password, salt, N, r, 1, 32)
    i = 1
    while time.process_time() < end:
        data = scrypt.hash(password, data, N, r, 1, 32)
        acc = xor_bytes(acc, data)
        i += 1
    return i, time.process_time() - start, acc

Converting this to Go is pretty simple except for the process_time function. I can't use time.Time / Timer because those measure wall-clock time (which is affected by everything else that may be running on the system). I need the CPU time actually used, ideally by the function, or at least by the thread or process it is running in.

What is the Go equivalent of process_time?

https://docs.python.org/3/library/time.html#time.process_time

like image 308
Terrel Shumway Avatar asked Oct 18 '22 00:10

Terrel Shumway


1 Answers

You may use runtime.LockOSThread() to wire the calling goroutine to its current OS thread. This will ensure that no other goroutines will be scheduled to this thread, so your goroutine will run and not get interrupted or put on hold. No other goroutines will interfere when thread is locked.

After this, you just need a loop until the given seconds have passed. You must call runtime.UnlockOSThread() to "release" the thread and make it available for other goroutines for execution, best done as a defer statement.

See this example:

func runUntil(end time.Time) {
    runtime.LockOSThread()
    defer runtime.UnlockOSThread()
    for time.Now().Before(end) {
    }
}

To make it wait for 2 seconds, it could look like this:

start := time.Now()
end := start.Add(time.Second * 2)
runUntil(end)

fmt.Println("Verify:", time.Now().Sub(start))

This prints for example:

Verify: 2.0004556s

Of course you can specify less than a second too, e.g. to wait for 100 ms:

start := time.Now()
runUntil(start.Add(time.Millisecond * 100))
fmt.Println("Verify:", time.Now().Sub(start))

Output:

Verify: 100.1278ms

You may use a different version of this function if that suits you better, one that takes the amount of time to "wait" as a value of time.Duration:

func wait(d time.Duration) {
    runtime.LockOSThread()
    defer runtime.UnlockOSThread()

    for end := time.Now().Add(d); time.Now().Before(end); {
    }
}

Using this:

start = time.Now()
wait(time.Millisecond * 200)
fmt.Println("Verify:", time.Now().Sub(start))

Output:

Verify: 200.1546ms

Note: Note that the loops in the above functions will use CPU relentlessly as there is no sleep or blocking IO in them, they will just query the current system time and compare it to the deadline.

What if the attacker increases system load by multiple concurrent attempts?

The Go runtime limits the system threads that can simultaneously execute goroutines. This is controlled by runtime.GOMAXPROCS(), so this is already a limitation. It defaults to the number of available CPU cores, and you can change it anytime. This also poses a bottleneck though, as by using runtime.LockOSThread(), if the number of locked threads equals to GOMAXPROCS at any given time, that would block execution of other goroutines until a thread is unlocked.

See related questions:

Number of threads used by Go runtime

Why does it not create many threads when many goroutines are blocked in writing file in golang?

like image 57
icza Avatar answered Oct 31 '22 15:10

icza