fs: stream_open - opener for stream-like files so that read and write can run simultaneously without deadlock
Commit9c225f2655
("vfs: atomic f_pos accesses as per POSIX") added locking for file.f_pos access and in particular made concurrent read and write not possible - now both those functions take f_pos lock for the whole run, and so if e.g. a read is blocked waiting for data, write will deadlock waiting for that read to complete. This caused regression for stream-like files where previously read and write could run simultaneously, but after that patch could not do so anymore. See e.g. commit581d21a2d0
("xenbus: fix deadlock on writes to /proc/xen/xenbus") which fixes such regression for particular case of /proc/xen/xenbus. The patch that added f_pos lock in 2014 did so to guarantee POSIX thread safety for read/write/lseek and added the locking to file descriptors of all regular files. In 2014 that thread-safety problem was not new as it was already discussed earlier in 2006. However even though 2006'th version of Linus's patch was adding f_pos locking "only for files that are marked seekable with FMODE_LSEEK (thus avoiding the stream-like objects like pipes and sockets)", the 2014 version - the one that actually made it into the tree as9c225f2655
- is doing so irregardless of whether a file is seekable or not. See https://lore.kernel.org/lkml/53022DB1.4070805@gmail.com/ https://lwn.net/Articles/180387 https://lwn.net/Articles/180396 for historic context. The reason that it did so is, probably, that there are many files that are marked non-seekable, but e.g. their read implementation actually depends on knowing current position to correctly handle the read. Some examples: kernel/power/user.c snapshot_read fs/debugfs/file.c u32_array_read fs/fuse/control.c fuse_conn_waiting_read + ... drivers/hwmon/asus_atk0110.c atk_debugfs_ggrp_read arch/s390/hypfs/inode.c hypfs_read_iter ... Despite that, many nonseekable_open users implement read and write with pure stream semantics - they don't depend on passed ppos at all. And for those cases where read could wait for something inside, it creates a situation similar to xenbus - the write could be never made to go until read is done, and read is waiting for some, potentially external, event, for potentially unbounded time -> deadlock. Besides xenbus, there are 14 such places in the kernel that I've found with semantic patch (see below): drivers/xen/evtchn.c:667:8-24: ERROR: evtchn_fops: .read() can deadlock .write() drivers/isdn/capi/capi.c:963:8-24: ERROR: capi_fops: .read() can deadlock .write() drivers/input/evdev.c:527:1-17: ERROR: evdev_fops: .read() can deadlock .write() drivers/char/pcmcia/cm4000_cs.c:1685:7-23: ERROR: cm4000_fops: .read() can deadlock .write() net/rfkill/core.c:1146:8-24: ERROR: rfkill_fops: .read() can deadlock .write() drivers/s390/char/fs3270.c:488:1-17: ERROR: fs3270_fops: .read() can deadlock .write() drivers/usb/misc/ldusb.c:310:1-17: ERROR: ld_usb_fops: .read() can deadlock .write() drivers/hid/uhid.c:635:1-17: ERROR: uhid_fops: .read() can deadlock .write() net/batman-adv/icmp_socket.c:80:1-17: ERROR: batadv_fops: .read() can deadlock .write() drivers/media/rc/lirc_dev.c:198:1-17: ERROR: lirc_fops: .read() can deadlock .write() drivers/leds/uleds.c:77:1-17: ERROR: uleds_fops: .read() can deadlock .write() drivers/input/misc/uinput.c:400:1-17: ERROR: uinput_fops: .read() can deadlock .write() drivers/infiniband/core/user_mad.c:985:7-23: ERROR: umad_fops: .read() can deadlock .write() drivers/gnss/core.c:45:1-17: ERROR: gnss_fops: .read() can deadlock .write() In addition to the cases above another regression caused by f_pos locking is that now FUSE filesystems that implement open with FOPEN_NONSEEKABLE flag, can no longer implement bidirectional stream-like files - for the same reason as above e.g. read can deadlock write locking on file.f_pos in the kernel. FUSE's FOPEN_NONSEEKABLE was added in 2008 ina7c1b990f7
("fuse: implement nonseekable open") to support OSSPD. OSSPD implements /dev/dsp in userspace with FOPEN_NONSEEKABLE flag, with corresponding read and write routines not depending on current position at all, and with both read and write being potentially blocking operations: See https://github.com/libfuse/osspd https://lwn.net/Articles/308445 https://github.com/libfuse/osspd/blob/14a9cff0/osspd.c#L1406 https://github.com/libfuse/osspd/blob/14a9cff0/osspd.c#L1438-L1477 https://github.com/libfuse/osspd/blob/14a9cff0/osspd.c#L1479-L1510 Corresponding libfuse example/test also describes FOPEN_NONSEEKABLE as "somewhat pipe-like files ..." with read handler not using offset. However that test implements only read without write and cannot exercise the deadlock scenario: https://github.com/libfuse/libfuse/blob/fuse-3.4.2-3-ga1bff7d/example/poll.c#L124-L131 https://github.com/libfuse/libfuse/blob/fuse-3.4.2-3-ga1bff7d/example/poll.c#L146-L163 https://github.com/libfuse/libfuse/blob/fuse-3.4.2-3-ga1bff7d/example/poll.c#L209-L216 I've actually hit the read vs write deadlock for real while implementing my FUSE filesystem where there is /head/watch file, for which open creates separate bidirectional socket-like stream in between filesystem and its user with both read and write being later performed simultaneously. And there it is semantically not easy to split the stream into two separate read-only and write-only channels: https://lab.nexedi.com/kirr/wendelin.core/blob/f13aa600/wcfs/wcfs.go#L88-169 Let's fix this regression. The plan is: 1. We can't change nonseekable_open to include &~FMODE_ATOMIC_POS - doing so would break many in-kernel nonseekable_open users which actually use ppos in read/write handlers. 2. Add stream_open() to kernel to open stream-like non-seekable file descriptors. Read and write on such file descriptors would never use nor change ppos. And with that property on stream-like files read and write will be running without taking f_pos lock - i.e. read and write could be running simultaneously. 3. With semantic patch search and convert to stream_open all in-kernel nonseekable_open users for which read and write actually do not depend on ppos and where there is no other methods in file_operations which assume @offset access. 4. Add FOPEN_STREAM to fs/fuse/ and open in-kernel file-descriptors via steam_open if that bit is present in filesystem open reply. It was tempting to change fs/fuse/ open handler to use stream_open instead of nonseekable_open on just FOPEN_NONSEEKABLE flags, but grepping through Debian codesearch shows users of FOPEN_NONSEEKABLE, and in particular GVFS which actually uses offset in its read and write handlers https://codesearch.debian.net/search?q=-%3Enonseekable+%3D https://gitlab.gnome.org/GNOME/gvfs/blob/1.40.0-6-gcbc54396/client/gvfsfusedaemon.c#L1080 https://gitlab.gnome.org/GNOME/gvfs/blob/1.40.0-6-gcbc54396/client/gvfsfusedaemon.c#L1247-1346 https://gitlab.gnome.org/GNOME/gvfs/blob/1.40.0-6-gcbc54396/client/gvfsfusedaemon.c#L1399-1481 so if we would do such a change it will break a real user. 5. Add stream_open and FOPEN_STREAM handling to stable kernels starting from v3.14+ (the kernel where9c225f2655
first appeared). This will allow to patch OSSPD and other FUSE filesystems that provide stream-like files to return FOPEN_STREAM | FOPEN_NONSEEKABLE in their open handler and this way avoid the deadlock on all kernel versions. This should work because fs/fuse/ ignores unknown open flags returned from a filesystem and so passing FOPEN_STREAM to a kernel that is not aware of this flag cannot hurt. In turn the kernel that is not aware of FOPEN_STREAM will be < v3.14 where just FOPEN_NONSEEKABLE is sufficient to implement streams without read vs write deadlock. This patch adds stream_open, converts /proc/xen/xenbus to it and adds semantic patch to automatically locate in-kernel places that are either required to be converted due to read vs write deadlock, or that are just safe to be converted because read and write do not use ppos and there are no other funky methods in file_operations. Regarding semantic patch I've verified each generated change manually - that it is correct to convert - and each other nonseekable_open instance left - that it is either not correct to convert there, or that it is not converted due to current stream_open.cocci limitations. The script also does not convert files that should be valid to convert, but that currently have .llseek = noop_llseek or generic_file_llseek for unknown reason despite file being opened with nonseekable_open (e.g. drivers/input/mousedev.c) Cc: Michael Kerrisk <mtk.manpages@gmail.com> Cc: Yongzhi Pan <panyongzhi@gmail.com> Cc: Jonathan Corbet <corbet@lwn.net> Cc: David Vrabel <david.vrabel@citrix.com> Cc: Juergen Gross <jgross@suse.com> Cc: Miklos Szeredi <miklos@szeredi.hu> Cc: Tejun Heo <tj@kernel.org> Cc: Kirill Tkhai <ktkhai@virtuozzo.com> Cc: Arnd Bergmann <arnd@arndb.de> Cc: Christoph Hellwig <hch@lst.de> Cc: Greg Kroah-Hartman <gregkh@linuxfoundation.org> Cc: Julia Lawall <Julia.Lawall@lip6.fr> Cc: Nikolaus Rath <Nikolaus@rath.org> Cc: Han-Wen Nienhuys <hanwen@google.com> Signed-off-by: Kirill Smelkov <kirr@nexedi.com> Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>
This commit is contained in:
parent
be76865df5
commit
10dce8af34
|
@ -622,9 +622,7 @@ static int xenbus_file_open(struct inode *inode, struct file *filp)
|
|||
if (xen_store_evtchn == 0)
|
||||
return -ENOENT;
|
||||
|
||||
nonseekable_open(inode, filp);
|
||||
|
||||
filp->f_mode &= ~FMODE_ATOMIC_POS; /* cdev-style semantics */
|
||||
stream_open(inode, filp);
|
||||
|
||||
u = kzalloc(sizeof(*u), GFP_KERNEL);
|
||||
if (u == NULL)
|
||||
|
|
18
fs/open.c
18
fs/open.c
|
@ -1215,3 +1215,21 @@ int nonseekable_open(struct inode *inode, struct file *filp)
|
|||
}
|
||||
|
||||
EXPORT_SYMBOL(nonseekable_open);
|
||||
|
||||
/*
|
||||
* stream_open is used by subsystems that want stream-like file descriptors.
|
||||
* Such file descriptors are not seekable and don't have notion of position
|
||||
* (file.f_pos is always 0). Contrary to file descriptors of other regular
|
||||
* files, .read() and .write() can run simultaneously.
|
||||
*
|
||||
* stream_open never fails and is marked to return int so that it could be
|
||||
* directly used as file_operations.open .
|
||||
*/
|
||||
int stream_open(struct inode *inode, struct file *filp)
|
||||
{
|
||||
filp->f_mode &= ~(FMODE_LSEEK | FMODE_PREAD | FMODE_PWRITE | FMODE_ATOMIC_POS);
|
||||
filp->f_mode |= FMODE_STREAM;
|
||||
return 0;
|
||||
}
|
||||
|
||||
EXPORT_SYMBOL(stream_open);
|
||||
|
|
|
@ -560,11 +560,12 @@ ssize_t vfs_write(struct file *file, const char __user *buf, size_t count, loff_
|
|||
|
||||
static inline loff_t file_pos_read(struct file *file)
|
||||
{
|
||||
return file->f_pos;
|
||||
return file->f_mode & FMODE_STREAM ? 0 : file->f_pos;
|
||||
}
|
||||
|
||||
static inline void file_pos_write(struct file *file, loff_t pos)
|
||||
{
|
||||
if ((file->f_mode & FMODE_STREAM) == 0)
|
||||
file->f_pos = pos;
|
||||
}
|
||||
|
||||
|
|
|
@ -158,6 +158,9 @@ typedef int (dio_iodone_t)(struct kiocb *iocb, loff_t offset,
|
|||
#define FMODE_OPENED ((__force fmode_t)0x80000)
|
||||
#define FMODE_CREATED ((__force fmode_t)0x100000)
|
||||
|
||||
/* File is stream-like */
|
||||
#define FMODE_STREAM ((__force fmode_t)0x200000)
|
||||
|
||||
/* File was opened by fanotify and shouldn't generate fanotify events */
|
||||
#define FMODE_NONOTIFY ((__force fmode_t)0x4000000)
|
||||
|
||||
|
@ -3074,6 +3077,7 @@ extern loff_t no_seek_end_llseek_size(struct file *, loff_t, int, loff_t);
|
|||
extern loff_t no_seek_end_llseek(struct file *, loff_t, int);
|
||||
extern int generic_file_open(struct inode * inode, struct file * filp);
|
||||
extern int nonseekable_open(struct inode * inode, struct file * filp);
|
||||
extern int stream_open(struct inode * inode, struct file * filp);
|
||||
|
||||
#ifdef CONFIG_BLOCK
|
||||
typedef void (dio_submit_t)(struct bio *bio, struct inode *inode,
|
||||
|
|
|
@ -0,0 +1,363 @@
|
|||
// SPDX-License-Identifier: GPL-2.0
|
||||
// Author: Kirill Smelkov (kirr@nexedi.com)
|
||||
//
|
||||
// Search for stream-like files that are using nonseekable_open and convert
|
||||
// them to stream_open. A stream-like file is a file that does not use ppos in
|
||||
// its read and write. Rationale for the conversion is to avoid deadlock in
|
||||
// between read and write.
|
||||
|
||||
virtual report
|
||||
virtual patch
|
||||
virtual explain // explain decisions in the patch (SPFLAGS="-D explain")
|
||||
|
||||
// stream-like reader & writer - ones that do not depend on f_pos.
|
||||
@ stream_reader @
|
||||
identifier readstream, ppos;
|
||||
identifier f, buf, len;
|
||||
type loff_t;
|
||||
@@
|
||||
ssize_t readstream(struct file *f, char *buf, size_t len, loff_t *ppos)
|
||||
{
|
||||
... when != ppos
|
||||
}
|
||||
|
||||
@ stream_writer @
|
||||
identifier writestream, ppos;
|
||||
identifier f, buf, len;
|
||||
type loff_t;
|
||||
@@
|
||||
ssize_t writestream(struct file *f, const char *buf, size_t len, loff_t *ppos)
|
||||
{
|
||||
... when != ppos
|
||||
}
|
||||
|
||||
|
||||
// a function that blocks
|
||||
@ blocks @
|
||||
identifier block_f;
|
||||
identifier wait_event =~ "^wait_event_.*";
|
||||
@@
|
||||
block_f(...) {
|
||||
... when exists
|
||||
wait_event(...)
|
||||
... when exists
|
||||
}
|
||||
|
||||
// stream_reader that can block inside.
|
||||
//
|
||||
// XXX wait_* can be called not directly from current function (e.g. func -> f -> g -> wait())
|
||||
// XXX currently reader_blocks supports only direct and 1-level indirect cases.
|
||||
@ reader_blocks_direct @
|
||||
identifier stream_reader.readstream;
|
||||
identifier wait_event =~ "^wait_event_.*";
|
||||
@@
|
||||
readstream(...)
|
||||
{
|
||||
... when exists
|
||||
wait_event(...)
|
||||
... when exists
|
||||
}
|
||||
|
||||
@ reader_blocks_1 @
|
||||
identifier stream_reader.readstream;
|
||||
identifier blocks.block_f;
|
||||
@@
|
||||
readstream(...)
|
||||
{
|
||||
... when exists
|
||||
block_f(...)
|
||||
... when exists
|
||||
}
|
||||
|
||||
@ reader_blocks depends on reader_blocks_direct || reader_blocks_1 @
|
||||
identifier stream_reader.readstream;
|
||||
@@
|
||||
readstream(...) {
|
||||
...
|
||||
}
|
||||
|
||||
|
||||
// file_operations + whether they have _any_ .read, .write, .llseek ... at all.
|
||||
//
|
||||
// XXX add support for file_operations xxx[N] = ... (sound/core/pcm_native.c)
|
||||
@ fops0 @
|
||||
identifier fops;
|
||||
@@
|
||||
struct file_operations fops = {
|
||||
...
|
||||
};
|
||||
|
||||
@ has_read @
|
||||
identifier fops0.fops;
|
||||
identifier read_f;
|
||||
@@
|
||||
struct file_operations fops = {
|
||||
.read = read_f,
|
||||
};
|
||||
|
||||
@ has_read_iter @
|
||||
identifier fops0.fops;
|
||||
identifier read_iter_f;
|
||||
@@
|
||||
struct file_operations fops = {
|
||||
.read_iter = read_iter_f,
|
||||
};
|
||||
|
||||
@ has_write @
|
||||
identifier fops0.fops;
|
||||
identifier write_f;
|
||||
@@
|
||||
struct file_operations fops = {
|
||||
.write = write_f,
|
||||
};
|
||||
|
||||
@ has_write_iter @
|
||||
identifier fops0.fops;
|
||||
identifier write_iter_f;
|
||||
@@
|
||||
struct file_operations fops = {
|
||||
.write_iter = write_iter_f,
|
||||
};
|
||||
|
||||
@ has_llseek @
|
||||
identifier fops0.fops;
|
||||
identifier llseek_f;
|
||||
@@
|
||||
struct file_operations fops = {
|
||||
.llseek = llseek_f,
|
||||
};
|
||||
|
||||
@ has_no_llseek @
|
||||
identifier fops0.fops;
|
||||
@@
|
||||
struct file_operations fops = {
|
||||
.llseek = no_llseek,
|
||||
};
|
||||
|
||||
@ has_mmap @
|
||||
identifier fops0.fops;
|
||||
identifier mmap_f;
|
||||
@@
|
||||
struct file_operations fops = {
|
||||
.mmap = mmap_f,
|
||||
};
|
||||
|
||||
@ has_copy_file_range @
|
||||
identifier fops0.fops;
|
||||
identifier copy_file_range_f;
|
||||
@@
|
||||
struct file_operations fops = {
|
||||
.copy_file_range = copy_file_range_f,
|
||||
};
|
||||
|
||||
@ has_remap_file_range @
|
||||
identifier fops0.fops;
|
||||
identifier remap_file_range_f;
|
||||
@@
|
||||
struct file_operations fops = {
|
||||
.remap_file_range = remap_file_range_f,
|
||||
};
|
||||
|
||||
@ has_splice_read @
|
||||
identifier fops0.fops;
|
||||
identifier splice_read_f;
|
||||
@@
|
||||
struct file_operations fops = {
|
||||
.splice_read = splice_read_f,
|
||||
};
|
||||
|
||||
@ has_splice_write @
|
||||
identifier fops0.fops;
|
||||
identifier splice_write_f;
|
||||
@@
|
||||
struct file_operations fops = {
|
||||
.splice_write = splice_write_f,
|
||||
};
|
||||
|
||||
|
||||
// file_operations that is candidate for stream_open conversion - it does not
|
||||
// use mmap and other methods that assume @offset access to file.
|
||||
//
|
||||
// XXX for simplicity require no .{read/write}_iter and no .splice_{read/write} for now.
|
||||
// XXX maybe_steam.fops cannot be used in other rules - it gives "bad rule maybe_stream or bad variable fops".
|
||||
@ maybe_stream depends on (!has_llseek || has_no_llseek) && !has_mmap && !has_copy_file_range && !has_remap_file_range && !has_read_iter && !has_write_iter && !has_splice_read && !has_splice_write @
|
||||
identifier fops0.fops;
|
||||
@@
|
||||
struct file_operations fops = {
|
||||
};
|
||||
|
||||
|
||||
// ---- conversions ----
|
||||
|
||||
// XXX .open = nonseekable_open -> .open = stream_open
|
||||
// XXX .open = func -> openfunc -> nonseekable_open
|
||||
|
||||
// read & write
|
||||
//
|
||||
// if both are used in the same file_operations together with an opener -
|
||||
// under that conditions we can use stream_open instead of nonseekable_open.
|
||||
@ fops_rw depends on maybe_stream @
|
||||
identifier fops0.fops, openfunc;
|
||||
identifier stream_reader.readstream;
|
||||
identifier stream_writer.writestream;
|
||||
@@
|
||||
struct file_operations fops = {
|
||||
.open = openfunc,
|
||||
.read = readstream,
|
||||
.write = writestream,
|
||||
};
|
||||
|
||||
@ report_rw depends on report @
|
||||
identifier fops_rw.openfunc;
|
||||
position p1;
|
||||
@@
|
||||
openfunc(...) {
|
||||
<...
|
||||
nonseekable_open@p1
|
||||
...>
|
||||
}
|
||||
|
||||
@ script:python depends on report && reader_blocks @
|
||||
fops << fops0.fops;
|
||||
p << report_rw.p1;
|
||||
@@
|
||||
coccilib.report.print_report(p[0],
|
||||
"ERROR: %s: .read() can deadlock .write(); change nonseekable_open -> stream_open to fix." % (fops,))
|
||||
|
||||
@ script:python depends on report && !reader_blocks @
|
||||
fops << fops0.fops;
|
||||
p << report_rw.p1;
|
||||
@@
|
||||
coccilib.report.print_report(p[0],
|
||||
"WARNING: %s: .read() and .write() have stream semantic; safe to change nonseekable_open -> stream_open." % (fops,))
|
||||
|
||||
|
||||
@ explain_rw_deadlocked depends on explain && reader_blocks @
|
||||
identifier fops_rw.openfunc;
|
||||
@@
|
||||
openfunc(...) {
|
||||
<...
|
||||
- nonseekable_open
|
||||
+ nonseekable_open /* read & write (was deadlock) */
|
||||
...>
|
||||
}
|
||||
|
||||
|
||||
@ explain_rw_nodeadlock depends on explain && !reader_blocks @
|
||||
identifier fops_rw.openfunc;
|
||||
@@
|
||||
openfunc(...) {
|
||||
<...
|
||||
- nonseekable_open
|
||||
+ nonseekable_open /* read & write (no direct deadlock) */
|
||||
...>
|
||||
}
|
||||
|
||||
@ patch_rw depends on patch @
|
||||
identifier fops_rw.openfunc;
|
||||
@@
|
||||
openfunc(...) {
|
||||
<...
|
||||
- nonseekable_open
|
||||
+ stream_open
|
||||
...>
|
||||
}
|
||||
|
||||
|
||||
// read, but not write
|
||||
@ fops_r depends on maybe_stream && !has_write @
|
||||
identifier fops0.fops, openfunc;
|
||||
identifier stream_reader.readstream;
|
||||
@@
|
||||
struct file_operations fops = {
|
||||
.open = openfunc,
|
||||
.read = readstream,
|
||||
};
|
||||
|
||||
@ report_r depends on report @
|
||||
identifier fops_r.openfunc;
|
||||
position p1;
|
||||
@@
|
||||
openfunc(...) {
|
||||
<...
|
||||
nonseekable_open@p1
|
||||
...>
|
||||
}
|
||||
|
||||
@ script:python depends on report @
|
||||
fops << fops0.fops;
|
||||
p << report_r.p1;
|
||||
@@
|
||||
coccilib.report.print_report(p[0],
|
||||
"WARNING: %s: .read() has stream semantic; safe to change nonseekable_open -> stream_open." % (fops,))
|
||||
|
||||
@ explain_r depends on explain @
|
||||
identifier fops_r.openfunc;
|
||||
@@
|
||||
openfunc(...) {
|
||||
<...
|
||||
- nonseekable_open
|
||||
+ nonseekable_open /* read only */
|
||||
...>
|
||||
}
|
||||
|
||||
@ patch_r depends on patch @
|
||||
identifier fops_r.openfunc;
|
||||
@@
|
||||
openfunc(...) {
|
||||
<...
|
||||
- nonseekable_open
|
||||
+ stream_open
|
||||
...>
|
||||
}
|
||||
|
||||
|
||||
// write, but not read
|
||||
@ fops_w depends on maybe_stream && !has_read @
|
||||
identifier fops0.fops, openfunc;
|
||||
identifier stream_writer.writestream;
|
||||
@@
|
||||
struct file_operations fops = {
|
||||
.open = openfunc,
|
||||
.write = writestream,
|
||||
};
|
||||
|
||||
@ report_w depends on report @
|
||||
identifier fops_w.openfunc;
|
||||
position p1;
|
||||
@@
|
||||
openfunc(...) {
|
||||
<...
|
||||
nonseekable_open@p1
|
||||
...>
|
||||
}
|
||||
|
||||
@ script:python depends on report @
|
||||
fops << fops0.fops;
|
||||
p << report_w.p1;
|
||||
@@
|
||||
coccilib.report.print_report(p[0],
|
||||
"WARNING: %s: .write() has stream semantic; safe to change nonseekable_open -> stream_open." % (fops,))
|
||||
|
||||
@ explain_w depends on explain @
|
||||
identifier fops_w.openfunc;
|
||||
@@
|
||||
openfunc(...) {
|
||||
<...
|
||||
- nonseekable_open
|
||||
+ nonseekable_open /* write only */
|
||||
...>
|
||||
}
|
||||
|
||||
@ patch_w depends on patch @
|
||||
identifier fops_w.openfunc;
|
||||
@@
|
||||
openfunc(...) {
|
||||
<...
|
||||
- nonseekable_open
|
||||
+ stream_open
|
||||
...>
|
||||
}
|
||||
|
||||
|
||||
// no read, no write - don't change anything
|
Loading…
Reference in New Issue