FPPS4/sys/fs/nullfs/null_vnops.pas

945 lines
19 KiB
Plaintext

unit null_vnops;
{$mode ObjFPC}{$H+}
{$CALLING SysV_ABI_CDecl}
interface
uses
vnode,
vnode_if,
vfs_default;
function null_bypass(ap:p_vop_generic_args):Integer;
function null_lookup(ap:p_vop_lookup_args):Integer;
function null_open(ap:p_vop_open_args):Integer;
function null_setattr(ap:p_vop_setattr_args):Integer;
function null_getattr(ap:p_vop_getattr_args):Integer;
function null_access(ap:p_vop_access_args):Integer;
function null_accessx(ap:p_vop_accessx_args):Integer;
function null_remove(ap:p_vop_remove_args):Integer;
function null_rename(ap:p_vop_rename_args):Integer;
function null_rmdir(ap:p_vop_rmdir_args):Integer;
function null_lock(ap:p_vop_lock1_args):Integer;
function null_unlock(ap:p_vop_unlock_args):Integer;
function null_inactive(ap:p_vop_inactive_args):Integer;
function null_reclaim(ap:p_vop_reclaim_args):Integer;
function null_print(ap:p_vop_print_args):Integer;
function null_getwritemount(ap:p_vop_getwritemount_args):Integer;
function null_vptofh(ap:p_vop_vptofh_args):Integer;
function null_vptocnp(ap:p_vop_vptocnp_args):Integer;
function null_link(ap:p_vop_link_args):Integer;
var
null_vnodeops:vop_vector=(
vop_default :nil;
vop_bypass :@null_bypass;
vop_islocked :@vop_stdislocked;
vop_lookup :@null_lookup;
vop_create :nil;
vop_whiteout :nil;
vop_mknod :nil;
vop_open :@null_open;
vop_close :nil;
vop_access :@null_access;
vop_accessx :@null_accessx;
vop_getattr :@null_getattr;
vop_setattr :@null_setattr;
vop_markatime :nil;
vop_read :nil;
vop_write :nil;
vop_ioctl :nil;
vop_poll :nil;
vop_kqfilter :nil;
vop_revoke :nil;
vop_fsync :nil;
vop_remove :@null_remove;
vop_link :@null_link;
vop_rename :@null_rename;
vop_mkdir :nil;
vop_rmdir :@null_rmdir;
vop_symlink :nil;
vop_readdir :nil;
vop_readlink :nil;
vop_inactive :@null_inactive;
vop_reclaim :@null_reclaim;
vop_lock1 :@null_lock;
vop_unlock :@null_unlock;
vop_bmap :@VOP_EOPNOTSUPP;
vop_strategy :@VOP_EOPNOTSUPP;
vop_getwritemount :@null_getwritemount;
vop_print :@null_print;
vop_pathconf :nil;
vop_advlock :nil;
vop_advlockasync :nil;
vop_advlockpurge :@vop_stdadvlockpurge;
vop_reallocblks :nil;
vop_getpages :nil;
vop_putpages :nil;
vop_vptofh :@null_vptofh;
vop_vptocnp :@null_vptocnp;
vop_allocate :nil;
vop_unp_bind :nil;
vop_unp_connect :nil;
vop_unp_detach :nil;
); public;
implementation
uses
sysutils,
nullfs,
null_subr,
errno,
vnamei,
vmount,
vfs_subr,
vfs_vnops,
vfs_cache,
kern_mtx;
type
ppp_vnode=^pp_vnode;
function MOUNTTONULLMOUNT(mp:p_mount):p_null_mount; inline;
begin
Result:=mp^.mnt_data;
end;
function VTONULL(vp:p_vnode):p_null_node; inline;
begin
Result:=vp^.v_data;
end;
function NULLTOV(xp:p_null_node):p_vnode; inline;
begin
Result:=xp^.null_vnode;
end;
function null_bypass(ap:p_vop_generic_args):Integer;
label
_out,
_nxt,
_err;
var
this_vp_p:pp_vnode;
error:Integer;
old_vps:array[0..VDESC_MAX_VPS-1] of p_vnode;
vps_p :array[0..VDESC_MAX_VPS-1] of pp_vnode;
vppp:ppp_vnode;
descp:p_vnodeop_desc;
reles,i:Integer;
begin
descp:=ap^.a_desc;
{
* Map the vnodes going in.
* Later, we'll invoke the operation based on
* the first mapped vnode's operation vector.
}
reles:=descp^.vdesc_flags;
i:=0;
while (i < VDESC_MAX_VPS) do
begin
if (descp^.vdesc_vp_offsets[i]=Byte(-1)) then
break; { bail out at end of list }
this_vp_p:=VOPARG_OFFSETTO(descp^.vdesc_vp_offsets[i],ap);
vps_p[i]:=this_vp_p;
{
* We're not guaranteed that any but the first vnode
* are of our type. Check for and don't map any
* that aren't. (We must always map first vp or vclean fails.)
}
if (i<>0) then
begin
if (this_vp_p^=nil) then
begin
old_vps[i]:=nil;
goto _nxt;
end;
if (this_vp_p^^.v_op<>@null_vnodeops) then
begin
old_vps[i]:=nil;
goto _nxt;
end;
end;
old_vps[i]:=this_vp_p^;
vps_p[i]^:=NULLVPTOLOWERVP(this_vp_p^);
{
* XXX - Several operations have the side effect
* of vrele'ing their vp's. We must account for
* that. (This should go away in the future.)
}
if ((reles and VDESC_VP0_WILLRELE)<>0) then
VREF(this_vp_p^);
//
_nxt:
reles:=reles shr 1;
Inc(i);
end;
{
* Call the operation on the lower layer
* with the modified argument structure.
}
if (vps_p[0]=nil) then goto _err;
if (vps_p[0]^<>nil) then
begin
error:=VCALL(ap);
end else
begin
_err:
Writeln(Format('null_bypass: no map for %s',[descp^.vdesc_name]));
error:=EINVAL;
end;
{
* Maintain the illusion of call-by-value
* by restoring vnodes in the argument structure
* to their original value.
}
reles:=descp^.vdesc_flags;
i:=0;
while (i < VDESC_MAX_VPS) do
begin
if (descp^.vdesc_vp_offsets[i]=Byte(-1)) then
break; { bail out at end of list }
if (old_vps[i]<>nil) then
begin
vps_p[i]^:=old_vps[i];
if ((reles and VDESC_VP0_WILLRELE)<>0) then
vrele((vps_p[i])^);
end;
//
reles:=reles shr 1;
Inc(i);
end;
{
* Map the possible out-going vpp
* (Assumes that the lower layer always Exits
* a VREF'ed vpp unless it gets an error.)
}
if (descp^.vdesc_vpp_offset<>VDESC_NO_OFFSET) and
((descp^.vdesc_flags and VDESC_NOMAP_VPP)=0) and
(error=0) then
begin
{
* XXX - even though some ops have vpp Exited vp's,
* several ops actually vrele this before Exiting.
* We must avoid these ops.
* (This should go away when these ops are regularized.)
}
if ((descp^.vdesc_flags and VDESC_VPP_WILLRELE)<>0) then
goto _out;
vppp:=VOPARG_OFFSETTO(descp^.vdesc_vpp_offset,ap);
if (vppp^<>nil) then
begin
error:=null_nodeget(old_vps[0]^.v_mount,vppp^^,vppp^);
end;
end;
_out:
Exit(error);
end;
{
* We have to carry on the locking protocol on the null layer vnodes
* as we progress through the tree. We also have to enforce read-only
* if this layer is mounted read-only.
}
function null_lookup(ap:p_vop_lookup_args):Integer;
var
cnp:p_componentname;
dvp:p_vnode;
flags:Integer;
vp,ldvp,lvp:p_vnode;
error:Integer;
begin
cnp:=ap^.a_cnp;
dvp:=ap^.a_dvp;
flags:=cnp^.cn_flags;
if ((flags and ISLASTCN)<>0) and
((p_mount(dvp^.v_mount)^.mnt_flag and MNT_RDONLY)<>0) and
((cnp^.cn_nameiop=DELETE) or (cnp^.cn_nameiop=RENAME)) then
begin
Exit(EROFS);
end;
{
* Although it is possible to call null_bypass(), we'll do
* a direct call to reduce overhead
}
ldvp:=NULLVPTOLOWERVP(dvp);
vp:=nil;
lvp:=nil;
if (ldvp<>nil) then
begin
error:=VOP_LOOKUP(ldvp, @lvp, cnp);
end else
begin
error:=ENOENT;
end;
if (error=EJUSTRETURN) and
((flags and ISLASTCN)<>0) and
((p_mount(dvp^.v_mount)^.mnt_flag and MNT_RDONLY)<>0) and
((cnp^.cn_nameiop=CREATE) or (cnp^.cn_nameiop=RENAME)) then
error:=EROFS;
if ((error=0) or (error=EJUSTRETURN)) and (lvp<>nil) then
begin
if (ldvp=lvp) then
begin
ap^.a_vpp^:=dvp;
VREF(dvp);
vrele(lvp);
end else
begin
error:=null_nodeget(dvp^.v_mount, lvp, @vp);
if (error=0) then
begin
ap^.a_vpp^:=vp;
end;
end;
end;
Exit(error);
end;
function null_open(ap:p_vop_open_args):Integer;
var
retval:Integer;
vp,ldvp:p_vnode;
begin
vp:=ap^.a_vp;
ldvp:=NULLVPTOLOWERVP(vp);
if (ldvp=nil) then Exit(0);
retval:=null_bypass(Pointer(ap));
if (retval=0) then
begin
vp^.v_object:=ldvp^.v_object;
end;
Exit(retval);
end;
{
* Setattr call. Disallow write attempts if the layer is mounted read-only.
}
function null_setattr(ap:p_vop_setattr_args):Integer;
var
vp:p_vnode;
vap:p_vattr;
begin
vp:=ap^.a_vp;
vap:=ap^.a_vap;
if ((p_mount(vp^.v_mount)^.mnt_flag and MNT_RDONLY)<>0) and
(
(vap^.va_flags <>VNOVAL) or
(vap^.va_uid <>VNOVAL) or
(vap^.va_gid <>VNOVAL) or
(vap^.va_atime.tv_sec<>VNOVAL) or
(vap^.va_mtime.tv_sec<>VNOVAL) or
(vap^.va_mode <>VNOVAL)
) then
begin
Exit(EROFS);
end;
if (vap^.va_size<>VNOVAL) then
begin
case (vp^.v_type) of
VDIR:
begin
Exit(EISDIR);
end;
VCHR,
VBLK,
VSOCK,
VFIFO:
begin
if (vap^.va_flags<>VNOVAL) then
begin
Exit(EOPNOTSUPP);
end;
Exit(0);
end;
else
begin
{
* Disallow write attempts if the filesystem is
* mounted read-only.
}
if ((p_mount(vp^.v_mount)^.mnt_flag and MNT_RDONLY)<>0) then
begin
Exit(EROFS);
end;
end;
end;
end;
if (NULLVPTOLOWERVP(vp)=nil) then Exit(0);
Exit(null_bypass(Pointer(ap)));
end;
{
* We handle getattr only to change the fsid.
}
function null_getattr(ap:p_vop_getattr_args):Integer;
var
error:Integer;
begin
if (NULLVPTOLOWERVP(ap^.a_vp)=nil) then Exit(0);
error:=null_bypass(Pointer(ap));
if (error<>0) then
begin
Exit(error);
end;
ap^.a_vap^.va_fsid:=p_mount(ap^.a_vp^.v_mount)^.mnt_stat.f_fsid.val[0];
Exit(0);
end;
{
* Handle to disallow write access if mounted read-only.
}
function null_access(ap:p_vop_access_args):Integer;
var
vp:p_vnode;
accmode:accmode_t;
begin
vp:=ap^.a_vp;
accmode:=ap^.a_accmode;
{
* Disallow write attempts on read-only layers;
* unless the file is a socket, fifo, or a block or
* character device resident on the filesystem.
}
if ((accmode and VWRITE)<>0) then
begin
case (vp^.v_type) of
VDIR,
VLNK,
VREG:
begin
if ((p_mount(vp^.v_mount)^.mnt_flag and MNT_RDONLY)<>0) then
begin
Exit(EROFS);
end;
end;
else;
end;
end;
if (NULLVPTOLOWERVP(vp)=nil) then Exit(0);
Exit(null_bypass(Pointer(ap)));
end;
function null_accessx(ap:p_vop_accessx_args):Integer;
var
vp:p_vnode;
accmode:accmode_t;
begin
vp:=ap^.a_vp;
accmode:=ap^.a_accmode;
{
* Disallow write attempts on read-only layers;
* unless the file is a socket, fifo, or a block or
* character device resident on the filesystem.
}
if ((accmode and VWRITE)<>0) then
begin
case (vp^.v_type) of
VDIR,
VLNK,
VREG:
begin
if ((p_mount(vp^.v_mount)^.mnt_flag and MNT_RDONLY)<>0) then
begin
Exit(EROFS);
end;
end;
else;
end;
end;
if (NULLVPTOLOWERVP(vp)=nil) then Exit(0);
Exit(null_bypass(Pointer(ap)));
end;
{
* Increasing refcount of lower vnode is needed at least for the case
* when lower FS is NFS to do sillyrename if the file is in use.
* Unfortunately v_usecount is incremented in many places in
* the kernel and, as such, there may be races that result in
* the NFS client doing an extraneous silly rename, but that seems
* preferable to not doing a silly rename when it is needed.
}
function null_remove(ap:p_vop_remove_args):Integer;
var
retval,vreleit:Integer;
lvp,vp:p_vnode;
tnn:p_null_node;
begin
vp:=ap^.a_vp;
if (vrefcnt(vp) > 1) then
begin
lvp:=NULLVPTOLOWERVP(vp);
if (lvp<>nil) then
begin
VREF(lvp);
end;
vreleit:=1;
end else
vreleit:=0;
if (lvp<>nil) then
begin
tnn:=VTONULL(vp);
tnn^.null_flags:=tnn^.null_flags or NULLV_DROP;
retval:=null_bypass(Pointer(ap));
if (vreleit<>0) then
begin
vrele(lvp);
end;
end else
begin
retval:=0;
end;
Exit(retval);
end;
{
* We handle this to eliminate null FS to lower FS
* file moving. Don't know why we don't allow this,
* possibly we should.
}
function null_rename(ap:p_vop_rename_args):Integer;
label
_cross;
var
tdvp,fvp,fdvp,tvp:p_vnode;
tnn:p_null_node;
begin
tdvp:=ap^.a_tdvp;
fvp:=ap^.a_fvp;
fdvp:=ap^.a_fdvp;
tvp:=ap^.a_tvp;
if (tvp<>nil) then
begin
if (fvp^.v_mount<>tvp^.v_mount) then goto _cross;
end;
{ Check for cross-device rename. }
if (fvp^.v_mount<>tdvp^.v_mount) then
begin
_cross:
if (tdvp=tvp) then
vrele(tdvp)
else
vput(tdvp);
if (tvp<>nil) then
begin
vput(tvp);
end;
vrele(fdvp);
vrele(fvp);
Exit(EXDEV);
end;
if (tvp<>nil) then
begin
tnn:=VTONULL(tvp);
tnn^.null_flags:=tnn^.null_flags or NULLV_DROP;
end;
if (tnn^.null_lowervp=nil) then Exit(0);
Exit(null_bypass(Pointer(ap)));
end;
function null_rmdir(ap:p_vop_rmdir_args):Integer;
var
tnn:p_null_node;
begin
tnn:=VTONULL(ap^.a_vp);
if (tnn^.null_lowervp=nil) then Exit(0);
tnn^.null_flags:=tnn^.null_flags or NULLV_DROP;
Exit(null_bypass(Pointer(ap)));
end;
{
* We need to process our own vnode lock and then clear the
* interlock flag as it applies only to our vnode, not the
* vnodes below us on the stack.
}
function null_lock(ap:p_vop_lock1_args):Integer;
var
vp:p_vnode;
flags:Integer;
nn:p_null_node;
lvp:p_vnode;
error:Integer;
begin
vp:=ap^.a_vp;
flags:=ap^.a_flags;
if ((flags and LK_INTERLOCK)=0) then
begin
VI_LOCK(vp);
flags:=flags or LK_INTERLOCK;
ap^.a_flags:=flags;
end;
nn:=VTONULL(vp);
{
* If we're still active we must ask the lower layer to
* lock as ffs has special lock considerations in it's
* vop lock.
}
if (nn<>nil) then
begin
lvp:=NULLVPTOLOWERVP(vp);
if (lvp<>nil) then
begin
VI_LOCK(lvp);
VI_UNLOCK(vp);
{
* We have to hold the vnode here to solve a potential
* reclaim race. If we're forcibly vgone'd while we
* still have refs, a thread could be sleeping inside
* the lowervp's vop_lock routine. When we vgone we will
* drop our last ref to the lowervp, which would allow it
* to be reclaimed. The lowervp could then be recycled,
* in which case it is not legal to be sleeping in it's VOP.
* We prevent it from being recycled by holding the vnode
* here.
}
vholdl(lvp);
error:=VOP_LOCK(lvp,flags,{$INCLUDE %FILE%},{$INCLUDE %LINENUM%});
{
* We might have slept to get the lock and someone might have
* clean our vnode already, switching vnode lock from one in
* lowervp to v_lock in our own vnode structure. Handle this
* case by reacquiring correct lock in requested mode.
}
if (VTONULL(vp)=nil) and (error=0) then
begin
ap^.a_flags:=ap^.a_flags and (not (LK_TYPE_MASK or LK_INTERLOCK));
case (flags and LK_TYPE_MASK) of
LK_SHARED:
begin
ap^.a_flags:=ap^.a_flags or LK_SHARED;
end;
LK_UPGRADE,
LK_EXCLUSIVE:
begin
ap^.a_flags:=ap^.a_flags or LK_EXCLUSIVE;
end;
else
Assert(False,'Unsupported lock request');
end;
VOP_UNLOCK(lvp, 0);
error:=vop_stdlock(ap);
end;
vdrop(lvp);
end else
error:=vop_stdlock(ap);
end else
error:=vop_stdlock(ap);
Exit(error);
end;
{
* We need to process our own vnode unlock and then clear the
* interlock flag as it applies only to our vnode, not the
* vnodes below us on the stack.
}
function null_unlock(ap:p_vop_unlock_args):Integer;
label
_else;
var
vp:p_vnode;
flags:Integer;
mtxlkflag:Integer;
nn:p_null_node;
lvp:p_vnode;
error:Integer;
begin
vp:=ap^.a_vp;
flags:=ap^.a_flags;
mtxlkflag:=0;
if ((flags and LK_INTERLOCK)<>0) then
mtxlkflag:=1
else
if (not mtx_owned(VI_MTX(vp)^)) then
begin
VI_LOCK(vp);
mtxlkflag:=2;
end;
nn:=VTONULL(vp);
if (nn<>nil) then
begin
lvp:=NULLVPTOLOWERVP(vp);
if (lvp=nil) then goto _else;
VI_LOCK(lvp);
flags:=flags or LK_INTERLOCK;
vholdl(lvp);
VI_UNLOCK(vp);
error:=VOP_UNLOCK(lvp, flags);
vdrop(lvp);
if (mtxlkflag=0) then
begin
VI_LOCK(vp);
end;
end else
begin
_else:
if (mtxlkflag=2) then
begin
VI_UNLOCK(vp);
end;
error:=vop_stdunlock(ap);
end;
Exit(error);
end;
{
* Do not allow the VOP_INACTIVE to be passed to the lower layer,
* since the reference count on the lower vnode is not related to
* ours.
}
function null_inactive(ap:p_vop_inactive_args):Integer;
var
vp,lvp:p_vnode;
xp:p_null_node;
mp:p_mount;
xmp:p_null_mount;
begin
vp:=ap^.a_vp;
xp:=VTONULL(vp);
lvp:=NULLVPTOLOWERVP(vp);
if (lvp=nil) then Exit(0);
mp:=vp^.v_mount;
xmp:=MOUNTTONULLMOUNT(mp);
if ((xmp^.nullm_flags and NULLM_CACHE)=0) or
((xp^.null_flags and NULLV_DROP)<>0) or
((lvp^.v_vflag and VV_NOSYNC)<>0) then
begin
{
* If this is the last reference and caching of the
* nullfs vnodes is not enabled, or the lower vnode is
* deleted, then free up the vnode so as not to tie up
* the lower vnodes.
}
vp^.v_object:=nil;
vrecycle(vp);
end;
Exit(0);
end;
{
* Now, the nullfs vnode and, due to the sharing lock, the lower
* vnode, are exclusively locked, and we shall destroy the null vnode.
}
function null_reclaim(ap:p_vop_reclaim_args):Integer;
var
vp:p_vnode;
xp:p_null_node;
lowervp:p_vnode;
begin
vp:=ap^.a_vp;
xp:=VTONULL(vp);
if (xp=nil) then Exit(EINVAL);
lowervp:=xp^.null_lowervp;
if (lowervp=nil) then Exit(EINVAL);
Assert((vp^.v_vnlock<>@vp^.v_lock),'Reclaiming incomplete null vnode');
null_hashrem(xp);
{
* Use the interlock to protect the clearing of v_data to
* prevent faults in null_lock().
}
lockmgr(@vp^.v_lock, LK_EXCLUSIVE, nil);
VI_LOCK(vp);
vp^.v_data:=nil;
vp^.v_object:=nil;
vp^.v_vnlock:=@vp^.v_lock;
VI_UNLOCK(vp);
if (lowervp<>nil) then
begin
if ((xp^.null_flags and NULLV_NOUNLOCK)<>0) then
vunref(lowervp)
else
vput(lowervp);
end;
FreeMem(xp);
Exit(0);
end;
function null_print(ap:p_vop_print_args):Integer;
var
vp:p_vnode;
begin
vp:=ap^.a_vp;
Writeln(Format('vp=%p, lowervp=%p', [vp,VTONULL(vp)^.null_lowervp]));
Exit(0);
end;
{ ARGSUSED }
function null_getwritemount(ap:p_vop_getwritemount_args):Integer;
label
_else;
var
xp:p_null_node;
lowervp:p_vnode;
vp:p_vnode;
begin
vp:=ap^.a_vp;
VI_LOCK(vp);
xp:=VTONULL(vp);
if (xp<>nil) then
begin
lowervp:=xp^.null_lowervp;
if (lowervp=nil) then goto _else;
VI_LOCK(lowervp);
VI_UNLOCK(vp);
vholdl(lowervp);
VI_UNLOCK(lowervp);
VOP_GETWRITEMOUNT(lowervp, ap^.a_mpp);
vdrop(lowervp);
end else
begin
_else:
VI_UNLOCK(vp);
ap^.a_mpp^:=nil;
end;
Exit(0);
end;
function null_vptofh(ap:p_vop_vptofh_args):Integer;
var
lvp:p_vnode;
begin
lvp:=NULLVPTOLOWERVP(ap^.a_vp);
if (lvp=nil) then Exit(EOPNOTSUPP);
Exit(VOP_VPTOFH(lvp, ap^.a_fhp));
end;
function null_vptocnp(ap:p_vop_vptocnp_args):Integer;
var
vp:p_vnode;
dvp:pp_vnode;
lvp,ldvp:p_vnode;
error,locked:Integer;
begin
vp:=ap^.a_vp;
dvp:=ap^.a_vpp;
if (vp^.v_type=VDIR) then
Exit(vop_stdvptocnp(ap));
locked:=VOP_ISLOCKED(vp);
lvp:=NULLVPTOLOWERVP(vp);
if (lvp=nil) then
begin
ap^.a_buf[0]:='/';
ap^.a_buf[1]:=#0;
ap^.a_buflen^:=1;
Exit(0);
end;
vhold(lvp);
VOP_UNLOCK(vp, 0); { vp is held by vn_vptocnp_locked that called us }
ldvp:=lvp;
vref(lvp);
error:=vn_vptocnp(@ldvp, ap^.a_buf, PDWORD(ap^.a_buflen));
vdrop(lvp);
if (error<>0) then
begin
vn_lock(vp, locked or LK_RETRY,{$INCLUDE %FILE%},{$INCLUDE %LINENUM%});
Exit(ENOENT);
end;
{
* Exclusive lock is required by insmntque1 call in
* null_nodeget()
}
error:=vn_lock(ldvp, LK_EXCLUSIVE,{$INCLUDE %FILE%},{$INCLUDE %LINENUM%});
if (error<>0) then
begin
vrele(ldvp);
vn_lock(vp, locked or LK_RETRY,{$INCLUDE %FILE%},{$INCLUDE %LINENUM%});
Exit(ENOENT);
end;
vref(ldvp);
error:=null_nodeget(vp^.v_mount, ldvp, dvp);
if (error=0) then
begin
VOP_UNLOCK(dvp^, 0); { keep reference on *dvp }
end;
vn_lock(vp, locked or LK_RETRY,{$INCLUDE %FILE%},{$INCLUDE %LINENUM%});
Exit(error);
end;
function null_link(ap:p_vop_link_args):Integer;
begin
if (ap^.a_tdvp^.v_mount<>ap^.a_vp^.v_mount) then
Exit(EXDEV);
if (NULLVPTOLOWERVP(ap^.a_vp)=nil) then Exit(0);
Exit(null_bypass(Pointer(ap)));
end;
end.