|
|
Log in / Subscribe / Register

Leading items

Welcome to the LWN.net Weekly Edition for July 22, 2021

This edition contains the following feature content:

This week's edition also includes these inner pages:

  • Brief items: Brief news items from throughout the community.
  • Announcements: Newsletters, conferences, security updates, patches, and more.

Please enjoy this week's edition, and, as always, thank you for supporting LWN.net.

Comments (none posted)

The Sequoia seq_file vulnerability

By Jake Edge
July 21, 2021

A local root hole in the Linux kernel, called Sequoia, was disclosed by Qualys on July 20. A full system compromise is possible until the kernel is patched (or mitigations that may not be fully effective are applied). At its core, the vulnerability relies on a path through the kernel where 64-bit size_t values are "converted" to signed integers, which effectively results in an overflow. The flaw was reported to Red Hat on June 9, along with a local systemd denial-of-service vulnerability, leading to a kernel crash, found at the same time. Systems with untrusted local users need updates for both problems applied as soon as they are available—out of an abundance of caution, other systems likely should be updated as well.

Down in the guts of the kernel's seq_file interface, which is used for handling virtual files in /proc and the like, buffers are needed to store each line of the file's "contents". To start, a page of memory is allocated for the buffer, but if that is not sufficient, a new buffer that is twice the size of the old one is allocated. This is all done using a size_t, which is an unsigned 64-bit quantity (on x86_64) that is large enough to hold the results, so "the system would run out of memory long before this multiplication overflows".

But that value (m->size in the advisory) is passed to other functions that expect a signed 32-bit integer. In particular, the exploit uses the output of /proc/self/mountinfo to get to a place in the kernel where that is the case. The attacker can create a directory hierarchy with a path length larger than 1GB (roughly one-million nested directories), bind-mount it into a user namespace, and then delete the directory. In the namespace, the attacker opens and reads mountinfo, which causes the string "//deleted" to be written outside of the seq_file buffer. The seq_file interface creates a buffer that is 2GB in size, but down in the code that writes the string, that value gets interpreted as -2GB, which is used to calculate where to do the write. That is far outside the buffer, of course, and so it overwrites memory at a known offset elsewhere in the vmalloc() region.

As might be guessed, writing a ten-byte fixed string outside of the buffer is only the start of a complicated, but easily replicated, series of gyrations leading to a root shell. The Qualys advisory goes into great detail about the journey, which involves several different kernel features that have come about over the last decade or so: user namespaces, BPF, and user-space page fault handling (or FUSE, which is much older). But in the end, it is the mishandling of the integer buffer size that gets the foot in the door; the fix is something of a band-aid to simply reject seq_buf allocations that get "too large".

Qualys said that it has exploit code that can gain root on "default installations of Ubuntu 20.04, Ubuntu 20.10, Ubuntu 21.04, Debian 11, and Fedora 34 Workstation", furthermore "other Linux distributions are certainly vulnerable, and probably exploitable". One of the suggested mitigations is turning off the ability for unprivileged users to create user namespaces (via /proc/sys/kernel/unprivileged_userns_clone), because that will stop their ability to mount the deep directory hierarchy that results in the 1GB+ path name. But even without user namespaces, there may be an alternative:

However, the attacker may mount a long directory via FUSE instead; we have not fully explored this possibility, because we accidentally stumbled upon CVE-2021-33910 in systemd: if an attacker FUSE-mounts a long directory (longer than 8MB), then systemd exhausts its stack, crashes, and therefore crashes the entire operating system (a kernel panic).

The systemd bug, which Qualys also describes well, stems from switching from strdup() to strdupa() in the mount-path handling code. That switched from heap-based allocations to stack-based ones, which provides the opportunity to exhaust the 8MB (by default) stack. So an overlong path that is found when systemd is parsing /proc/self/mountinfo will result in a segmentation fault; Linux is decidedly unhappy about running without an init process, so it panics as well.

A user can trigger the crash by mounting a FUSE filesystem, creating a directory path longer than 8MB, moving the FUSE filesystem to that directory, and causing systemd to parse the mountinfo file again. The fix in this case is to switch back to using strdup() as it was before an April 2015 commit.

The systemd crash was found while developing Sequoia exploit, which uses BPF code to find and then overwrite the modprobe_path variable in the kernel. That path is used to run an executable as root (normally /sbin/modprobe) when a kernel module gets loaded. Pointing that at a different path, where an attacker-controlled executable lives, gives root privileges.

One would expect the BPF verifier to thwart any attempts to execute a harmful program, since that is the primary mechanism to restrict users from loading unsafe BPF programs. But that is where user-space page-fault handling comes into play: the userfaultfd() system call allows the exploit to effectively pause the kernel after the BPF verifier has been run. Then the overwrite of "//deleted" can be arranged to land at the "right" place in the BPF code, which is loaded into the vmalloc() region. userfaultfd() allows the exploit to change the BPF code after it has been verified but before it gets JIT-compiled, thus evading the verifier.

The out-of-bounds write of "//deleted" is turned into an information disclosure that allows the exploit code to have limited control over what gets overwritten. After that, the techniques from a 2020 BPF vulnerability found by Manfred Paul were used to "transform this limited out-of-bounds write into an arbitrary read and write of kernel memory" The six-step "Exploitation overview" section of the advisory gives a nice overview of the exploit, while the following section gives all the gory details. It makes for interesting reading for those who are curious about kernel exploits and the intricate steps that are needed to make them work.

The reports of both flaws were in the hands of Red Hat for nearly a month until they were reported to the closed mailing lists for kernel and distribution security reports on July 6. Two weeks after that, the coordinated release of the advisories was made and distribution updates started rolling out. It is not at all clear why there was a month-long delay, since neither of the fixes seems particularly challenging. Maybe that time was spent looking for other similar problems in the kernel and systemd.

Clearly the integer conversion at the heart of the exploit needed fixing; one wonders how many other size_t-to-int problems of that sort still linger in the kernel. One might also wonder what kind of a can of worms was opened when userfaultfd() was added to the kernel; it provides a way for user space to semi-arbitrarily pause the kernel in spots of its choosing. That may come in handy again for kernel exploits down the road.

Meanwhile, systemd has hopefully scrutinized any other stack-based allocations it is doing, especially for user-controllable values like paths. While exhausting the stack is often not a security problem for user-space programs (except, of course, for exploits like Stack Clash), systemd sits in a sensitive place in many Linux systems. Any user-controlled means to bring it down is a clear route to a denial of service on the system.

Integer-conversion problems of the sort we see here are something that would likely be impossible in some other languages (e.g. Rust). Stack (or memory) exhaustion, on the other hand, is not really something that can be handled at the language level; hitting a resource limit must be handled somehow and crashing a user-space program is often the least harmful thing the operating system can do. But at least we now have N-2 bugs in our systems; unfortunately, the unknown N is likely distressingly large.

Comments (30 posted)

GitHub is my copilot

By Jonathan Corbet
July 15, 2021
Your editor has worked in the computing field for rather longer than he cares to admit; for all of that time it has been said that a day will come when all that tedious programming work will no longer be necessary. Instead, we'll just say what we want and the computer will figure it out. Arguably, the announcement of GitHub Copilot takes us another step in that direction. On the way, though, it raises some interesting questions about copyright and free-software licensing.

Copilot is a machine-learning system that generates code. Given the beginning of a function or data-structure definition, it attempts to fill in the rest; it can also work from a comment describing the desired functionality. If one believes the testimonials on the Copilot site, it can do a miraculous job of figuring out the developer's intent and providing the needed code. It promises to take some of the grunge work out of development and increase developer productivity. Of course, it can happily generate security vulnerabilities; it also uploads the code you're working on and remembers if you took its suggestions, but that's the world we've built for ourselves.

Machine-learning systems, of course, must be trained on large amounts of data. Happily for GitHub, it just happens to be sitting on a massive pile of code, most of which is under free-software licenses. So the company duly used the code in the publicly available repositories it hosts to train this model; evidently private repositories were not used for this purpose. For now, the result is available as a restricted beta offering; the company plans to turn it into a commercial product going forward.

Copy-and-paste

Looked at one way, GitHub Copilot is the embodiment of a number of aspects of software development that are, perhaps, not fully covered in school:

  • Much of a software developer's time is spent cranking out boilerplate code that looks much like a lot of other boilerplate code in circulation. Unsurprisingly, developers do not find being freed of this work to be a distasteful prospect.
  • An awful lot of software development is actually done by copying and pasting code. It's tempting to say that this is especially true of contemporary developers, but development worked this way even before the days of Stack Overflow.
  • While we like to think that the code we write is original, we are all strongly influenced by code we have seen in the past. Developers who have read a lot of code tend to have many useful patterns at their fingertips.

If much of our work really comes down to copying and pasting at varying degrees of remove, perhaps it makes sense to get the computer to do that work for us when it can.

The use of free software to train Copilot has raised some interesting questions, though. If a machine-learning model has been trained on a particular body of code, is that model a derived work of that code? If so, since GPL-licensed code was used to train the model, the result would also come under the terms of the GPL. If that were true, it would not change much, since GitHub does not appear to have any interest in distributing its model.

But what about the code that Copilot spits out? Is that code, too, a derived work of the code used to train the model? The fact that Copilot occasionally regurgitates verbatim copies of the training code (0.1% of the time, according to the Copilot FAQ) tends to support those who believe that Copilot's output should be seen as a derived work. If this is true, then any code body using Copilot output is in the same situation, which would be a bit of a mess, since it will be derived from multiple bodies of code with conflicting licenses and an endless list of attribution requirements. The derived-work interpretation would make any code developed with Copilot's help entirely undistributable.

The best outcome is unclear

Your editor is not a lawyer and certainly does not wish to play one on the net. That said, there are arguments to be made to the effect that Copilot's output should not be seen as a derived work of the code used for training. Certainly GitHub sees it that way; the Copilot FAQ states: "Training machine learning models on publicly available data is considered fair use across the machine learning community". How closely that consideration matches actual copyright law is not entirely clear, but it is an ongoing precedent and practice.

More intuitively, one can easily compare Copilot with a seasoned software developer who has seen a lot of code over a long career. The code that developer writes today will surely be influenced by what they have seen in the past, but today's code is not generally seen as being a derived work of yesterday's reading. One could argue that Copilot is doing the same thing; the only difference is that, since it's a computer, it can read vast amounts of code — even PHP code — without going insane.

Former European parliamentarian Julia Reda makes the argument that the code snippets produced by Copilot are not large or complex enough to be considered original, copyrightable works. One might well wonder how much better Copilot has to get before that line will be crossed, but she also claims that "the output of a machine simply does not qualify for copyright protection – it is in the public domain". This argument, if taken to its extreme, suggests that copyrighted work could be put into the public domain by running it through a photocopier. These arguments may hold for now, but it's not clear that they are tenable in the long term.

More interestingly, Reda, along with Matthew Garrett, argues that a derived-work interpretation is not in the interests of the free-software community in any case. Copyleft, they say, is a response to overly strong copyright protection for code, not a reason to make it stronger. As Garrett put it:

The powers that the GPL uses to enforce sharing of code are used by the authors of proprietary software to reduce that sharing. They attempt to forbid us from examining their code to determine how it works - they argue that anyone who does so is tainted, unable to contribute similar code to free software projects in case they produce a derived work of the original. Broadly speaking, the further the definition of a derived work reaches, the greater the power of proprietary software authors.

On the other hand, he continues, systems like Copilot offer the prospect of training models with proprietary code and using the result without worries of being tainted. That, he says, is likely to be a positive outcome for the free-software community.

It seems reasonable to assume that Copilot is not the only machine-learning-based code-synthesis system out there; it is also plausible that these systems will become more capable over time. The copyright issues raised by Copilot seem to be concentrated on free software for now, but they may well expand beyond that realm in the future. What happens now, though, will set precedents for the that future; if the free-software community somehow shuts down Copilot over copyright issues, other interests will have a stronger argument for strengthened copyright laws applied to future systems. That power could be used to extend the reach of proprietary software or shut down machine-learning systems that are beneficial to the community. We should, thus, be careful about what we wish for, lest we actually get it.

Comments (98 posted)

NUMA policy and memory types

By Jonathan Corbet
July 16, 2021
Non-uniform memory access (NUMA) systems have an architecture that attaches memory to "nodes" within the system. CPUs, too, belong to nodes; memory that is attached to the same node as a CPU will be faster to access (from that CPU) than memory on other nodes. This aspect of performance has important implications for programs running on NUMA systems, and the kernel offers a number of ways for user space to optimize their behavior. The NUMA abstraction is now being extended, though, and that is driving a need for new ways of influencing memory allocation; the multi-preference memory policy patch set is an attempt to meet that need.

Memory policies

There is no one-size-fits-all allocation policy that yields the best performance for all workloads. If an application can run entirely within a single NUMA node, the best policy is often to allocate all memory on the same node so that all accesses are local (and therefore fast). Larger applications may want to restrict allocations to a subset of the available nodes. For others, though, the best policy may be to distribute allocations across the system so that performance is roughly uniform across all CPUs and overall bandwidth is maximized. As a general rule, the kernel cannot determine what the best policy for any given process might be.

Thus, it's up to user space to help the kernel with memory-allocation policies; there are two system calls for that purpose. set_mempolicy() sets the default memory policy for the calling thread, while mbind() sets policies for specific portions of the calling process's address space. There are a number of options that can be used with either system call. For example, MPOL_PREFERRED asks the kernel to allocate memory on a single "preferred" node if possible. A process can, instead, use MPOL_BIND to provide a set of nodes that must be used for all memory allocations. MPOL_INTERLEAVE asks the kernel to spread allocations, page by page, across the specified set of nodes. And so on.

These options are generally sufficient for the needs of performance-tuned applications — or, at least, they used to be. But there is a bit of a shift in memory technology underway. The kernel's NUMA support was designed in a world where all memory attached to any given system is the same, and the only difference is the node the memory is attached to. In that world, the only parameter of interest is how local to the CPU any given range of memory is.

The plot thickens

Increasingly, though, systems are being built with multiple types of memory. The most common example at the moment is systems with persistent memory installed; while that memory can be used for long-term storage, it can also be used as a slower form of normal RAM. Persistent memory has the advantage of being relatively cheap; the ability to install lots of it may more than make up for its lower performance for many workloads. Meanwhile, vendors are also working on high-performance memory that is faster than ordinary DRAM, but which is too expensive to use exclusively.

This presents the kernel (and user space) with a decision that it didn't have to make before: which type of memory should be used to satisfy an allocation request? The solution that has been adopted so far is to organize exotic memory arrays into special, CPU-less NUMA nodes. An application that knows it wants a big chunk of slow memory can set its memory policy to allocate from the appropriate node, and slow memory is what it will get.

There is one little problem with using the existing NUMA infrastructure this way, though: it doesn't quite fit the problem space. An application that is willing to use slower memory is like an economy-class airline passenger; it is unlikely to be upset if it is given an upgrade at allocation (boarding) time. That application may suggest allocating from a slow-memory node, but allocations should fall back to normal memory if the slow variety is unavailable.

The semantics of MPOL_BIND do not allow for this behavior; it is a more strict regime. If the policy is MPOL_BIND and no memory is available on the specified nodes, unfortunate things can happen; these include the unleashing of the out-of-memory killer or the delivery of unexpected SIGSEGV signals to the allocating process. The willingness to use slower memory does not generally extend to a willingness to see things collapse in flames if that memory doesn't happen to be available, so it is hard to blame users if they see MPOL_BIND as being a little too binding.

On the other hand, MPOL_PREFERRED would appear to be the needed option; it expresses a preference that can be bypassed if the preferred node has no available memory. The problem is that, for reasons known only to the designer of that interface, MPOL_PREFERRED only allows the specification of a single preferred node. If the desired memory is distributed across multiple nodes, which is not unlikely, this interface will not allow an application to make use of all of it.

Multi-preference policies

The multi-preference memory policy patch set, which contains work by Feng Tang, Dave Hansen, and Ben Widawsky, seeks to address this problem by adding another option called MPOL_PREFERRED_MANY; it behaves like MPOL_PREFERRED except that it allows multiple nodes to be specified. Programs using this option can request allocation from a set of nodes offering the desired type of memory, but the kernel can allocate from elsewhere if the desired nodes lack available resources. The patch itself is relatively simple, though it does take a little work to wire up the new option in all of the desired places.

This work solves the immediate problem, but it does sidestep a relevant question: is the NUMA abstraction the right tool for choosing between different types of memory? Arguments in favor include the facts that it works now, doesn't require a whole new memory-type API, and often matches the actual architecture of the underlying hardware. On the other hand, it does conflate two independent concerns (memory type and locality) and forces user space to work it all out. It feels a little awkward.

This is not the first time that this kind of question has been raised, but the appetite for new APIs remains low. Experience suggests that, as long as the NUMA API can be made to work for memory-type selection, there will not be a lot of pressure to supplement it with something else; the potential benefits probably do not justify the considerable extra cost. So MPOL_PREFERRED_MANY is probably the way things will go. The patch set appears to be about ready; this option may appear in a mainline kernel as soon as 5.15.

Comments (1 posted)

Descriptorless files for io_uring

By Jonathan Corbet
July 19, 2021
The lowly file descriptor is one of the fundamental objects in Linux systems. A file descriptor, which is a simple integer value, can refer to an open file — or to a network connection, a running process, a loaded BPF program, or a namespace. Over the years, the use of file descriptors to refer to transient objects has grown to the point that it can be difficult to justify an API that uses anything else. Interestingly, though, the io_uring subsystem looks as if it is moving toward its own number space separate from file descriptors.

Io_uring was created to solve the asynchronous I/O problem; this is a functionality that Linux has never supported as well as users would have liked. User space can queue operations in a memory segment that is shared directly with the kernel, allowing those operations to be initiated, in many cases, without the need for an expensive system call. Similarly, another shared-memory segment contains the results of those operations once they complete. Initially, io_uring focused on simple operations (reading and writing, for example), but it has quickly gained support for many other system calls. It is evolving into the general asynchronous-operation API that Linux systems have always lacked.

Fixed files

A read or write operation must specify both the file descriptor to be operated on and a buffer to hold the data. There is a fair amount of setup work that must be done in the kernel before that operation can proceed, though. That includes taking a reference to the open file (to prevent it from going away while the operation is underway) and locking down the memory for the buffer. That overhead can, in many cases, add up to a significant part of the total cost of the operation; since programs tend to perform multiple operations with the same file descriptors and the same buffers, this overhead can be paid many times for the same resources, and it can add up.

From the beginning, io_uring has included a way to reduce that overhead in the form of the io_uring_register() system call:

    int io_uring_register(unsigned int fd, unsigned int opcode,
                          void *arg, unsigned int nr_args);

If opcode is IORING_REGISTER_BUFFERS, the io_uring subsystem will perform the setup work for the nr_args buffers pointed to by arg and keep the result; those buffers can then be used multiple times without paying that setup cost each time. If, instead, opcode is IORING_REGISTER_FILES, then arg is interpreted as an array of nr_args file descriptors. Each file in that array will be referenced and held open so that, once again, it can be used efficiently in multiple operations. These file descriptors are called "fixed" in io_uring jargon.

There are a couple of interesting aspects to fixed files. One is that the application can call close() on the file descriptor associated with a fixed file, but the reference within io_uring will remain and will still be usable. The other is that a fixed file is not referenced in subsequent io_uring operations by its file-descriptor number. Instead, operations use the offset where that file descriptor appeared in the args array during the io_uring_register() call. So if file descriptor 42 was placed in args[13], it will subsequently be known as fixed file 13 within io_uring.

So the io_uring subsystem has, in essence, set up a parallel descriptor space that can refer to open files, but which is independent of the regular file descriptors. In current kernels, though, it is still necessary to obtain a regular file descriptor for a file and register it for the file to appear in the io_uring fixed-file space. If, however, an application will never do anything with a file outside of io_uring, the creation of the regular file descriptor serves no real purpose.

It is, indeed, possible to create, use, and close a file descriptor entirely within io_uring. As noted above, this subsystem is not limited to simple I/O; it is also possible to open files and accept network connections with io_uring operations. At the moment, though, user space must intervene between the creation of the file descriptor and its use to install it as a fixed file. The cost of this work is not huge but it, too, can add up in an application that processes a lot of file descriptors.

No more file descriptors

To address this problem, Pavel Begunkov has posted this patch series adding a direct-to-fixed open operation. The io_uring operations that can create file descriptors — the equivalents of the openat2() and accept() system calls — gain the ability to, instead, store their result directly into the fixed-file table at a user-supplied offset. When this option is selected, there is no regular file descriptor created at all; the io_uring alternative descriptor is the only way to refer to the file.

The most likely use case for this feature is network servers; a busy server can create (with accept()) and use huge numbers of file descriptors in a short period of time. While io_uring operations, being asynchronous, can generally be executed in any order, it is possible to chain operations so that one does not begin before the previous one has successfully completed. Using this capability, a network server could queue a series of operations to accept the next incoming connection (storing it in the fixed-file table), write out the standard greeting, and initiate a read for the first data from the remote peer. User space would only need to become involved once that data has arrived and is ready to be processed.

This is clearly an interesting capability, and it shows how io_uring is quickly evolving into an alternative programming interface for Linux systems. The separation from the traditional file-descriptor space is just one more step in that direction. With the future addition of BPF support (which is still under development), the separation will become even more pronounced; the user-space component of some applications may become small indeed. Use of the io_uring API will probably not be worthwhile for the majority of applications, but for some it can make a large difference. It will be interesting to see where it goes from here.

Comments (52 posted)

Tor gets financial support for Arti development

By Jake Edge
July 20, 2021

There is a lot of buzz around the Rust programming language these days—which strikes some folks as irritating, ridiculous, or both. But the idea of a low-level language that can replace C, with fewer built-in security pitfalls, is attractive for any number of projects. Recently, the Tor Project announced the Arti project as a complete Rust rewrite of Tor's core protocols, which provide internet privacy and anonymity. In addition, Tor announced that Arti received a grant to support its development over the next year or so.

Tor

Tor came about in the early 2000s as a mechanism to route a user's packets through multiple relay nodes in its network using "onion routing". The idea is to obscure the connection between where a packet originates and its destination from outside observers. Onion routing was developed in the mid-1990s; it wraps a message in multiple layers of encryption that are peeled off and decrypted one-by-one at various relays in the network. Each relay can only decrypt its layer and does not know how many layers there are, nor where it is located in the chain of relays being used, though the "exit node" knows that it is the last in the chain as it needs to connect outside of the network to the actual destination for the packet. The name "Tor" is an acronym for "The Onion Router".

The core Tor application provides both client and server implementations of the protocols used in the network; it is largely written in C. There was a plan back in 2017 to slowly migrate pieces of the application to Rust. But, according to the blog post announcing Arti, "that hasn't worked out". The post went on to explain why the Tor developers decided on a full rewrite:

Our problem here is that the modules in our existing C code are not terribly well separated from one another: most modules are reachable from most other modules. That makes it hard for us to rewrite our code one module at a time, without first untangling it to be more modular. And untangling the code is risky, for all the same reasons that working in C is typically risky.

With a rewrite, we figured that we can keep our existing C code stable and make only minimal changes to it, while building up a working base of Rust code to serve as a basis for future development.

(And while we're writing a new implementation, we can clean up design issues that have been hard to fix in C. For example, the complicated structure of the C code has made it hard to adopt for embedding into other applications. But with our Arti rewrite, we can take embedding into account from the start, to help support applications down the road.)

Other projects that are looking at replacing pieces of their C code with Rust may well run into similar problems depending on the modularity of their code base.

Grant

Back in March, Tor co-founder Nick Mathewson posted about Arti (which stands for "A Rust Tor implementation") on the community forum for the Zcash cryptocurrency. It was, in effect, a pre-pitch to the community before making a grant proposal for Arti development to the Zcash Open Major Grants (ZOMG) program, which is part of the Zcash Foundation. Both the foundation and the grants program were funded by portions of the block reward for mining Zcash.

Mathewson had been encouraged to post by Holmes Wilson, who is on the ZOMG committee, because Wilson believes that Tor and Zcash fit well together. Zcash, which is implemented in Rust and C++, needs Tor or something like it in order to thwart de-anonymization efforts against its transactions by governments and others, he said. Zcash is aimed in part at providing more privacy for its users than other cryptocurrencies do. Past attempts to use Tor in Zcash have run aground on the difficulties of embedding the Tor code in other applications, at least in part.

Fast forward to July, and a nearly $700,000 grant has been awarded by ZOMG to Tor in order to get Arti to a production-ready 1.0 release—and a bit beyond—over the next year or so. That is meant to cover salaries for three developers and a part-time project manager. The Arti 1.1 milestone, which adds features that will help clients avoid network censorship measures, is also part of the work under the grant; it is scheduled for October 2022. The grant is focused on pieces that are directly usable by Zcash (and, of course, anyone else who wants to use Tor), but will not cover support for onion services, nor will it reach feature parity with the C code. Those pieces are left to milestones that are currently unfunded.

Why Rust?

There are various reasons that the Tor project is interested in a Rust implementation of its protocols. As a particularly security-sensitive project, Tor needs to be extremely careful about its code; using C is worrisome because "it's notoriously error-prone to use". That means extra scrutiny for even the simplest code, which slows the project down and makes new features more costly to implement. Rust can help fix many of those problems:

It's a high-level language, and significantly more expressive than C. What's more, it's got some really innovative features that let the language enforce certain safety properties at compile-time. To a first approximation, if the code compiles, and it isn't explicitly marked as "unsafe", then large categories of bugs are supposed to be impossible.

The Tor project has been tracking its security vulnerabilities since 2016. It turns out that "at least half of them were specifically due to mistakes that should be impossible in safe Rust code". The announcement explained that the project has long wanted to spread its cryptographic work to multiple cores, but that the prospect of doing so in C was daunting because of its fragility when accessing shared data within multi-threaded programs:

But in Rust, this kind of bug is easy to avoid: the same type system that keeps us from writing memory unsafety prevents us from writing dangerous concurrent access patterns. Because of that, Arti's circuit cryptography has been multicore from day 1, at very little additional programming effort.

Status

At this point, Arti is at the "eats babies" stage; it works, in a limited sense of that term, but does not provide any real privacy yet. It can run as a SOCKS proxy and connect via the Tor network, but it does not run as a Tor relay (i.e. server). It can be embedded in other Rust programs, allowing them to make connections to Tor as a client, but the API is not stable. Those who want to give Arti a test spin should consult the Contributing document, which describes hooking Arti up to Tor Browser—the project's version of Firefox specifically tweaked to better support users' privacy and to use the Tor network out of the box.

Arti is available under either version 2.0 of the Apache License or the MIT license (or both). Those looking to get involved can find some ideas for starting points in the Contributing document, including suggestions to review the code and the Architecture document (note that there is a module-organization diagram in the grant proposal). There is a list of issues that are tagged as "First Contribution", which might make for a good place to begin working on Arti. Regular reports on progress will be posted to the tor-dev mailing list; the first was posted at the same time as the announcement. In addition, meetings are being recorded and posted to the Tor project's YouTube channel in an Arti playlist.

The schedule seems fairly aggressive, especially since the team is also coming up to speed on Rust. The first phase is due in October, as Arti 0.0.1, providing basic anonymity services. Arti 0.1 is planned for release in March 2022; it will be ready for embedding into other applications, as well as improving the efficiency and user experience of the application. The production-ready Arti 1.0 release is scheduled for September 2022, with the 1.1 anti-censorship release coming shortly thereafter.

Over time, the C code is expected to wither away, but that will not happen for several years—at minimum. Even after Arti is production-ready for some use cases, there will still be more work needed to get to the point where Arti can do everything that the existing code can do. In particular, Tor relay support will need to be added, which is a substantial effort. But the Tor team will slowly be shifting its focus toward Rust and Arti, so the C code will slowly age out:

We expect that the pace of new features in C will slow, but we will continue fixing issues, shipping bugfixes, and solving important problems in our C code, until Rust is ready to replace it entirely. We will work to keep C Tor users secure, safe, and private, until it is finally ready to be replaced.

Arti will be an interesting project to watch. It should provide a nice experiment to try to show the benefits that Rust proponents have been touting. Arti is a fairly low-level networking application, and the internet provides plenty of "untrusted input" to surface the kinds of problems that have often tripped up similar C programs (including Tor itself, of course). The vulnerability tracking for Arti might well provide evidence for Rust's efficacy in avoiding attacks of that nature. Arti will also give other projects a look at what their future might hold, should they decide to make a similar switch.

Comments (4 posted)

Page editor: Jonathan Corbet
Next page: Brief items>>


Copyright © 2021, Eklektix, Inc.
Comments and public postings are copyrighted by their creators.
Linux is a registered trademark of Linus Torvalds