|
|
Subscribe / Log in / New account

LWN.net Weekly Edition for August 18, 2022

Welcome to the LWN.net Weekly Edition for August 18, 2022

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)

Tornado Cash and collateral damage

By Jake Edge
August 17, 2022

On August 8, the US government sanctioned the Tornado Cash cryptocurrency mixer for money laundering. The sanction means that no US citizen or company can interact with Tornado Cash in any way, all assets of the organization are to be reported so that they can be seized, and more. But at the core of Tornado Cash is a chunk of open-source code for "smart contracts" that run in the Ethereum blockchain; that code was "seized" as well. There are some disturbing implications here for our communities.

Mixing

Tornado Cash is (or was) a service for mixing cryptocurrency in order to provide privacy protection for the owners. While transactions on cryptocurrency blockchains are pseudonymous, the parties are only identified by public keys, there are ways to trace the transactions and to associate individuals with their holdings. Early on, one of the attractions of Bitcoin was its anonymity, but that turned out to be illusory. Cryptocurrency mixers provide a means to restore some level of anonymity to the system.

Mixers (or "tumblers") work by collecting up a bunch of deposits, which get coalesced into larger chunks over a random period of time, then doling out portions of that chunk as withdrawals at a later time. Naturally, a percentage of the deposits (1-3% normally) typically stay with the service as profit. Because the deposits and withdrawals are not synchronized in time, users can break the link between a particular chunk of cryptocurrency and their holdings. Privacy-conscious users presumably change the patterns of their withdrawals by using differing amounts than those of the deposits to further obfuscate any links.

Someone who wants to donate to a cause that might be unpopular with some nation-states (say, money for Ukraine) might use a mixer to avoid problems from a donation being traced back to them. Of course, there are others who want privacy for less savory reasons: criminals of various sorts. It seems clear that mixers are used for money laundering, but many tools, perhaps all, can be used for both good and ill. Typically, however, tools are not prosecuted (or sanctioned), people are.

There is a point at which any anonymity or pseudonymity generally must be shed due to existing anti-money-laundering protocols. In particular, when someone wants to turn their cryptocurrency into, say, US dollars (or any other real currency), they have to work with a financial organization that is subject to the "know your customer" (KYC) guidelines/rules. The financial organization will gather identity information from its customers establishing a link between a given address and a person. That is one of the prime reasons a criminal might want to mix their ill-gotten gains before a withdrawal, but there are perfectly legitimate reasons for others to do so.

We do not normally post our bank balances to the world at large, nor our credit-card purchase history. But if a link can be made between a particular address/key and an individual, that's effectively what would be available—stored immutably on the blockchain. A data breach at a KYC-following organization could "out" all of its customers, their holdings, and their transactions. It does not take a criminal mind to want to avoid that kind of disclosure.

What is the legal status of ten units of cryptocurrency obtained legally that is mixed with one unit, say, from some crime source? We deposit our money into banks that undoubtedly take in some deposits from dubious sources as well, but the presence of the "bad" money does not normally taint withdrawals made from them. Perhaps the goal is to force these mixers to follow KYC, but the nature of the Tornado Cash transaction means that there really is no "custodian" to do so.

The smart contracts that underlie Tornado Cash work in such a way that the participants and the smart-contract code share the custodial duties at some level. The contracts are programs that run on the blockchain to ensure (assuming no bugs) that all of the various parties perform the actions required of them. The code to make that all happen used to be available in the GitHub repository for Tornado Cash, but the repository disappeared on August 8 as well.

Beyond that, these smart contracts are still floating around the Ethereum blockchain and nothing—other than concern for being tainted by association with Tornado Cash perhaps—is stopping anyone from using them. In fact, someone would appear to be trolling the authorities by sending small amounts from prohibited Tornado Cash addresses to prominent people; those transactions cannot be declined, but receiving such a "gift" is technically in violation of the sanctions.

Reactions

Given that code has been found to be protected free speech, at least in the US, causing its removal from GitHub seems like a clear violation of the constitution. Cryptographer Matthew Green noted that, but also pointed out the irony of removing code from a distributed, decentralized revision-control system like Git. One suspects there are thousands of copies of the Git repository available in various places and that any attempt to "disappear" the code further will only make it spread.

The Electronic Frontier Foundation (EFF) is also concerned about this action, noting that the list of sanctioned entities has an open-source project (i.e. Tornado Cash) on it. It is worth noting that no individuals are on that list, which is mostly just a long list of cryptocurrency addresses, along with three other entries: "TORNADO CASH (a.k.a. TORNADO CASH CLASSIC; a.k.a. TORNADO CASH NOVA); Website tornado.cash; [...] Organization Established Date 2019; [...]"

However, GitHub went further than just taking down the repository for Tornado Cash; it also suspended the accounts of founders and contributors to the project. Beyond that, authorities in the Netherlands arrested one of the founders on August 12. Various other organizations have completely distanced themselves from Tornado Cash as well, including other cryptocurrency firms freezing assets associated with Tornado Cash. Meanwhile, anyone subject to US laws who has cryptocurrency deposited into Tornado Cash contracts cannot (legally) access it. Given the nature of the smart contracts, and the tenacity of the US government, anyone in that position should probably plan to simply walk away from those assets—ill-gained or no.

GitHub may well have had no choice but to remove the Tornado Cash repository, though the futility of doing so is fairly obvious. The code is effectively just collateral damage. But it would not be surprising to learn that repositories with the full history of the project, albeit with different hash values, already exists elsewhere on GitHub itself. Creating a mirror-image repository from a starting point one byte different than the original will create an entirely different Git "blockchain", after all; evading checks for identical object hashes is similarly trivial.

Suspending the contributor accounts is harder to understand, however. As noted, no individuals are on the sanctions list so there is no obvious reason (other than taint) to suspend them. It also seems likely that those contributors had other projects on GitHub; now those projects are unavailable, which may have a ripple effect throughout our communities. Those who are using GitHub for their projects may want to carefully consider what the company might do to their repositories—without any real evidence and no charges—down the road. GitHub is certainly not required to host anything or anyone, but these kinds of actions seem sure to give developers pause about using its services.

The press release from the US paints a typically bleak picture of what Tornado Cash has "done", though "facilitated" seems far more accurate. There are several notorious cryptocurrency thefts mentioned, including one made by a rogue state (North Korea), where the proceeds were laundered via Tornado Cash. Some $7-billion dollars worth of cryptocurrency was laundered that way, the release said, though that seems to lump any legitimate uses in with the criminals since that is the figure widely used for the total of all Tornado Cash transactions.

There is no doubt that criminals should be caught, convicted, and punished for these and other crimes, but it is easy to downplay the collateral damage of this kind of action by trying to paint it as mostly affecting criminals. It is all part of the same playbook that attacks strong encryption and tools like Tor because they can also be used by terrorists, child sexual abuse offenders, and the like. It is true that those tools can be used to facilitate such activities, but so can a wide variety of other things. The tools are not at fault.

Here at LWN, we generally do not pay much attention to the machinations of the cryptocurrency world—all of that tends to be far outside of our remit. There is a fair amount of distasteful hype, snake oil, and scams that go with that territory as well. But this event is not about any of that stuff, really; this comes down to a question of fundamental freedoms and human rights. As with the much-maligned asset forfeiture scheme, governments are taking things away, without due process, and forcing those wrongly affected to demonstrate how they were wronged—often at great expense.

This kind of event could also be something of a bellwether test for the "freedoms" and "rights" of code. The Tornado Cash code exists, it is running on an infrastructure that cannot (easily) be shut down; meanwhile, it can be used for good or not. To what lengths will humans go to try to make the bad things stop happening? Perhaps HAL 9000 is watching—with popcorn.

Comments (159 posted)

The trouble with 64-bit DMA

By Jonathan Corbet
August 11, 2022
We live in a 64-bit world, to the point that many distributors want to stop supporting 32-bit systems at all. However, lurking within our 64-bit kernels is a subsystem that has not really managed to move past 32-bit addresses. The quick merge-window failure of an attempt to use 64-bit addresses in the I/O memory-management unit (IOMMU) subsystem shows how hard it can be to leave all of one's 32-bit history behind.

Peripheral devices that move data at any significant rate have to support direct memory access (DMA) to get reasonable performance. As the DMA name suggests, these devices once had direct access to the system's memory in the physical address space. Over time, though, most systems have moved to interposing an IOMMU between devices and memory, for a number of reasons. The IOMMU can help to ensure that the device only accesses the memory that was intended for it, for example. It is also possible to use the IOMMU to make pages scattered throughout physical memory appear to be contiguous from the device's point of view.

For all of this to work, a device driver must create an IOMMU mapping for an I/O buffer before presenting the mapped addresses to the device. Those addresses, called I/O virtual addresses (or IOVAs), look like physical addresses, but they have their own 64-bit address space. One would expect to be able to pass an address anywhere in that range to a device, but life is not so simple; many devices have surprising limitations on how many address bits they can actually use. The kernel's DMA-mapping layer takes this into account; drivers pass in a mask indicating the address range that the device can handle, and the kernel finds an address within that range.

The IOMMU layer imposes an additional constraint, though, in that it will pick an address below 4GB (i.e. one that fits in 32 bits) if at all possible. In the early days of the PCI bus, a device performing DMA to a 64-bit address had to use a special "dual-address cycle" (DAC) mode with each access; DAC cycles were slower than single-address cycles, and a lot of devices either didn't implement them at all or had buggy implementations. Limiting IOVAs to the 32-bit range helped performance and danced around the ever-present possibility of hardware bugs.

It is now 2022, and the PCI bus has been superseded by PCI-Express, which does not have the same performance problems with DAC addresses. One might think that current hardware would not have trouble with 64-bit addresses, which are not exactly new technology at this point. The 32-bit constraint is still in place, though, and it is causing some pain of its own. Back in June, Robin Murphy posted a patch describing that pain:

The IOVA allocator doesn't behave all that well once the 32-bit space starts getting full. As DMA working sets get bigger, this optimisation increasingly backfires and adds considerable overhead to the dma_map path for use-cases like high-bandwidth networking. We've increasingly bandaged the allocator in attempts to mitigate this, but it remains fundamentally at odds with other valid requirements to try as hard as possible to satisfy a request within the given limit.

At a first glance, 4GB of DMA address space seems like it should be enough for anybody, but a big system with the right workload can fragment that space and make allocations hard. On the theory that the kernel is needlessly restricting its options to satisfy constraints that no longer make sense, Murphy changed the default so that the IOMMU layer would no longer try to find a 32-bit-compatible address and would, instead, use the full address range that the target device claimed to support. That makes the performance problem go away, which is a good thing.

The problem of buggy devices, though, cannot be made to disappear with a simple kernel patch. In a sense, that problem is even worse now, in that the 32-bit constraint may have papered over bugs in both devices and the drivers that control them for years. A driver author, perhaps an inexperienced developer who has not yet learned about the mendacity of hardware data sheets, may have trusted the documentation and told the DMA-mapping layer that their hardware could handle full 64-bit IOVAs when, in fact, it cannot. Now the only thing making that hardware actually work is the 32-bit constraint applied by the IOMMU layer.

Murphy acknowledged the risk that this change would expose this kind of bug; the patch included a couple of options for restoring the old behavior. But Murphy wanted to push the change through:

Let's be brave and default it to off in the hope that CI systems and developers will find and fix those bugs, but expect that desktop-focused distro configs are likely to want to turn it back on for maximum compatibility.

IOMMU maintainer Joerg Roedel applied the patch with reservations: "I don't have an overall good feeling about this, but as you said, let's be brave". The patch then landed in the mainline during the 6.0 merge window.

It didn't stay there for long, though. One of the core rules of kernel development is that good things rarely result from breaking Linus Torvalds's machine, and that is what happened here. He promptly reverted the change, saying: "It turns out that it was hopelessly naive to think that this would work, considering that we've always done this. The first machine I actually tested this on broke at bootup". He added that Murphy could "try again in a decade or so".

The problem, of course, is that the problems created by the 32-bit constraint are unlikely to get better by themselves in the next decade or so. There is going to be increasing pressure to leave that behavior behind, at least on machines where the hardware is known to work properly. Somehow, the community is going to have to find a way to change things that doesn't break systems across the planet. Perhaps drivers could set a new flag for hardware that is known to be good, or perhaps some sort of list could be maintained separately. The kernel has spent years papering over buggy hardware and drivers; climbing out of the resulting hole is likely to take a while as well.

Comments (62 posted)

A fuzzy issue of responsible disclosure

By Jonathan Corbet
August 12, 2022
Fuzz testing is the process of supplying a program with random inputs and watching to see what breaks; it has been responsible for the identification of vast numbers of bugs in recent years — and the fixing of many of them. Developers generally appreciate bug reports, but they can sometimes be a bit less enthusiastic about a flood of reports from automated fuzzing systems. A recent discussion around filesystem fuzzing highlighted two points of view on whether the current fuzz-testing activity is a good thing.

Filesystem code must accept input from two different directions. On one side is the system-call interface used by applications to work with files. Any bugs in this interface can have widespread implications ranging from data corruption to exploitable security vulnerabilities. But filesystem code also must deal with the persistent form of the filesystems it manages. On-disk filesystem representations are complex data structures that can become corrupted in a number of ways, ranging from hardware errors or filesystem bugs all the way to deliberate manipulation by an attacker.

Crashing when presented with a corrupted filesystem image is considered poor form, so filesystem developers generally try to keep that from happening. But it is hard to envision all of the ways in which a filesystem image can go wrong, especially if the corruption is created deliberately by a hostile actor. Many of our filesystems have their roots in a time when malicious filesystem images were not something that most people worried about; as a result, they may not be entirely well prepared for that situation. For this reason, allowing the mounting of untrusted filesystem images is generally seen as a bad idea.

It is thus not entirely surprising that, when fuzz-testers turn their attention to filesystem images, they tend to find bugs. Wenqing Liu has been doing this type of fuzzing for a while, resulting in the expected series of bug reports and filesystem fixes. One recent report from Liu of a bug found in the ext4 filesystem, though, drew some unhappy responses. XFS developer Darrick Wong started it off with this complaint:

If you are going to run some scripted tool to randomly corrupt the filesystem to find failures, then you have an ethical and moral responsibility to do some of the work to narrow down and identify the cause of the failure, not just throw them at someone to do all the work.

Lukas Czerner disagreed, saying that these bugs exist whether or not they are reported by fuzz testers and that reporters have no particular ethical responsibility to debug the problems they find. But Dave Chinner (also an XFS developer) saw things differently and made the case that these fuzzing reports are not "responsible disclosure":

Public reports like this require immediate work to determine the scope, impact and risk of the problem to decide what needs to be done next. All public disclosure does is start a race and force developers to have to address it immediately.

Responsible disclosure gives developers a short window in which they can perform that analysis without fear that somebody might already be actively exploiting the problem discovered by the fuzzer.

In a recent documentation patch, Wong complained about reports from "Fuzz Kiddiez", saying that: "The XFS maintainers' continuing ability to manage these events presents an ongoing risk to the stability of the development process".

A relevant question that neither Chinner nor Wong addressed, though, is which problem reports should be subject to this sort of "responsible disclosure" requirement? The nature of the kernel is such that a large portion of its bugs will have security implications if one looks at them hard enough; that is (part of) why kernel developers rarely even try to identify or separate out security fixes. Taken to its extreme, any public bug report could be seen as a failure to disclose responsibly. If that is not the intent, then the reporter of an ext4 filesystem crash is arguably being asked to make a determination that most kernel developers will not bother with.

Returning to the discussion, ext4 filesystem maintainer Ted Ts'o didn't think that this was a matter of responsible disclosure in any case:

I don't particularly worry about "responsible disclosure" because I don't consider fuzzed file system crashes to be a particularly serious security concern. There are some crazy container folks who think containers are just as secure(tm) as VM's, and who advocate allowing untrusted containers to mount arbitrary file system images and expect that this not cause the "host" OS to crash or get compromised. Those people are insane(tm), and I don't particularly worry about their use cases.

Ts'o went on to say that the hostility toward fuzzing reports in the XFS subsystem has caused fuzz testers to stop trying to work with that filesystem. Chinner vigorously disagreed with that statement, saying that the lack of fuzz-testing reports for XFS is due, instead, to the inability of current testers to find any bugs in that filesystem. That, he said, is because the fstests suite contains a set of XFS-specific fuzzing tests, so the sorts of bugs that fuzz testers can find have already been fixed in XFS.

Chinner also challenged Ts'o's description of this kind of bug as being low priority:

All it requires is a supply chain to be subverted somewhere, and now the USB drive that contains the drivers for your special hardware from a manufacturer you trust (and with manufacturer trust/anti-tamper seals intact) now powns your machine when you plug it in.

Ts'o, though, doubled down on the claim that exploiting these bugs requires physical access and said that, if an attacker has that access, there are many other bad things that can happen. Attackers have fuzzers too and know how to run them, he added, so little is gained by keeping the results hidden.

As one might imagine, there was no meeting of the minds that brought this exchange to a happy conclusion. Little is likely to change in any case; the people actually doing the fuzz testing were not a part of the conversation, and would be unlikely to change their behavior even if they had been. There appears to be a strong incentive to run up counts of bugs found by automated systems; it is not surprising that people respond to those incentives by developing and running those systems — and publicly posting the results.

The best solution may well be not doing as the XFS developers say (keeping crash reports hidden until developers agree that they can be disclosed) but, instead, as the XFS developers do. As Chinner described it, keeping the fuzzing tests in fstests happy has resulted in XFS "becoming largely immune to randomised fuzzing techniques". This protection clearly cannot be absolute; otherwise the XFS developers would view the activities of fuzz testers with more equanimity. But it may be indicative of the best way forward.

Making filesystems robust in the face of corrupted images has often come second to priorities like adding features and improving performance, but the experience with XFS would seem to indicate that, with some focused effort, progress can be made in that direction. Increasing the energy put into solidifying that side of filesystem code could make the issue of responsible disclosure of filesystem-image problems nearly moot.

Comments (81 posted)

6.0 Merge window, part 2

By Jonathan Corbet
August 15, 2022
Linus Torvalds released 6.0-rc1 and closed the merge window on August 14, as expected; by then, 13,543 non-merge changesets had found their way into the mainline repository. Just over half of those were pulled after our first 6.0 merge-window summary was written. The latter part of the merge window tends to be more focused on fixes than new features, but there were still a number of interesting changes added during this time.

The most significant changes pulled in the second half of the 6.0 merge window include:

Architecture-specific

  • The OpenRISC and LoongArch architectures have both gained support for PCI buses.
  • The RISC-V architecture now has support for the "Zicbom" extension, which provides for the management of devices with non-cache-coherent DMA.

Core kernel

  • The runtime verification subsystem has been merged. This subsystem allows the creation of a model of various kernel states and the transitions between them; should the kernel fail to follow the model at some point, it can raise the alarm or even panic the system. One example is the WIP model which ensures that wakeup events do not happen with preemption enabled.
  • The DAMON mechanism has gained some new features that can be used to influence memory management from user space. Most significant is the "LRU_SORT" module, which can reorder the least-recently-used lists to prioritize some pages over others. See this documentation commit for details.
  • Support for the creation of new CXL memory regions has been added; this commit changelog describes how the sysfs-based administrative interface works.

Filesystems and block I/O

  • The kernel has gained support for NVMe in-band authentication.
  • The ext4 filesystem supports two new ioctl() operations — EXT4_IOC_GETFSUUID and EXT4_IC_SETFSUUID, which can fetch or set the UUID stored in a filesystem superblock.
  • The NFSv4 server will now limit itself to 1024 active clients for each 1GB of memory in the system. Attempts to exceed that number will return temporary failures and start the process of cleaning up old, idle clients.

Hardware support

  • Clock: Microchip PolarFire realtime clocks, TI K3 realtime clocks, and Nuvoton NCT3018Y realtime clocks.
  • GPIO and pin control: Intel Meteor Lake pin and GPIO controllers, Renesas RZ/V2M pin and GPIO controllers, Renesas R-Car V4H pin controllers, Qualcomm 8909 pin controllers, Allwinner D1 pin controllers, and Qualcomm SM6375 pin controllers.
  • Input: Elan eKTH6915 i2c-hid touchscreens.
  • Miscellaneous: Microchip SAMA7G5 OTP controllers, Microchip Polarfire SPI FPGA managers, Qualcomm SPMI round-robin analog-to-digital converters, Qualcomm SM6350 interconnects, Freescale i.MX8MQ interconnects, MediaTek PCIe and DisplayPort PHYs, Meson G12A MIPI analog DPHYs, Freescale i.MX8qm LVDS PHYs, Microsoft Surface tablet-mode switches, Qualcomm SM8450 camera clock controllers, Qualcomm SM8350 graphics clock controllers, Apple audio DMA controllers, Renesas R-Car UFS controllers, Alibaba Elastic RDMA adapters, TI TPS380x reset controllers, I2C-connected TPM 2.0 trusted platform modules, Broadcom BCM63138 LED controllers, and Mellanox BlueField reset controllers.
  • Sound: NVIDIA Tegra210 output processing engines, Intel Meteor Lake audio interfaces, Qualcomm WSA8830/WSA8835 class-D amplifiers, Texas Instruments TAS2780 mono audio amplifiers, and Mediatek MT8186 audio interfaces.
  • USB: Aspeed USB2.0 device controllers, Microchip PolarFire USB controllers, Analogix ANX7411 type-C DRP port controllers, and STMicroelectronics STM32G0 type-C PD controllers.
  • Also: the VME bus subsystem has been moved into the staging directory (for the second time) and will be removed entirely unless somebody steps up to maintain it.

Miscellaneous

  • The perf tool has gained new reports describing lock contention and time used by kernel work; this merge commit describes both (and a lot of other perf enhancements as well). Even more perf enhancements came in with this merge.

Virtualization and containers

  • The "guest vCPU stall detector" is a new pseudo-device that serves as a sort of watchdog; a virtual machine must "pet" it occasionally or the host can conclude that the machine has stalled. This commit has a little more information.

Internal kernel changes

  • The CONFIG_ANDROID configuration option has been removed as "obviously a bad idea".
  • There is a new debugfs interface with a wealth of information about the known memory shrinkers and their performance. This documentation commit has the details.
  • The configuration option to build the kernel with -O3 optimization has been removed.

There are, of course, some things that didn't make it. The printk() pull request was rather severely rejected by Torvalds, so it seems there will be no work on that subsystem at all in this cycle. Coming after the reverting of the console-thread feature in 5.19, this makes two difficult cycles for that subsystem. Meanwhile, there had been some hopes that Rust support would be merged for 6.0, but that code still needs wider review.

There are also a couple of significant memory-management features that had been penciled in for 6.0 but were not pushed in the end. The maple tree data structure is getting closer, but was deemed to not be ready enough to go into this release. The multi-generational LRU feature ended up as collateral damage from that decision. The current plan, according to Andrew Morton, is to get both patch sets into -mm with an eye toward merging in 6.1.

First, though, the community has to get the 6.0 kernel out; that will mean finding and fixing a lot of bugs between now and the release date, which will almost certainly be either October 2 or 9.

Comments (2 posted)

From late-bound arguments to deferred computation, part 1

By Jake Edge
August 16, 2022

Back in November, we looked at a Python proposal to have function arguments with defaults that get evaluated when the function is called, rather than when it is defined. The article suggested that the discussion surrounding the proposal was likely to continue on for a ways—which it did—but it had died down by the end of last year. That all changed in mid-June, when the already voluminous discussion of the feature picked up again; once again, some people thought that applying the idea only to function arguments was too restrictive. Instead, a more general mechanism to defer evaluation was touted as something that could work for late-bound arguments while being useful for other use cases as well.

Background

Python programmers can specify default values for function arguments, which get used if no value is passed for the argument when the function is called. But that default value is determined by evaluating the expression at definition time, so the following will not do what is likely intended:

    def fn(a, max=len(a)):
        ...
That might look like max will default to the length of the argument a, but in reality it will default to length of some other a in an enclosing scope when the definition is being executed. If the other a does not exist, the definition will raise a NameError.

Chris Angelico wanted to provide a mechanism to tell the interpreter to evaluate the default expression when the function was called, which would fix that particular problem. He proposed using "=>" to indicate a late-bound default, so the definition of max would change to max=>len(a) in the example above. To that end, he authored PEP 671 ("Syntax for late-bound function argument defaults") in October and posted it to the python-ideas mailing list, which set off the first round of discussion that was covered in our article.

On December 1, he announced an updated version of the PEP, which set off an even longer discussion thread. The level of opposition to the feature had grown in the interim, it would seem. For some, the feature did not truly provide enough "bang for the buck"; there are simply not enough real-world uses of it to offset the cost in terms of development and maintenance. Beyond that, adding a feature like this increases the cognitive load of the language, which affects existing developers as well as those just learning Python.

But there was also a possible alternative that kept cropping up in the various discussions: deferred computation. Having a way to specify that an expression should not be evaluated until its value is truly needed would provide a more general solution—or so it is argued. But no PEP, or even concrete description, for deferred computation emerged, so proponents of PEP 671 were often frustrated by the amorphous nature of the supposed alternative. Things got rather heated in the discussion, to the point that a list moderator placed python-ideas in moderated mode for a day, but it all wound down without any sort of resolution in mid-December.

More discussion

That all changed in mid-June when the thread was revived by two posts, one from "Bluenix" opposed to the PEP and another by Steve Jorgensen strongly in favor of it. Angelico suggested that Bluenix create their own PEP "instead of just hating on my proposal"; he noted that it is more work to do so, of course, "but it would also be a lot more useful".

In response to Jorgensen, Rob Cliffe asked about the status of the PEP. Angelico said that he had wearied of the earlier discussion, so he had back-burnered the PEP; he had suggestions for how to potentially revive it, though it would seem that folks decided on more python-ideas discussion instead. That discussion got heated and contentious at times, as well; it seems clear that all "sides" have dug into their positions and there is little—or no—room for changing minds.

Another use case where Angelico sees value for late-bound defaults is with a default collection such as a list. The "obvious" way to write that probably does not do, what the programmer expects:

    # naive version
    def fn(arg=[]):    # creates the list at compile time
        ...

    # better version, but more verbose
    def fn(arg=None):
        if arg is None:
            arg = []   # creates the list each time
        ...

    # PEP 671 version
    def fn(arg=>[]):
        ...
The "naive" version will reuse the same list every time fn() is called without passing arg; the other two would create a new empty list every time. The extra code needed in the second version could be eliminated with PEP 671, but "I don't find that burdensome enough to want syntax", Stephen J. Turnbull said. Paul Moore agreed:
It's somewhat attractive in theory, adding a short form for something which people do tend to write out "longhand" at the moment. But the saving is relatively small, and there are a number of vaguely annoying edge cases that probably don't come up often, but overall just push the proposal into the "annoyingly complicated" area. The net result is that we get something that *might* help with a minor annoyance, but the cost in (theoretical, but necessary) complexity is just a bit too high.

One of those complexities is how to handle default values that refer to other arguments. The PEP specifies that late-bound defaults are evaluated in left-to-right order as specified in the function definition, but even then there are oddities, such as:

    def fn(n=>len(items), items=[]):
        ...
If items were a late-bound default, a call to fn() with no arguments would be illegal, because of the left-to-right rule. The PEP waffles a bit on what should happen with a (confusing) function definition like that; "implementations MAY choose to do so in two separate passes, first for all passed arguments and early-bound defaults, and then a second pass for late-bound defaults". As Moore noted, though, those and other edge cases should be rare.

Deferred computation

As before, David Mertz and Steven D'Aprano were the main proponents of at least considering a generalized deferred-computation mechanism before setting the late-bound argument syntax in stone. Mertz said that he still opposed the PEP, but that he might be swayed if a "soft keyword" version was proposed. Soft keywords are new keywords that are added to the language in a way that does not preclude their existing uses as variable and function names. New language keywords are generally avoided because of their impact on existing code, but the switch to a PEG parser for CPython allows context-specific keywords; for example, the match and case keywords for the structural pattern matching feature are soft keywords.

Mertz suggested that a keyword version of late-bound defaults, using later, defer, or delay, for example, would make the function-argument handling less of a special case. That keyword could be used in other contexts if the more-general deferred-computation mechanism appears. But it is clear that some participants were losing their patience with the constant "deferred computation" refrain in the absence of any kind of concrete specification (or even description) of how that might look. Cliffe said that the late-bound defaults and deferred computation (or "lazy evaluation" as he called it) are orthogonal to each other. He thought that raising lazy evaluation in the context of PEP 671 was off topic—or even FUD.

D'Aprano disagreed with the idea that the two are unrelated, however; "Late-bound defaults is a very small subset of lazy evaluation." There is a subtle difference, however, that Angelico raised (and not for the first time): the scope of the default evaluation specified in the PEP is rather different from what a generalized lazy scheme would provide. The PEP specifies that the evaluation of the default expression is done in the function's run-time scope:

    def fn(stuff, max=>len(stuff)):
	stuff.append(1)
	print(max)
So a call to fn([]) would print 0 since that is the length of stuff in the function before any code in it is run. But if max were deferred, thus only evaluated when it was needed for the print() call, it would print 1. There is some further weirdness that was not directly mentioned there, however: in the deferred case, if the print() call is moved up a line to before the append, it would print 0 as well. So the default argument would change value depending on where it is referred to in the function.

To a certain extent, D'Aprano waved away that objection. Angelico responded to that, clearly frustrated, by demanding that D'Aprano actually go implement what he was describing; "You'll find it's a LOT more problematic than you claim." He also suggested that D'Aprano simply specify what a deferred evaluation object would look like for Python and how it could be used to implement late-bound defaults, but Angelico's rhetoric gets in the way of what he is trying to say, as Brendan Barnwell pointed out. Moore also suggested toning things down; he said that Angelico is forgetting that there are other participants who may just be trying to follow along:

Asking for more concrete examples of the proposed alternative is reasonable. Getting frustrated when they are not provided is understandable, but doesn't help. Calling the proposed alternative "vapourware" just doubles down on the uncompromising "implement it or I'll ignore you" stance. And replying with increasingly frustrated posts that end up at a point where people like me can't even work out how we'd go looking to see whether anyone *had* provided concrete examples of deferred evaluation just makes things worse. All of which could have been avoided by simply including an early argument posted here in the PEP, under rejected alternatives, with a link to the post and a statement that "this was proposed as an alternative, but there's not enough detail provided to confirm how it would replace the existing proposal".

Moore's message refers back to an earlier exchange, where D'Aprano pushed for deferred evaluation to be added as part of the "rejected ideas" section of the PEP, which Angelico resisted. But Angelico did give some reasons why he felt the two concepts were not fully compatible:

Generic "lazy evaluation" is sufficient to do *some* of what can be done with late-bound argument defaults, but it is not sufficient for everything [...]

[...] Generic lazy evaluation should be processed at some point where the parameter is used, but late-bound defaults are evaluated as the function begins. They are orthogonal.

At the time, Moore suggested including some of that explanation into the PEP. Had that happened, perhaps some (small part) of the unpleasantness in the thread could have been avoided. Angelico did add some text about deferred evaluation back into the PEP a few days later, though.

Part 2

On June 21, Mertz posted the first version of his proto-PEP for "Generalized deferred computation". It was meant to flesh out the idea of a mechanism to defer the evaluation of Python expressions and to incorporate late-bound defaults as well. Stay tuned for part 2, which looks at the proposal and the reactions to it.

Comments (15 posted)

Page editor: Jonathan Corbet

Inside this week's LWN.net Weekly Edition

  • Briefs: CVE-2021-0920 analysis; Linux 6.0-rc1; Android 13; Rust 1.63; Quote; ...
  • Announcements: Newsletters, conferences, security updates, patches, and more.
Next page: Brief items>>

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