| unique failing test | history |
|---|---|
| conf-sanity4@zfs:test_302 | NEW unique failure for this branch in the last 30 days, and was seen 0 times across 0 other branches 0 reviews |
LU-20637 llapi: name a device scan's objects
A scan of a target holds names and parent FIDs but no paths, so lfind
prints FIDs and a user has no way to turn them into pathnames. The
HLD names an Output Format for exactly this, beside the FID printing
that is already there.
Add llapi_scan_rec_path(), which uses the path a namespace walk
already put in the record and resolves the FID through a client mount
when there is none. lfind gets --fid2path MOUNT for it.
The cost is one ioctl per object, so it is the consumer's to ask for
rather than the scanner's to pay: it falls on the objects a search
kept, not on every object it saw. That is also why the HLD prices
pathname generation separately from the scan rate.
An object with no pathname is counted and reported at the end rather
than printed: the objects a target keeps for itself have none, their
FIDs being below FID_SEQ_NORMAL, so a search under --internal is
expected to report some. An OST data object does have a pathname --
its FID resolves through the OSC to the file that owns it, which
sanity test_154C asserts -- so a scan of an OST names that file once
per stripe it has on the target. A hardlinked object is printed once,
under its first name. A FID that cannot be looked up at all -- no
CAP_DAC_READ_SEARCH, which ll_fid2path() checks first -- is an error
and not a missing name, so a broken search cannot read as a complete
one.
Neither half of the resolution asks anything of the target being
scanned, which is what lets it name objects on a target that is out of
service -- and a ZFS target can only be scanned out of service, its
pool being exported.
An OST data object is named by the file that owns it, read from the
object's own trusted.fid. Its own FID is an OST-sequence one, which
__ll_fid2path() answers only by asking the OST holding it, through the
OSC: exactly the target the scan is reading. ff_parent is the owner's
MDT FID, and the stripe index filter_fid keeps in its f_ver is
dropped,
that field being an index and not a version. An object no file owns
yet -- precreated and never written, of which a target holds many --
carries no trusted.fid and is reported as having no name rather than
being chased through the OSC.
trusted.fid is not the only place that FID lives. An OST whose inodes
are 256 bytes or smaller has none at all: osd_xattr_set_pfid() packs
the parent into the LMA as a struct lustre_ost_attrs and removes the
separate xattr, saying so with LMAC_STRIPE_INFO. Reading only
trusted.fid there leaves every object on the target without an owner,
so --fid2path prints nothing and reports them all unnamed -- measured
on an OST formatted -I 256: 33 objects, no owner on any of them,
against four files whose paths it should have printed. mkfs.lustre
defaults to -I 512 now, but -I 256 is still accepted and older OSTs
carry it. The LMA buffer is already in hand for the classification,
so the fallback costs no second xattr, and ll_decode_filter_fid(8)
reads the same two places.
An MDT object is named from its trusted.link and a map of the target's
directories, FID to parent and name, built by a pass over the target
before the search. A pass of its own because objects arrive in inode
order, so an object is usually delivered before its ancestors are;
resolving inline would mean holding every match until the scan ended.
The pass asks for no layout, no SOM and no HSM, and a filesystem holds
far fewer directories than files. On an OST it stops at the first
object and leaves the map empty.
There is no fall back to the lookup once the map is in use. The map
is
used precisely when the target cannot answer, so one object it could
not place would stop the whole search there. Two things are nameless
in consequence and counted with the rest: an ownerless OST object, and
on DNE an object whose ancestors live on another MDT, the map covering
only the target scanned.
--paths is the same composition with no mount at all. --fid2path
still needs one, not to resolve anything but as the filesystem the
paths hang from and for the comparison below, and a filesystem whose
only MDT is the target being scanned has none to give -- which for a
single-MDT ZFS filesystem is every scan of its MDT, the pool having
to be exported. --paths prints filesystem-root-relative names
instead, asks nothing of any running service, and is refused on an
OST, whose objects are named by files that live on an MDT.
A mount whose own filesystem cannot be read is refused rather than
run unguarded: --fid2path was asked for explicitly, and the fallback
was the very outcome the comparison exists to prevent, another
filesystem's pathnames printed with a zero exit.
llapi_scan_rec_path.3 says the same thing to a consumer pairing that
call with llapi_scan_device() directly, because nothing in the
library can make the check for them: the call is handed a descriptor
and a record and never sees the target the record came from. lfind
compares the mount's fsname with the target's label; anyone else has
to do that too, or know by construction that the two agree.
A MOUNT of another filesystem is refused when the target is opened,
its fsname compared with the one the target's label names. The
comparison sits inside the scan's own open rather than in a probe
ahead of it: a second open is not free on every backend, and opening
a target twice buys nothing the one open cannot answer. It cannot be
caught later: FID sequences are not unique across filesystems.
FID_SEQ_ROOT and FID_SEQ_DOT_LUSTRE are constants, so the root of
every filesystem has the same FID, and normal sequences overlap
because each filesystem's controller starts from FID_SEQ_NORMAL
(LUSTRE_SEQ_SPACE_RANGE). fld_client_lookup() on the wrong mount
therefore finds a range and __ll_fid2path() answers with a pathname
from it, so the run would have printed another filesystem's paths as
though they were the target's and exited 0.
The mount point is resolved before use: a bare fsname, a
subdirectory and a trailing slash all open the same root, and none of
them names a file when prefixed to what fid2path answers.
conf-sanity test_302 stops an OST, exports its pool where the backend
is
ZFS, and compares the names a scan of it gives against what the client
saw before it went down -- a scan that resolved nothing and one that
resolved everything both exit 0, so the set is what is asserted. Its
files are written rather than merely created, a precreated object
being
rightly nameless.
conf-sanity test_301 diffs the resolved paths against what the client
sees, and checks that a hardlinked object is named once. It mounts a
client on the MDS for the duration, because --fid2path resolves
through a mount on the node running the scan and a server is not a
client; a configuration that cannot mount one skips. It is ldiskfs
only: reading a target in service is what ZFS refuses.
scan_device_run()'s comment says what a second open costs rather than
calling it a deadlock, because it is not one: llapi_find_device()
builds the directory map in a pass of its own, so every --fid2path and
--paths
run already opens and closes the target twice in one process, and
conf-sanity test_302 does exactly that against a stopped OST with its
pool exported and passes on conf-sanity4@zfs. The reason to test the
fsname inside the scan's own open is the cost of the second import and
export, not a hang. The wasted pass on an OST -- where
scan_dirmap_cb() stops at the first object -- is left as it is;
folding the map into the search's own open would serve both from one
open and is worth doing separately.
The resolve-failure message names the FID that was looked up and the
mount only when there is one. An OST object is resolved by its
owner's FID rather than its own, so the message named the wrong one;
and under --paths there is no mount at all, yet -ERANGE and -ELOOP
still arrive here and reported a failure "through the mount given".
lfind(8) says what --fid2path costs, which differs by target and is
easy to state as one number and be wrong. One lookup per kept object
is the OST's cost; on an MDT there is no lookup and a whole extra pass
instead, to build the map the page describes four blocks further down
-- the more expensive of the two, and paid before the search rather
than per match. Where a name cannot be composed the page points at
lfs-fid2path(1), which is a tool that exists, rather than at a
lookup-only mode of this one, which is not.
--paths and --fid2path together goes through the same exit as every
other error in that function. Returning CMD_HELP from main() exits 4
and prints no usage, where "ret = CMD_HELP; goto out" prints it and
exits EXIT_FAILURE.
fp_paths takes fp_unused_bit1. struct find_param keeps those bits for
exactly this, and a trailing byte would have cost 8 with padding.
The directory map's names stay inline. At NAME_MAX + 1 an entry is
~288 bytes, so a million directories is ~288 MB and 1.5x that while
scan_dirmap_grow() holds both tables, and interning them in an arena
would cut it several-fold. Left for now: it is a change to the map's
memory layout for a case no measurement here has reached, and a
calloc() failure already fails the search cleanly rather than
corrupting it. Worth revisiting when --paths is pointed at a target
big enough to need it.
conf-sanity test_303 covers --paths, where 301 and
302 both exercise only --fid2path, so the composition --paths exists
for was never run. It scans an MDT in service with no mount given,
which is the case a filesystem whose only MDT is the target has to be
named in, and asserts the answer is the client's set: one path per
object rather than one per name, the hardlinked object once, no FID
where a path was asked for, and no mount point in the answer -- root-
relative being what separates it from --fid2path, and a mounted prefix
meaning the wrong composer ran. It also asserts the two refusals:
--paths on an OST, whose objects are named by files that live on an
MDT, and --paths together with --fid2path. ldiskfs only, an imported
ZFS pool answering EBUSY by design.
lfsp_fsname makes the check a public caller can ask for.
llapi_scan_rec_path.3 tells one they must establish that the mount and
the target are the same filesystem, and nothing in
<lustre/lustreapi.h> let them: scan_device_run() is internal,
llapi_scan_device() passed want_fsname NULL, and the target's label
lives in a private header. The field is the same machinery under a
public name -- the scan refuses a target whose label names another
filesystem with -EXDEV -- and it has to be the scan that does it,
because only the scan reads the label and a lookup on the wrong mount
succeeds and answers with that filesystem's pathname.
The usage agrees with its own option list. "The output is a
FID because a target has no paths" sat two lines below --paths and
--fid2path, both of which print pathnames; it now says the output is a
FID unless one of them is given.
test_301 and test_303 sync the MDS, not only the client.
lustre_super_operations has no .sync_fs, so a client sync writes back
page-cache data and leaves the MDT's ldiskfs to commit on its own
schedule -- and both tests scan that ldiskfs directly while it is in
service, so an object still only in memory is one the scan cannot see
and the comparison against the client's answer is a race. test_302
needs none of this: its target is stopped, which flushes it.
test_302 also tells the scan where the OST's pool devices are. An
exported pool is found by reading vdev labels under a search path,
which defaults to /dev, and the test framework's ZFS vdevs are files
in $TMP -- so without --search the pool is not found at all and the
test failed on every ZFS configuration with ENOENT. On ldiskfs the
argument is empty and nothing changes.
test_301's one bare lfs becomes $LFS, as every other call in the file
is. An interop run that points LFS at another binary would have used
whatever PATH found.
The EXAMPLES fragment in llapi_scan_rec_path.3 escapes its newline as
n, not n: troff reads the single form as a register reference,
which rendered the line as printf("%s0, path). llapi_fid_parse.3
escapes it the same way.
The three places that said a target scan needs no client now say what
--fid2path changes. lfind.c's file header and llapi_find_device()'s
block comment both stated it flatly, and llapi_find_device.3
documented neither fp_fid2path_mnt nor the -ENAMETOOLONG an over-long
mount path answers with; its SEE ALSO gains llapi_scan_rec_path(3),
which is the call that does the resolving.
llapi_scan_rec_path.3's NOTES point a consumer at lfsp_fsname rather
than telling it to compare the mount and the target itself.
lfsp_fsname is that comparison -- the scan refuses a target of another
filesystem with -EXDEV -- and saying otherwise on the page that
introduces it would send a caller to write what the library already
does.
test_303's one bare lfs becomes $LFS, as test_301's did. It was added
after that fix and reintroduced it.
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools
Change-Id: I6476abfd28c91b2ccc04fb08c4e5860aef8fe009
| unique failing test | history |
|---|---|
| sanity2@ldiskfs+DNE:test_157d | seen in 1 other review |
| sanity2@zfs:test_157d | NEW unique failure for this branch in the last 30 days, and was seen 0 times across 0 other branches 0 reviews |
LU-20649 llapi: a changelog as an Object Stream The two scanners we have enumerate what exists. A changelog enumerates what happened, and llapi_scan_changelog() delivers it in the same record, so a consumer written against llapi_scan_namespace() or llapi_scan_device() can be fed by a delta without being rewritten. A changelog record carries the FID, the parent FID, the name, the event and its time, and -- where the server recorded them -- the uid and gid of whoever caused it. That pair is lfsr_event_uid and lfsr_event_gid, not lfsr_uid: mdd_changelog_ns_store() fills the uidgid extension from lu_ucred(env), the acting client's credentials, so it says who acted and not who owns the object, which is what lfsr_uid means in every scanner that fills it and what lfs find -uid matches. Delivering it as lfsr_uid would have answered "-uid alice" from the identity of whoever last touched her file. The record carries no size, mode, owner, times, layout or HSM state at any setting, so those arrive absent with their lfsr_valid bit clear. With LLAPI_SCAN_CL_F_RESOLVE they are filled by opening the object through a client mount, which is one open and one stat each, or two of each for a regular file whose size was asked for; the demand mask decides whether that happens at all, so a consumer reading only what an event carries pays nothing. Resolution fills what the stream lacks and never restates what it has, which is what lets a caller ask what an event recorded rather than what is true now. Two modes. Event mode delivers one record per event, in order. Object mode holds events in a FID-keyed cache and delivers one record per object once it has been quiet, so a file written a hundred times is one record and one lookup rather than a hundred of each. Clearing is off by default and never runs ahead of the callback: it is irreversible and it purges the registered user's whole backlog, not this reader's alone. Registering that user stays the caller's, since registering without consuming is how an MDT fills up. The record gains lfsr_event_type, _flags, _time, _index and _prev, the rename source, and a job id, behind three new validity bits. An event time is not an object time, so it does not land in lfsr_mtime: a search asking for one must not be handed the other. Only an event that implies a type answers for it -- the three creations, and CL_RMDIR, whose target is a directory by definition -- so the mode and its validity bit are set together, and a later event leaves both alone. Setting them as a pair is what keeps "cannot answer" apart from "the answer is zero": a mode written on every event, with the bit only ever OR'd in, would leave a coalesced object claiming a type while carrying zero for it. An object's type does not change, so the first answer stands for every event after it. lfsr_parent_fd and lfsr_fd are set to -1 rather than left as the memset found them. Zero is stdin, and a consumer written against llapi_scan_namespace() is told to test lfsr_parent_fd and then fstatat() on it. The Changelog reader is finished on every path out, the error one included. llapi_changelog_start_user() opens the device and sets *priv before it issues OBD_IOC_CHANGELOG_FILTER, so a failure there -- an unregistered user, an unreachable MDT -- hands back an error with the reader live, and the fd, its buffer and the kernel-side reader state would stay pinned for the life of the process if nothing closed them. sc_user, sc_mnt and sc_stats are read from the bounded copy rather than the caller's structure, and the advertised minimum sc_size reaches through sc_mdtname. A minimum stopping short of it would let a caller declare a structure without the field the call cannot work without, and reading that field off the caller's own memory would go past its end before the size check had a chance to refuse it. Clearing stops at the oldest record any cached object still needs, not at the last index delivered. Coalescing puts those apart: create B at 10, create A at 11, touch B at 1000, and A can age out and be accepted while B is still held with a record at 10. Clearing to 11 there and then stopping the scan loses B entirely -- no callback saw it, and a restart from sc_startrec cannot read it back, because the record is gone. An event with no target object is delivered per event even in object mode. mdd_changelog_ns_store() zeroes cr_tfid when its target is NULL, which mdd_rename() passes whenever the destination name did not already exist, and cr_markerflags -- a CL_MARK's union with the same bytes -- is not a FID either. Coalescing keys on the FID, so all of them shared one cache entry and only the last survived: an ordinary `mv a b` was enough to lose the one before it, and with clearing on, the record it came from is gone as well. The parent FID is taken only from an event that carries one. CL_CLOSE does not -- mdd_changelog_data_store() is called with pfid NULL -- so assigning it from every event while only ever OR-ing the bit in left create+write+close reporting LLAPI_SCAN_PARENT over a zero. A name or job id whose copy failed leaves its bit clear for the same reason: a bit that promises a string must not be set over a NULL. A resolve is skipped only for a demand mask every event can answer, which LLAPI_SCAN_TYPE is not: scan_cl_mode() answers it for the four record types that imply one and no others, so counting it there would leave a consumer asking for FID and type with neither a lookup nor a type on a CL_CLOSE. Resolution opens O_PATH and takes a real open only for the regular file whose size it came for. llite gives a special file init_special_inode(), so opening every object for read opened the driver behind a device node stored in the filesystem; O_NONBLOCK holds off the wait, not the rest of what an open does. The uidgid extension is asked for when sc_want is 0, and that case is tested on its own rather than by a mask. Zero means "everything", so it is the caller that most needs the extension -- and `0 & ~LLAPI_SCAN_EVENT` is 0, so any test of that shape would exclude precisely the caller that wanted it, leaving LLAPI_SCAN_EVENT_UID out of lfsr_valid. A size is reported strict only where the OSTs were actually glimpsed. The second open runs for a regular file whose demand mask names a size and can still fail, so a consumer asking for an mtime alone reaches the fill on the O_PATH descriptor; that is the MDT's lazy answer, and LLAPI_SCAN_LAZY_SIZE and LLAPI_SCAN_LAZY_BLOCKS are what say so. liblustreapi_pfind.c maps the two to OBD_MD_FLSIZE and OBD_MD_FLLAZYSIZE, so they are not interchangeable. LLAPI_SCAN_CL_F_RESOLVE is one open and one fstat, so the page says which six of the ten fields it fills. The project id, the layout, the directory stripe and the HSM state each cost an ioctl or an xattr of their own and are not filled at any setting; the page said otherwise by listing all ten and then naming the flag. The checkpoint gets its caveat, in the header and the page both. Under coalescing a record naming no object goes out as it arrives while an object whose earliest record sits lower is still held, so the delivered indexes are not a watermark. A run that reaches the end of the log flushes what it holds and the highest index is safe; a run the consumer stopped drops it, so that run must leave the checkpoint alone. sc_startrec and sc_endrec are both inclusive and the page now says so, the example resuming from the saved index plus one rather than re-delivering the record the job already processed. lfsr_event_flags is cr_flags & CLF_FLAGMASK, so CLF_RENAME, CLF_JOBID and CLF_EXTRA_FLAGS are masked off -- they say which extensions a record carries, not what happened. Three smaller things. The second fstat() is taken only when the glimpse replaced the descriptor. scan_cl_bucket() drops a modulo that cannot change llapi_fid_hash()'s value, that being hash_long() on the same shift. The sc_mdtname test after the memcpy goes: the minimum sc_size reaches through that field and the caller's own struct was tested at the top, so it could never fire. Four comments described an earlier revision of this patch rather than the code and now describe the code. The COALESCE flag says which event's fields the one record carries. It promised one record per object and left open which of the object's events that record spoke for, so a consumer matching on lfsr_name had no way to know it sees one name per object rather than every name the log recorded -- an object created under one name and hardlinked under another arrives carrying only the last of them. lfs find --changelog -name is that consumer, and the gap showed there first. sc_type_mask without sc_user is refused. The mask reaches the server only through llapi_changelog_start_user(), which needs a registered user to filter for; on the other branch llapi_changelog_start() never sees it, so a caller asking for CL_UNLINK alone read every event type and nothing said so. Refused rather than applied client-side, because that is a different thing -- the records still cross the wire -- and sc_filter is already the client-side spelling. The man page said only that the mask is intersected with the user's own, which does not tell a caller what happens when there is no user. llapi_scan_changelog_test covers the five things the shape of this stream turns on: that a record describes the event it came from, that object mode collapses a burst into one record, that the demand mask keeps a lookup from happening at all, that a consumer's own return value comes back unchanged, and that bad parameters are refused rather than crashed on. It takes -m for the MDT, -u for a registered user and -d for a client mount, since three of those need a live log. Nothing in lustre/tests drives it yet, unlike its two siblings, which land their suite case with the scanner: that case wants a changelog registered and cleared around it and a skip where the MDT records nothing, and is left for its own patch rather than grown here. sanity 157d runs it, so the binary is not merely built. Its events have to be on the MDT whose log it reads, which under DNE a mkdir below a striped root does not guarantee, so the test makes the directory on MDT0 itself rather than leaving the binary to notice and refuse. -u passes the registered Changelog user the clearing cases need; without one they skip themselves. Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com> Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools Change-Id: I12efd3bd2150592a4507533129ab56573ef924d9
LU-20650 llapi: fill a scan record for one FID llapi_scan_namespace() walks what exists, llapi_scan_device() reads what exists on a target, and llapi_scan_changelog() reads what happened. A consumer holding a FID had no way in, so the changelog module resolved privately and with fstat() alone: mode, nlink, uid, gid, three times, size and blocks, and nothing else. A record filled that way answers -size and -mtime but not -stripe-count, --hsm-state or -projid, so "lfs find --since -stripe-count 4" would answer differently from a plain find on the same filesystem. That is a defect rather than a difference of provenance: --since promises what is true now, verified through the mount. This patch adds the call; the search moves onto it in "lfs: find --changelog, the log as source", which is where that example starts answering. The default demand mask is the walk's, so --hsm-state and -projid still want naming in lfsp_want; what the call fixes without being asked is the layout, which is what the example above turns on. llapi_scan_fid() is the fourth way into the record and fills it the way the walk does, because it is the walk's own gather. It resolves the FID to a pathname and gathers there rather than gathering from a descriptor opened by FID: scan_rec_gather() is path-oriented throughout -- get_lmd_info_fd() wants a parent descriptor and a name, get_projid() opens by path, and the HSM and MDT-index branches open the path themselves -- so the alternative is an fd-shaped twin of every one of those, which is the second copy of the knowledge LU-20611 existed to remove. Resolution is not an added cost on the caller that wants this: --since prints pathnames and prefix-matches them for its subtree restriction, so it needs the pathname anyway. What that costs is stated rather than buried: per object one fid2path, one type-only statx and one open of the object or its parent, and CAP_DAC_READ_SEARCH unless llite.*.user_fid2path is set. The statx asks for STATX_TYPE alone deliberately: anything touching size, blocks or mtime keeps need_glimpse set in ll_getattr_dentry() and buys an OST glimpse per regular file that the gather then discards. An object unlinked since the caller learned of it no longer resolves and comes back -ENOENT, which for a consumer asking what is true now is the answer and not an error. The call fills a fresh record and never reads one in. A consumer merging it into a record it already holds decides which fields it lets this one supply, which is what will keep --changelog --resolve filling only what the stream lacks. A filter that returns a positive value skips the object, as it does for the other three scanners, and the call returns 0 rather than handing that value back to a caller testing `if (rc)`. The gather runs against a pathname, so the FID it found is checked against the one asked for and a name unlinked and recreated underneath it answers -ESTALE rather than a record for a different object. Only lfsp_want and lfsp_filter are read from the parameter block. lfsp_max_depth, lfsp_stats, lfsp_search and lfsp_fsname describe a traversal there is none of and are ignored; lfsp_thread_count above 1 is refused rather than ignored, because a caller setting it has misunderstood the call. It widens a lfsp_want of 0 through LLAPI_SCAN_WANT_DEFAULT, the definition llapi_scan_namespace() already uses, so the two mount-side entry points mean the same thing by 0. A trailing slash on mnt_path is trimmed before the pathname is composed. The leading slashes of fid2path's answer were already stripped, but "/mnt/lustre/" with the root FID composed "/mnt/lustre/" and left lfsr_name on the terminating NUL -- which lustreapi.h says means NULL, not empty -- and every other FID got a doubled separator in the published lfsr_path. The kernel-doc no longer lists lfsp_flags among the fields that are ignored. Every flag it defines is, but a bit outside them is refused with -EINVAL, as the code does and llapi_scan_fid.3 already says. llapi_find_device() clears lfsp_got. scan_param_copyin() copies the caller's block whole, so the caller's pointer came through and was answered with find's own demand mask -- a mask for a question the caller never asked. The field's kernel-doc is rewritten with it, to say that it is per scan rather than per library and why it is reached through a pointer. The syscalls go through mnt_fd, not through the composed pathname. The FID was already resolved through that descriptor; the statx, the object's open and the parent's now use it too, at the name fid2path answered, so mnt_fd alone says which filesystem is read. An fd pins its mount where a path string is resolved afresh every time, and a mount replaced between the open and the scan would otherwise have the resolve read one filesystem and the gather another, with rc 0 and nothing said. It also takes the mount prefix off every lookup, three per object, which over a changelog's worth of FIDs is what this entry point is for. mnt_fd has to be the mount root now. The ioctl behind llapi_fid2path_at() takes any descriptor in the filesystem, and the page always named llapi_root_path_open(), which returns the root; a caller that passed something else answered before and answers -ENOENT now. mnt_path stays required and stays what lfsr_path is built from, so a wrong one still mislabels the answer -- it just no longer decides what was read. llapi_scan_test gains a case for it, so sanity 157c runs it: one record for a known FID, checked against the pathname and the basename the client sees; a filter that skips, which is a success with nothing delivered; the argument refusals, including the -EBADF that a negative mnt_fd has to answer rather than the -EINVAL that also means "this FID has no name"; and a FID that resolves to nothing, which must fail and deliver no record whichever way it fails. Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com> Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools Change-Id: I796db84d3fe596b1cfa93658dcfeb240db8e33a5
(minor) This paragraph lists what the record grew, but two other pieces of new public API aren't accounted for anywhere in the body: `struct llapi_scan_stats` with the `sp_stats` parameter field, and the `LLAPI_SCAN_F_INTERNAL` flag. Both are things a caller has to know about, and the counters in particular are described in the man page as part of the answer rather than a diagnostic. Worth a sentence each so the diff holds no surprises.
Done. Held back through several rounds as prose-only, but you are right that new public API is not prose. The message now names `struct llapi_scan_stats` with `sp_stats` and says what LLAPI_SCAN_F_INTERNAL is for: the objects a target holds that its namespace never shows -- the OSD's own, the DNE agent inodes, the ones with no LMA -- which a scan otherwise counts in ss_class and keeps back. Checked against `enum llapi_scan_class` and the gate in liblustreapi_scan_device.c rather than written from the flag's name.
API changes are not critical within the development releases.
LU-20606 llapi: scan an ldiskfs target directly
Add llapi_scan_device(), which reads an MDT's or OST's objects
straight off the device with libext2fs and delivers the same records
llapi_scan_namespace() does. No mount, no MDS, no kernel change: it
runs against a stopped target, a snapshot or a failover partner's LUN,
read-only and with no write path.
libext2fs stays off liblustreapi. The backend builds as
scan_osd_ldiskfs.so and is dlopen'ed on first use, named and found the
way mount.lustre's own mount_osd_ldiskfs.so is, so a client build has
no plugin and the call answers ENOTSUP. Three refusals get three
answers, because a caller keys off them: ENOTSUP where there is no
backend to ask, ENOPKG where the backend is there and refused the
target -- stock e2fsprogs will not open a filesystem with dirdata set,
and the package to install is the answer -- and EINVAL for a device
that is not a Lustre target at all. Giving the first two the same
errno would have conf-sanity test_300 skip itself on a node carrying a
real MDT and a build that can scan it.
The record grows what a device answers for and a walk does not: the
object id, the inode generation, the LMA flags, the raw linkea and a
class. The label is read the four ways mkfs.lustre writes it -- '-',
':', '=' and the '+' that tunefs.lustre --nolocallogs leaves on disk
while the target is stopped, which is exactly when this is what there
is to read it with.
Two more pieces of public API, so the diff holds no surprises. struct
llapi_scan_stats is what the new lfsp_stats points at: the caller sets
ss_size and owns the storage, the scan fills the rest on return, and
the man page describes it as part of the answer rather than as a
diagnostic. And LLAPI_SCAN_F_INTERNAL asks for the objects a target
holds that its namespace never shows -- the OSD's own, the DNE agent
inodes, the ones with no LMA -- which a scan otherwise counts in
ss_class and keeps back. Without it "every object on this device" and
"every object a walk would find" would be the same question, and only
one of them is what a target scan is for.
The counters are one accounting rather than several: ss_seen is
incremented once per object, in the pre-filter, so a skip the backend
raises after that point adds to ss_skipped alone. ss_seen equals
ss_filtered plus ss_skipped plus the sum of ss_class on any completed
scan, and llapi_scan_device_test asserts it -- an inequality would not
have caught counting the same object on both sides of the pre-filter.
An object with no LMA is answered with its IGIF rather than with the
parts to build one from. The inode number and generation of such an
object are its FID -- osd_scrub.c builds the same one, and it is not a
reconstruction, which is LFSCK's job -- so lfsr_fid carries it and
LLAPI_SCAN_FID is set. That leaves the generation with no consumer of
its own and it is not a field: what remains is lfsr_ino, which every
object has and which names the ones no IGIF can, a ZFS object id being
wider than an IGIF holds. There is no LLAPI_SCAN_GEN bit for it.
llapi_scan_namespace.3 gains the pointer to this page here rather than
in the patch that wrote it, where it would have referred to a page the
tree did not yet have.
A worker that cannot be initialised degrades the scan rather than
failing it, once one worker exists, as the pthread_create() arm beside
it does: a backend opens the device again per worker,
lfsp_thread_count is a __u8, and the cursor feeds whatever workers
there are, so running out of them is not a reason to turn a complete
result into an error.
The page says what stx_attributes carries across scanners. A device
scan narrows the inode's own flags to the set llite also declares, so
one file answers "lfs find -attrs d" the same way whichever scanner
ran, and so_attrs_mask declares that set in the record's
stx_attributes_mask -- which is what lets a consumer tell "not
immutable" from "this scanner cannot say". ERRORS also spells out the
two EINVAL cases a caller is most likely to trip, a short lfsp_size
and a short ss_size.
A directory's own stripe takes LMV_MAGIC_V1. LMV_USER_MAGIC is how
the tree marks a *default* LMV: cb_get_dirstripe() sets it exactly
when fp_get_default_lmv is asked for and LMV_MAGIC_V1 otherwise, and a
default lives in trusted.dmv, which is never read here -- trusted.lmv
is the directory's actual stripe. llite fills LMV_MAGIC_V1 for
LL_IOC_LMV_GETSTRIPE, so LMV_USER_MAGIC here would have the two
scanners answer one striped directory with two different values in the
same field -- the opposite of what lfsr_lmv promises, which is that
the field means one thing whichever scanner filled it.
No in-tree consumer reaches that difference from a scan record yet, so
this is a contract broken rather than a wrong answer observed.
lmv_dump_user_lmm() is what it would break: the magic decides its
"(Default)" prefix and which fields a bare -v shows, and
llapi_lov_dump_user_lmm() accepts both values, so a caller pointed at
a scan record instead of an ioctl reply would print a device scan's
directories as defaults. The size is unchanged either way,
lmv_user_md_size() growing only for LMV_USER_MAGIC_SPECIFIC.
A parent FID the scan will not vouch for is left cleared rather than
written: LLAPI_SCAN_PARENT is set only where fid_is_sane() accepts
what trusted.link held, and the field follows the bit, so a caller
reading it without testing lfsr_valid does not get the raw xattr.
llapi_scan_device.3 gives lfsr_parent_fid an entry of its own, since
LLAPI_SCAN_PARENT is both what a caller puts in lfsp_want and what it
has to test in lfsr_valid, and a field named in neither place is a
field a caller cannot use.
run_tests() in lustre/tests/llapi_test_utils.{c,h} is split so that
run_test_tbl() takes the table: the new test is handed a device where
the others take a mountpoint. llapi_scan_test, llapi_root_test and
llapi_pool_test share that helper, which is why the split is worth
naming here rather than left to be read out of the diff.
so_flags is narrowed to the attributes a namespace scan can also
report. ll_dir_ioctl() puts only IMMUTABLE, APPEND and, under
HAVE_LUSTRE_CRYPTO, ENCRYPTED into stx_attributes_mask, and
llapi_scan_namespace() takes stx_attributes from that mask -- so
reporting COMPR and NODUMP here would make one file answer "lfs find
--attrs d" differently depending on which scanner ran, where
stx_attributes is meant to mean one thing whichever filled it. The
bit is on the MDT inode either way: making it answerable is llite's to
do, for both scanners at once.
so_attrs_mask carries that same set into the record's
stx_attributes_mask, rather than the scanner assuming one set for
every backend: a bit clear there is "cannot say", which is a different
answer from the attribute itself being clear. It is carved from
so_padding, so every offset above it is unchanged, and a backend that
leaves it zero has its records leave LLAPI_SCAN_ATTRS clear rather
than claim attributes nothing vouches for.
scan_lmv_to_user() clears the shard area, not just the header. One
buffer serves every object a worker meets, and a directory's converted
LMV carries the real lum_stripe_count while the size answered is
header-only -- the shard FIDs being left out on purpose, an MDT index
needing an FLD lookup this has no client to make. A consumer sizing
lum_objects[] by the count rather than by lfsr_lmvsize, which
lmv_dump_user_lmm() does, then read whatever the last object left
there: a preceding foreign directory's opaque value, in full. The
clear now reaches as far as such a read can, bounded by the room
before a count off a device measures anything, so what it finds is
zeroes. lfsr_lmvsize is the measure either way, and the header says
so.
The buffer is a union of the two structures it holds rather than a
bare char[], which carries neither one's alignment while the call site
casts to both.
An unreadable inode is counted by the chunk that owns it and no
other. The skip arm continued without asking either question the
readable path asks below it, so an inode just past end_ino was
consumed by this chunk and read again by the chunk starting there,
and ss_seen and ss_skipped each took it twice; a reserved inode that
could not be read was counted as an object, where a readable one is
not. ext2fs_get_next_inode_full() assigns *ino before returning any
of the three errors this arm catches -- unlike the errors above it,
which leave it untouched -- so both questions can be asked here.
Nothing was ever delivered twice: it was the accounting that
overstated, which is what the counters exist to be trusted for.
fid_is_root() moves to the UAPI header and the scan calls it. It was
in lustre_fid.h by history rather than by need: LU_ROOT_FID and
lu_fid_eq() are both already public, so the helper is a one-line
predicate over two things userspace has, and userspace now has the
same question to ask -- a scan of a target has to tell the root from
the rest of FID_SEQ_ROOT. unlikely() is dropped, that header being
compiled in userspace too, and the definition is removed from
lustre_fid.h, which includes the UAPI one, so every existing caller in
llite, lmv, lod, mdd and mdt is unaffected.
That also settles a test that had drifted. lu_fid_eq() is a
whole-struct compare, so a hand-written test of f_seq and f_oid called
a FID in FID_SEQ_ROOT with FID_OID_ROOT and a non-zero f_ver the root,
where fid_is_namespace_visible() on the MDT would not. The narrower
point the spelling exists for is unchanged: the echo client's root
shares the sequence and is still not visible.
Both scanners fill lfsp_stats. The structure arrives with this patch,
which is why the walk did not fill it before -- the field came in with
the code that needed it, not with the walk -- and there is no reason
for it to stay that way: a walk counts an object seen once, then
filtered, skipped, or delivered and namespace-visible, which is every
class a walk can meet, and the same seen == filtered + skipped +
sum(class) holds. It counts atomically rather than per-worker,
llapi_scan_namespace() running its callback on every thread at once
where the device scan merges per-worker counters at the end.
lfsp_stats is checked against LLAPI_SCAN_STATS_MIN_SIZE there too, and
after the copy-in for the same reason: a short lfsp_size may not reach
the field.
The struct comment says what the field is rather than which scanners
use it. llapi_scan_device.3 also says the stats structure is
extensible -- ss_size negotiates it exactly as lfsp_size does, so
counters appended later leave an older caller reading its own fields
-- which was true and written down nowhere.
The struct comments describe the fields rather than which scanner uses
them. What each scanner offers lfsp_filter, and what each takes 0 in
lfsp_want to mean, were spelled out in the header and again on both
pages, and the header's copy is the one that goes stale: it claimed a
device scan "offers no path and no name", which LU-20637 makes untrue
two patches later by naming an OST object from the parent FID in
trusted.fid. What is left is the part that does not change -- the
filter sees only what the scan already had, and 0 means a default the
two scanners do not share -- with each page saying which fields that
is for itself. llapi_scan_device.3 gains the one sentence it was
missing, what 0 means there.
stack_trap is called without EXIT, which is its default, and the
setupall after stopall carries no justification -- it is the ordinary
shape and needs none. The base tree passes an explicit EXIT eighteen
times of ninety-two; this series had added thirteen more, so dropping
ours leaves the file at the eighteen that were already there.
The EXAMPLES fragment escapes its newline with a single backslash.
troff reads that as a register reference, so the page rendered
printf("%llu files; %llu seen, %llu skipped0, with the escape and the
closing quote swallowed. It needs the doubled form that
llapi_fid_parse.3 and llapi_scan_rec_path.3 already use.
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools
Change-Id: I8a28a8173b0b8f03ba8f9dc5a58a821118ac6296
LU-20650 lfs: find --since, from the changelog The three pieces were already here. llapi_scan_changelog() says which objects had an event, llapi_scan_fid() fills each one's record the way a walk fills it, and find_decide() judges it with the predicates a walk uses. --since composes them, so it answers over fewer objects and judges each the same way: the objects are a strict subset, narrowed and never different. The lines are not a subset of the lines -- a walk reaches a hardlinked file once per directory entry and prints it once per link, while --since reaches it once per FID and prints it once, under the first of its names that is under the root and matches. No new predicates and no second copy of the vocabulary. The argument is never guessed. A bare integer is an index, which is what "lfs changelog MDT <startrec>" already takes; a single-unit duration (30m, 2h, 7d) is that far back from now; a timestamp or @<seconds> is that time; anything else is an error naming the four spellings. A leading sign is refused rather than wrapped through strtoull(), and a duration reaching past the epoch is a typo rather than a window. cr_index belongs to one MDT, so a bare index is refused on a filesystem with more than one and the message says which time spellings do work. That count has to be right, and the obvious source is not: LL_IOC_GETOBDCOUNT answers with the size of the LMV's target array, so a single-MDT filesystem reports 64. It is used as an upper bound and each index confirmed with llapi_search_tgt() before its changelog is read. A changelog covers the whole filesystem, so the subtree the caller named is enforced against the pathname llapi_scan_fid() resolved, and the depth the walk would have counted is recovered from it for --maxdepth and --mindepth. Without that, a find rooted at one directory would answer for its siblings. Against every name the object has, not just the resolved one. llapi_scan_fid() resolves linkno 0, so a hardlinked file arrives under whichever name fid2path answered with first: with /a/f and /b/f the same inode, "lfs find /a --since 2h" would be handed b/f, reject it, and lose a file a walk of /a prints. -name has the same shape, which is why a target scan walks every linkea name. The names come from fid2path here, one ioctl each, and only a file whose link count says it has another name pays for any of them. A time anchor is cut on the record rather than at the start index, because a time cannot be turned into a per-MDT index without reading the log; coalescing has already reduced an object to its latest event, so an object whose latest event is older than the window did not change in it. An object unlinked between its event and now cannot be verified, so it is not in the answer -- that is what the flag gives up -- and the run says how many were dropped that way. --since is refused for a target scan rather than ignored. The option table is compiled into lfind as well, so `lfind --local --since 2h` parsed and then scanned the whole target: llapi_find_device() has no changelog to anchor on and never reads fp_since_kind. It joins the list find_device_supported() already keeps for --maxdepth, --threads and -printf. llapi_find_since() refuses fp_thread_count above 1 for itself, before it looks at anything else: this source reads one MDT's changelog at a time and has no work to divide. That is why lfs_find() now computes the default thread count on the walk's arm of the dispatch rather than above it -- computed for every search, the default alone was above 1 and the refusal fired on every --since run, which is not a setting the caller made. The mount point is found from the resolved root and not from the spelling, which are the same string only when the caller already gave a canonical absolute path. /home/user/data -> /mnt/lustre/data answered -ENODEV, llapi_search_mounts() matching mnt_dir literally, and a relative path went through the fsname to the *first* mount of that filesystem -- so under $MOUNT2 every object fell outside the subtree and the search printed nothing and exited 0. What is printed is then the caller's spelling of that root with the resolved remainder appended, because a walk prints the path as it was given -- param_callback() copies the argument and llapi_semantic_traverse() appends to it. Named from the mount every time, "lfs find . --since 2h" would answer /mnt/lustre/sub/f where "lfs find ." answers ./sub/f, and --since promises the same answer as the search without it. ONLY=56 is the regression check that the shared paths still answer as they did; the subtests for the new options arrive with the tests patch at the end of this series. Two things the page is careful about. An MDT with no registered user does not record at all, rather than recording and purging: mdd_changelog_off() clears CLM_ON when the last user deregisters and mdd_changelog_enabled() requires it. And the usage string spells the argument TIME|INDEX rather than TIME, because a bare integer is a changelog index -- the one distinction the parser goes out of its way to make, and the one a reader is most likely to get wrong. Test-Parameters: testlist=sanity env=ONLY=56 Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com> Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools Change-Id: I13426ef1ed12cd08d78aa5a5da9f8ba4465690d2
LU-20650 lfs: find --changelog, the log as source
--since treats the changelog as a candidate set and verifies each hit
against the live object, so it answers as it is now. --changelog
replaces the source: it reads the log and nothing else, and answers as
it was recorded. That is why -name here matches the name in the event
rather than the object's current one, and why an object unlinked since
the event is in this answer and cannot be in --since's.
It takes an MDT name or "all", required rather than optional because
getopt_long() accepts an optional argument only as --changelog=VALUE.
One named MDT is also what makes a bare --since index unambiguous, the
index being paired with the log it came from.
Every predicate a record cannot answer is refused naming itself,
rather than answered from a zero. Without --resolve that is -size,
-blocks, -perm, -links, --attrs, --xattr, the layout options, -projid,
-printf, --mdt-count and --mdt-hash; --xattr was the loud one,
reaching llistxattr(NULL, ...) once per object, and the rest were
quiet and worse, "--perm 644" matching nothing and its negation
everything. -uid and -gid are refused for a different reason and at
every setting short of --resolve: a changelog records who caused an
event, from the acting client's credentials, which is not who owns the
object. An object time is refused outright, a record carrying an
event time and the two being different questions; the message says
--since 1d is how to ask. Only --mdt, --maxdepth and --mindepth are
refused at every setting -- they describe a walk, or need something a
per-MDT log does not have. --ost and the object times are refused
without --resolve alone, the lookup supplying both.
-type is the one field with a third answer. A record says what kind
of object was created and only for four record types, so an object
whose event does not carry one is counted undecided rather than
compared against a mode of zero, which reads as "not a file" and "not
a directory" at once.
Output is a pathname where the FID still resolves to one and the FID
where it does not, which is the whole point of asking: an object
unlinked since its event is the case --changelog exists for, and
counting it as "no pathname" the way a target scan counts its own
objects would drop exactly that case in silence. The count of those
is reported at the end, so a caller knows some lines are FIDs.
The path argument names the filesystem, not a subtree, a changelog
knowing nothing about where an object sits; a path below the mount is
refused unless --resolve makes a pathname to match against.
--resolve looks each object up through the mount and hands
find_decide() what it found -- the path, the parent descriptor and the
object's own -- so -size glimpses the OSTs, -projid opens through a
descriptor of ours, and -printf gathers as a walk gathers. It fills
what the stream lacks and never restates what it has: the merge starts
from the looked-up record and puts every field the event answered for
back over it, taking only the type bits of the mode, so -perm reads
the live one. The subtree is tested on that pathname, over every name
the object has rather than the one linkno 0 answered with.
An object that has gone between its event and the lookup keeps the
answer honest in the other direction. It is still in this answer --
resolution fills and never removes -- but only where the record can
carry the question: where the search asks something only a lookup
supplies, or names a subtree the object can no longer be placed in, it
is counted undecided instead.
find_device_supported() refuses --changelog for the same reason it
refuses --since: the option table is compiled into lfind too, so it
parses there, and llapi_find_device() never reads it -- `lfind
--changelog all` would otherwise have run the target scan, printed
everything the other predicates kept, and exited 0.
-name joins -type as a predicate the record cannot always answer.
cr_namelen is 0 for an event on a FID whose name the log never held,
so CL_CLOSE, CL_SETATTR, CL_TRUNC and CL_LAYOUT all
arrive nameless -- a file created before the window and written inside
it is the ordinary case. Matching those against "" decided them both
ways and said nothing about it, and is not even reliably a non-match,
fnmatch("*", "") succeeding. They are counted undecided instead.
The first resolve failure that is not "this object has no name"
decides the exit status, as it already does for a target scan, so a
run that could resolve nothing cannot report itself complete.
fc_d is set only for a directory, as find_since_rec_cb() already had
it. printf_format_lustre() reads d == -1 as "not a directory", and
sr_fd is not always -1 for a regular file: under -printf the gather
widens, scan_rec_gather() opens the object for the MDT index and
leaves that descriptor in sr_fd. Passing it through sent every
striped file down the directory branch and onto an lmv buffer
find_rec_to_lmv() had just zeroed, so %Lc and %Li printed 0, %Lo
[0] and %Lp nothing.
-name is answered against the recorded name under --changelog.
find_since_pick() walks the object's other links to place it in the
subtree, and was substituting each link's basename into the record
before testing: right for --since, which asks what the object is now,
and wrong here, where the record is the answer. The alternate path
still settles the subtree; only the name stays as recorded.
The pathname a match prints goes through the caller's spelling
whichever way it was composed. Without --resolve it comes from
llapi_scan_rec_path(), which builds an absolute name from the mount,
so "lfs find ." answered /mnt/lustre/sub/f there and ./sub/f
everywhere else. find_since_respell() splits into a find_respell()
taking the two roots, and the print site uses it.
The demand mask -printf widens is the one handed to llapi_scan_fid():
find_want() builds fss_want with the -printf flag set and the resolve
passes it as sp_want. llapi_scan_changelog()'s own sc_want is a
separate literal and does not depend on -printf. What the two sources
share is fss_want, which is why --since and --changelog widen
together.
The comment above it claimed %s, %b and %LP as the reason; those come
from find_decide()'s own stat and find_get_projid(), gated on
fc_gather_all, and find_want() clears LLAPI_SCAN_PROJID on its way
out regardless. What the widening actually buys is LLAPI_SCAN_LMV,
for %Lc and its neighbours on a directory.
The cookie is not mentioned any more. fp_since_cookie arrives with
--since-cookie in the next patch, and this one documented it in
llapi_find_since()'s Return: block and in llapi_find_since.3 --
an -EINVAL for giving both an anchor and a cookie, and an -ESTALE,
neither of which exists here. -ENOTSUP does exist and was missing
from both.
A CL_MARK is dropped rather than delivered.
mdd_changelog_write_header() writes the marker flags into the union
with cr_tfid, so cr_tfid.f_seq would read back as CLM_ON|CLM_START,
0x10001 -- and fid_seq_is_igif() accepts anything in [12, 0xffffffff],
so the FID gate would pass a value that is not a FID and find_decide()
would print it as a FID with no pathname.
No such record can arrive today, so this is a guard and not a fix for
an observed answer: CHANGELOG_MINMASK is BIT(CL_MARK) and
mdd_changelog_mask_seq_write() hands it to cfs_str2mask() as the
floor, so CL_MARK cannot be masked out -- and
mdd_changelog_write_header() returns early whenever CL_MARK is in the
current mask, so no marker record is ever written. Asking for "creat
mkdir" gives back "MARK CREAT MKDIR", and deregistering every user and
re-registering leaves an empty log. The guard costs one test per
record and stops the flags being read as a FID if that ever changes.
It sits at the top of find_since_cand_cb(), above the FID gate. The
anchor is not taken there but in find_since_filter_cb(), which runs
first on every record the log delivered, marks included: it has to see
them, or it lags the end of the log by whatever trailing run of them
the scan finished on.
-printf's widening now reaches --since. find_want() clears
LLAPI_SCAN_PROJID on its way out and find_decide() gates
find_get_projid() and the OST glimpse on fc_gather_all rather than on
the demand mask, so a flag this patch sets for --changelog and not for
--since meant "--since -printf %LP" answered DEFAULT_PROJID for every
object where a walk answered the real one, and %s/%b answered from the
MDT instead of a glimpse.
The comment on the nameless case said cr_namelen is 0 for a MARK. It
is not: mdd_changelog_write_header() sets it to strlen(obd->obd_name)
and copies the device name in, so a mark arrives named "lustre-
MDT0000" -- which is also why "-name 'lustre-MDT*'" matched one before
the record was dropped above. A data event on a FID whose name the
log never held is the real case, and the sentence now says only that.
The undecided warning no longer claims a missing field.
find_since_cand_cb() also counts an object undecided when it has gone
and the search needed a lookup or named a subtree, and there the
record answered -- the object is what is missing. "could not be
decided from their changelog record" covers both.
LLAPI_SCAN_CL_F_ONCE is set for --since and not for --changelog. Once
means once per scan, which is what "as it is now" wants; a source
answering as it was recorded has a second burst to report, so an
object touched in two of them prints twice. The man page says so.
The dropped-objects warning names what the count is of -- "%llu of
%llu changed objects could not be read" -- rather than leaving a bare
number beside the errno it belongs to.
Test-Parameters: testlist=sanity env=ONLY=56
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools
Change-Id: If9ead118b5fc1dee86cfac6646106e982841d8e2
LU-20650 lfs: find --since-cookie, per-MDT anchor cr_index belongs to one MDT, so "where I got to" on a DNE filesystem is one number per MDT and not one number. --since <time> is the human spelling and means the same thing everywhere, but only as far as the clocks agree; a cookie is exact on every MDT, and is what a repeated job should use. The file is a line per MDT, "<mdtname> <index>", read as the anchors and rewritten with the indexes this run reached. It is written whole and renamed over, so an interrupted run leaves the previous anchors rather than half of the new ones -- a half-written cookie is worse than a stale one, because the stale one is caught. Which is the reason a cookie is worth having. If an MDT has purged past its anchor the records in between are gone, and reading from the oldest one left would return a short answer with nothing to say so. So the oldest surviving index is read first, by a one-record scan that stops at the first callback, and an anchor older than it is refused naming both numbers. Every MDT is checked before any of them is scanned: refusing from inside the scan would leave the earlier MDTs' matches on stdout with no cookie written, and the next run would repeat them. An MDT with no anchor is not probed at all -- 0 has nothing to be stale against, and that is every MDT of a first run. Zero is reserved for "the log is empty", which is why a record that arrives without an index is reported rather than recorded as index 0. Changelog indexes start at 1, so storing 0 for a record that has one would make a log holding records look like a log holding none, and 0 compares false against every anchor -- the check would go on being skipped and say nothing about it. An empty log does proceed: from a client, "nothing happened since the anchor" and "everything since it was purged" look the same, and refusing on that would fail on every idle filesystem. A cookie from another filesystem would anchor this one at indexes that mean nothing here, so a line whose name does not match the filesystem is ignored, and a file whose header names another filesystem is refused. Ignoring every one of its lines instead would leave no anchors at all, rescan this filesystem's whole log, and then drop the other one's anchors in the rewrite -- three wrong answers, none of them said out loud. The rest of the file is input the caller does not control either. The scanf width is not optional: %s into a fixed buffer is how such a file overruns it. Nor is the type of the bound test, since %x accepts a sign: "<fsname>-MDT-1" reads back as 0xffffffff, which passes a test made against an int and then indexes the array far past its end. So the sign is refused at the parse and the bound is compared against the value that does the indexing. --since and --since-cookie are two ways to say where to start, so giving both is refused rather than one of them silently winning. More than one path is refused for a related reason: lfs find runs the search once per path and every run advances the anchors, so the second path started past the end of the log, printed nothing, and left the records it should have matched behind the anchor for good. The cookie is proved writable in the same pre-pass that proves the anchors are not stale, and for the same reason -- a cookie that cannot be written leaves the matches on stdout and the next run repeating them. A missing directory component reads as ENOENT, which find_cookie_read() takes for a first run, so the typo was not noticed until every log had been scanned. The temporary file is fsync()ed before the rename, and the directory after it. The rename makes the replacement atomic against another process, which is not the same as durable: after a host crash the rename can be on disk while the contents are not, and an empty cookie parses as no anchors at all. The pre-pass probes the temporary name the write will use rather than the cookie itself, because the write creates "<cookie>.new" and renames over: what it needs is permission on the directory, which opening an existing cookie for append does not test. A cookie owned by the job user in a root-owned directory passed and then failed EACCES with every match already printed. The anchors are left where they were when a run did not finish. A resolve failure that is not "this object has no name" drops matches, and writing the cookie anyway would advance past the very records a rerun needs, so the exit status is settled before the rewrite rather than after it. An index is read with strtoull() and a leading sign refused, the MDT number having been refused for the same reason: "%llu" negates, so "-2" would anchor a run at 0xfffffffffffffffe, match nothing, and write that value back. find_device_supported() refuses --since-cookie along with --since: the option table is compiled into lfind too, so it parses there, and llapi_find_device() never reads it -- a target scan would have printed every object the other predicates kept and exited 0, without even reading or writing the cookie it was given. ONLY=56 is the regression check that the shared paths still answer as they did; --since-cookie's own subtests are in the tests patch at the end of this series. The cookie names the search as well as the filesystem, and a mismatch is refused the way a wrong fsname already was. The anchors say how far each log was read, not how far a subtree was: a record is counted before the FID and the subtree are tested, so a run over one subtree carries every anchor to the end of the log and a second subtree sharing the cookie starts past its own changes and prints nothing. That is the collision lfs_find() refuses when two paths are given to one command, reached by running the command twice. A cookie written before this carries no root and is still accepted. A cookie that exists and is not a regular file is refused up front. The probe settles the containing directory, not the name: the write ends in rename(), which answers EISDIR over a directory, while the read succeeds and returns no anchors -- so naming a directory scanned every log to the end, printed every match, and only then said it could not rewrite. An empty name is refused with it. The comment on the file format said a comment or a blank line costs nothing. It costs the read nothing; the rewrite emits the header and the anchors and nothing else, so it does not survive the run. Said so, the sentence otherwise reading as an invitation to annotate the file. The documentation for fp_since_cookie arrives here rather than in the previous patch, which described the field, its -EINVAL and its -ESTALE before any of them existed. The root is the rest of the header line rather than a field. A pathname can hold a space and %s cuts it at the first one, so a search under "/mnt/lustre/my data" was recorded whole and read back as "/mnt/lustre/my": the comparison then failed for every run after the first, and the message named a path the caller never gave. The root is whatever realpath() returned for the caller's path, so a directory with a space in it was enough to reach it. And the root is escaped rather than written raw, because the rest of a line still cannot hold a newline. Only '/' and NUL are excluded from a name, so "/mnt/lustre/anb" was written whole, read back as "/mnt/lustre/a", and refused for every run after the first naming a path the caller never gave -- the space case one character further out, with the remainder becoming its own line and being dropped. Both sides escape and the reader compares the escaped forms, so nothing decodes: the mapping only has to be injective for a comparison, which is also why backslash is escaped. The refusal names both sides in that spelling, or a newline would break the line meant to name it. find_cookie_check()'s two other refusals say so as well. An empty --since-cookie and a name too long to append ".new" to both returned the errno alone, beside a "is not a regular file" case that reports itself, so the run ended at the caller's "failed for '<path>': Invalid argument" with nothing naming the argument at fault. Two diagnostics that this patch makes reachable are corrected with it. The refusal of --resolve without --changelog said "--since already reads the object", which names an option the caller need not have given once --since-cookie exists; it names both anchored spellings now. And a cookie refused for its filesystem or its root was reported twice, the second line saying "cannot read" over a file that read perfectly well -- find_cookie_read() has already named which refusal it was, so the wrapper is left with what fopen() passed up. The MDT number is exactly the four hex digits find_cookie_write() emits. %4x converts the leading digits and ignores what follows, so "<fsname>-MDT00001" and "<fsname>-MDT0000_UUID" both anchored MDT0000 at an index never written for it -- the short answer with nothing said that the stale check exists to catch, from a file this function's own comment says it ignores anything but "<mdtname> <index>" in. The span refuses the sign as well, so the separate test for it goes. ERRORS gains -ESTALE, which the previous patch dropped on the understanding it would arrive with the cookie and which the kernel-doc Return: block has listed since. It is the error the whole option is built around, and the man page was the one place a caller of the API could not find it. Test-Parameters: testlist=sanity env=ONLY=56 Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com> Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools Change-Id: Ia8ce93368a2c2c8c2ea2ee88c8e7f7e099b1bfa5
LU-20650 tests: sanity cases for find's changelog flags lfs-find.1 gains --since, --since-cookie, --changelog and --resolve: what each answers, that --since is a strict subset of the same search without it, that a bare index belongs to one MDT and is refused where that is ambiguous, that a number which could be an index or an epoch second is never guessed at, and that reading a changelog neither consumes it nor needs a registered user while records exist only while some user is registered. TIME's spellings are given in full, since set_since() takes eight of them and its error text offers one the page would otherwise not list. The page says what --resolve does not answer for -- an object gone between its event and the lookup is counted undecided and dropped, so --resolve can leave out an object the same search without it reports by FID -- and which name -name is tested against. The log is coalesced to one record per object before the search sees it, so the name that can match is the one the object's latest event to carry one carried: a file created as f and hardlinked as g is matched by "-name g" alone, and the pathname printed is its first name rather than the one that matched. A plain walk meets each name in turn and matches all of them. The behaviour is deliberate -- substituting a current name would answer -name against something the record never said -- but a reader had no way to know it. Four sanity cases. 160aa asserts the promise --since is worth having -- that its answer is a subset of the same find without it, and a smaller one -- rather than asserting a particular file, so it does not depend on what else the filesystem is doing. It names the files it expects rather than only counting them, since a count that merely shrank is also satisfied by returning nothing, and it widens the gap around its window so a node that spends seconds in sync does not change what the test means. Its locals stay separate from the assignments that fill them, and it sorts off the pipeline rather than in it: "local x=$(...)" takes local's own exit status, which is always 0, and a command piped into sort reports sort's, so either shape would let a failing lfs find read as the empty answer the index case asserts. 160ab walks the refusals: -size, an object time, --maxdepth, a path below the mount, an unknown MDT, -perm, -links, --mdt-count, --mdt-hash and --xattr are each refused, --resolve on its own is refused rather than dropped, and a named MDT is accepted as the other half of --changelog's argument. It asserts the --resolve answer rather than the exit status alone: -size needs a strict size, the MDT holds only the lazy one, and a resolution that did not glimpse left every object "could not be decided" while still exiting 0. It also asserts -printf's %s, which reads the same size through a widened gather, that --projid is answered rather than failing on the first record, and that a subtree search under --resolve answers for that subtree and not the whole filesystem. 160ab covers the two things that make --changelog worth having over --since, which the man page leads with: an object unlinked since its event is still in the answer, and its FID names it once no pathname does. It ends on -name, which is answered from the record: the log is cleared, one file that predates the clear is written to -- data stores, which carry no name -- and another is created. The named one has to come back and the nameless one must not, so a -name that matched everything fails the second assertion and one that matched nothing fails the first. 160ac runs the cookie three times: the first writes it, the second returns the new object and not the old one, the third returns nothing, and then a stale anchor and both anchors together are refused. It asks for creations only. A wide mask records the test's own reads, and then "nothing changed since" is never true -- which is what hid an off-by-one in the cookie until a narrow mask made every run repeat the last object of the one before it. A test that asserts a precondition instead of creating one only holds where that precondition happens to be true. 160ac's stale anchor is made rather than assumed -- records the cookie has not consumed, then a purge, then one more past the hole -- because writing index 1 is only older than the oldest surviving record on a filesystem that already has changelog history. On a fresh one index 1 IS the oldest record, nothing was purged, and refusing would be wrong. 160ac also feeds the cookie two files it is never written with: one whose MDT index is negative, and one whose header names another filesystem. Neither is a bad anchor to ignore -- the first indexes the anchor array far past its end, and the second is a whole-log rescan of this filesystem plus the loss of the other one's anchors -- so one has to be ignored and the other refused, and the file it refuses has to still be there afterwards. 160ad hardlinks one file into two directories and searches under each. A changelog names an object by FID, and fid2path answers with one of its names -- the first linkea entry, the one the file was created under -- so a search rooted at the other directory is answered about a name that is not under it, and -name about a basename the object does not go by there. Without the links being walked it finds the file under a and loses it under b, which is a silent miss and the worst shape a wrong answer can take. The tag is tests: rather than doc: because that is where the bulk of the change is; the man page rides along because the options it describes are the ones these cases exercise. There is no version gate. Nothing in this series touches server code -- it is lustreapi.h, lustre/utils/ and Documentation/ -- so the options work against any server old enough to have changelogs, and a gate on the MDS would skip interop runs that pass. A client gate would say nothing either: this script ships with the client whose lfs has the options. The third Test-Parameters line below is that claim under test rather than only asserted: an old server, a new client, and these four cases. Test-Parameters: testlist=sanity env=ONLY=160aa,160ab,160ac,160ad Test-Parameters: testlist=sanity env=ONLY=160aa,160ab,160ac,160ad mdscount=2 mdtcount=4 Test-Parameters: testlist=sanity env=ONLY=160aa,160ab,160ac,160ad serverversion=2.15.6 Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com> Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools Change-Id: Id4001c6821e72f15b83a9733bbb28cef4248b822
| unique failing test | history |
|---|---|
| sanity2@ldiskfs+DNE:test_157d | NEW unique failure for this branch in the last 30 days, and was seen 0 times across 0 other branches 0 reviews |
| sanity2@zfs:test_157d | seen in 1 other review |
LU-20603 llapi: pull a scan's records in batches Add llapi_scan_namespace_open(), llapi_scan_next() and llapi_scan_close(): the namespace scan turned around, so a consumer pulls records in batches on its own thread instead of being called once per object on the scan's. A layer over llapi_scan_namespace() and not a second scanner -- the scan runs on a thread of its own, its callback copies each record into the batch being filled, and llapi_scan_next() hands out a full batch while the scan fills the other. The copy is what a batch costs. A callback record points into buffers the scan reuses for the next object, so a record that outlives its callback has to own what it points at: the path, the name, the layout, the directory stripe, the linkea, the rename source and the jobid are copied into an arena the batch owns and the pointers rewritten. The descriptors are not carried and read -1. Two batches suffice, the scan blocking when the one it fills is full, so what is in flight is bounded by the batch size and not by the tree. The caller sees an array of record pointers rather than of records, so its stride is a pointer's and a record that grows does not move its neighbours under a caller built against the older size. A filter in lfsp_filter runs on the scan's threads with the caller's data, which the layer keeps beside its own. An error is reported after every record produced before it has been handed out, so the batches before it are whole; a close on a running scan stops it, and that is not an error. A handle belongs to one consumer thread, and that is enforced rather than only stated: llapi_scan_next() and llapi_scan_close() refuse a second caller with -EBUSY. Without it a second consumer resets the batch the first is still reading and installs one whose sb_count is already sb_cap, and the next append writes past the end of sb_recs -- reproduced under ASan on the unfixed build, five runs out of five, and gone on the fixed one, which serves every object once. It is the opposite of the callback API's rule, where the callback runs on every scan thread at once, so it is said in the header and the man page too. The arena keeps every chunk across a reset rather than the largest. A batch needing two chunks -- about 320 bytes of payload per record at the 1024 default, which a path plus an 8-stripe layout reaches -- otherwise malloc'd and freed the second on every batch, which is what the comment said it did not do. Measured on a 20k-object tree off Lustre, where per-object work is a cached lstat() and the copy is at its largest share: a batch of 1024 costs 76 ns per object over the callback at 4 threads (+8%), 135 ns at one (+5%); a batch of 1 costs 2-4 us, the lock round trip per record. On a scan whose per-object work is an ioctl the same copy is a fraction of a percent, which the man page says. test16 rides along: it asks llapi_scan_namespace() what lfsp_got reports, which is the callback API and not this one, and belongs with whichever change lands lfsp_got. llapi_scan_next.3 says when a batch stops being the consumer's, not only that it does. The release happens at the top of the following llapi_scan_next(), before that call waits for the scan rather than when it returns, so a consumer that hands a batch to another thread and calls again to overlap the next fill has released what that thread is reading -- and the -EBUSY that refuses a second caller does not refuse a reader racing one. The arena is rewound rather than freed, which is what makes a steady-state scan allocate nothing per batch, so that reader finds the following batch's bytes and not a fault. The -EBUSY case is made rather than waited for. A second consumer is refused only while the first is inside llapi_scan_next(), and a test that calls from between two of them never reaches the guard it is about -- the ASan overflow above cannot fail it. A filter that sleeps holds the scan back so the third call blocks; two calls come first, the layer filling one batch ahead and the consumer not blocking until it has drained that. The overlap is then measured, not assumed: a machine slow enough to put the second call outside the window makes the case unreachable, and an unreachable case is reported rather than failed. Test-Parameters: trivial testlist=sanity env=ONLY="56El 157c" Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com> Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools Change-Id: I29aaa31a4c0bd34ca591316601a653f9b5ae70c2
LU-20564 utils: probe md RAID geometry for OST mkfs/mount mkfs.lustre now probes the md array underneath an OST target (through stacked dm devices) and seeds -E stride/stripe_width so mballoc can align and size allocations to full stripes. mke2fs can derive these from the blkid topology, but that is absent on some stacked configurations. raid0, raid4/5/6, raid10 and raidkm (level 71) are decoded; the replication factor of raid10 and the declustered g/m of raidkm come from the md layout word. User-supplied stride/stripe_width values are respected, misaligned partition starts draw a warning at mkfs time, and mount.lustre now warns when an OST on a striped array has no (or mismatched) fs geometry. A stripe_width that is a multiple of the array row is still stripe-aligned and is not reported. mkfs.lustre also warns when the array's data row (k * chunk) is not a power of two. Such a row cannot be written whole by any client: bulk RPC sizes are powers of two in practice, so a row divides some RPC size if and only if the row is itself a power of two. A geometry like k=5 at 64K (320KiB) or k=14 at 64K (896KiB) leaves a partial row at the tail of every RPC, which the array services as a read-modify- write, at every chunk size and with no tuning available. Unlike stride/stripe_width this cannot be corrected afterwards -- it is a property of the array -- so the warning is issued at mkfs time, while the geometry can still be chosen, and is not suppressed by a user-supplied stride/stripe_width. Tested on RHEL 9.7 against live loop-backed md arrays: raid0/4/5/6, raid10 near, far and offset layouts, a non-power-of-two row, user-supplied values, dm-linear over one array and spanning two with conflicting geometry, and aligned and misaligned partition starts. Also tested against live raidkm (md level 71) arrays: plain m=2 and m=3, parity-last, and declustered g=6/m=2 and g=8/m=3 over wider pools. In every case the computed row matches md's own queue/optimal_io_size. Test-Parameters: trivial Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com> Assisted-by: Claude:Fable-5 claude-code Assisted-by: Claude:Opus-5 claude-code Change-Id: I442528cea51a55c6b74f7f2282a1db566eab9783
LU-20542 gss: retry callback RPC when reverse ctx is stale
A server cannot renegotiate a reverse GSS context; one is only
installed from a peer's forward request. So when the peer drops its
contexts, the next callback is answered with GSS_S_NO_CONTEXT,
sptlrpc_req_replace_dead_ctx() returns -ECONNREFUSED, and
ldlm_handle_ast_error() evicts a client that was only mid-refresh.
This is the sanity-sec test_27d/27e failure.
Resend the callback a bounded number of times instead, marked with
rq_gss_rvs_resend so it skips the context refresh in
ptlrpc_check_set() and reaches the peer over the same dead context.
Every other resend still refreshes, so a reverse context that goes
stale on its own is still detected and replaced. Blocking and
completion ASTs stay bounded by rq_no_resend and the lock callback
timeout; for a glimpse AST, which sets neither, the resend count and
its backoff are the bound.
Fixes: 67acf6047e34 ("LU-17317 gss: do not continue using expired reverse context")
Test-Parameters: testgroup=review-dne-selinux-ssk-part-2
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Change-Id: I2a007b42656eb9aa0bed00291b67b6ac7db2d389
| failed enforced test | platform | detail | |
|---|---|---|---|
| review-dne-selinux-ssk-part-2 | RHEL 8.10 / x86_64 | ran 7 tests. 1 tests failed: sanity-sec. | session |
LU-20648 mdc: fix changelog mask composition
A zero cf_mask means two different things and chlg_ioctl() composed
them as if it meant one. On the server it means "this user has no
mask of its own": mdd_chlg_usermask() returns 0 for a record that
carries none. On the client it means "do not filter": a zero
crs_user_mask skips the record type test altogether.
So "lfs changelog --user NAME --mask creat" for a user who has no
mask computed in.cf_mask & 0, which is 0, which the record test then
read as unfiltered. The caller got every record type back and no
error, which is a wrong answer rather than a failure.
Compose the two zeros as what they mean. A user with no mask of its
own is not restricted to nothing, it is unrestricted, so a caller's
mask passes through unchanged; only two real masks intersect.
Registration writes a rec2 when either a mask or a name was given.
It fills cur_mask when a mask was given, and otherwise only while the
server's changelog_mask is still minimal, where a maskless user is
handed CHANGELOG_DEFMASK instead. So the reachable case today is a
registration with a name and no mask made while the server mask is
set to anything wider. A user registered with neither name nor mask
gets the older record type, whose mask reads as zero whatever was
written, and is affected too once LU-20647 makes such a user
reachable by lookup at all.
Two real masks that do not overlap intersect to zero as well, and
zero cannot carry "nothing matches" while it also means "do not
filter". Two absolute masks cannot reach it: cfs_str2mask() seeds an
absolute mask from its minmask argument, and both the registration and
"lfs changelog --mask" pass CHANGELOG_MINMASK, so "-m creat" is really
MARK|CREAT and "--mask mkdir" is MARK|MKDIR -- always sharing
BIT(CL_MARK). A relative one does. llapi_convert_str2mask() seeds
from *oldmask when the first token carries an operator, and
lfs_changelog() seeds that with CHANGELOG_DEFMASK, so "--mask
=-mark,-creat" is DEFMASK without those two: non-zero, and sharing no
bit with a user registered "-m creat". So the wrong answer is
reachable from the shell, and against such a user "lfs changelog
--user clN --mask=-mark,-creat" printed every record it had.
So crs_user_mask now names the record types to deliver: it starts as
all bits, an empty intersection is no bits, and the record test is a
plain bit test with no special case for zero.
test_160z asserts that the user carries no mask before going on,
rather than reasoning that it must: changelog_register() sets +hsm
before registering, and mdd_changelog_mask_seq_write() rebases a
relative mask onto CHANGELOG_DEFMASK while the proc mask is still
minimal, so the branch that hands a maskless user a default one
cannot fire there. The test widens the mask so that CREAT and MKDIR
are recorded whatever an earlier subtest left it at, and only on
$SINGLEMDS, which is the facet its trap restores. It asks for --mask
creat and requires the MKDIR record to be absent, then asks without
--mask and requires it to be present. The second half is what keeps
the fix from turning "no mask" into "no records".
160z covers the disjoint case in its second half, with a user
registered "-m creat" read back through "--mask=-mark,-creat". An
earlier attempt used two absolute masks and was removed after a
control run showed it passing against the unfixed module as well; this
one cannot, the composition there being zero and zero meaning "do not
filter". The control beside it is the same user read without --mask,
so a run that selects nothing because the user has nothing cannot be
mistaken for the fix working.
Fixes: 41b55cf2309d ("LU-19296 changelog: Add user-specific changelog filtering")
Test-Parameters: testlist=sanity env=ONLY=160z
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools
Change-Id: Ib178e4c15be40c415262bf06c08da8b061855cf9
| unique failing test | history |
|---|---|
| sanity-quota@zfs:test_48 | seen in 2 other reviews |
| failed enforced test | platform | detail | |
|---|---|---|---|
| review-dne-part-5 | RHEL 9.7 / x86_64 | ran 5 tests. 1 tests failed: sanityn. | session |
| review-dne-selinux-ssk-part-2 | RHEL 8.10 / x86_64 | ran 7 tests. 1 tests failed: sanity-sec. | session |
LU-20647 mdd: look up a changelog user of either record type
mdd_changelog_user_register() writes CHANGELOG_USER_REC when the
caller gave neither a mask nor a name, deliberately, for
compatibility. mdd_changelog_user_lookup_cb() matched
CHANGELOG_USER_REC2 only, so such a user was never found and
mdd_changelog_user_lookup() returned -ENOENT. That is the shipped
"lfs changelog --user", which reaches the callback through
OBD_IOC_CHANGELOG_FILTER, not only the library call.
Every other MDD walker already takes both types and guards the fields
that exist in only one of them, so the callback was alone in
rejecting the record rather than the field.
The type test cannot simply be dropped. struct
llog_changelog_user_rec is half the size of rec2 and carries neither
cur_mask nor cur_name, and the || short-circuited before strcmp()
reached cur_name; removing the first clause would read past the end
of the record into whatever the llog buffer holds next. So a lookup
by ID matches on cur_id alone, a lookup by name skips an old record
instead of comparing against it, and the reply's username is filled
only from a rec2. The reply's mask already came from
mdd_chlg_usermask(), which returns 0 for the old type, and a zero
mask is what the client reads as "do not filter".
The reply's username comes from the record's type and not from how the
lookup was made. mdd_iocontrol() passes one buffer as both request
and reply, so cf_user_id holds the record's ID by the time the old
"was this a lookup by ID" test read it -- true on every lookup, and
harmless only because a name lookup had matched that same name.
The name comparison is bounded as well. cur_name comes off disk with
no guarantee of a NUL inside its 16 bytes, and
mdd_changelog_name_check_cb() already allows for that on the same
field.
test_160w covers this command but registers both of its users with a
name and a mask, so both records are rec2 and the plain path was
never walked. test_160y registers one user plainly, one with a mask
and one with a name, and asks for the records of each; the masked user
is the control that has always worked. The named user is coverage
rather than a regression case: the old callback rejected a
CHANGELOG_USER_REC in the first clause of its ||, before strcmp() ran,
so a lookup by name walked past a plain record before this patch as it
does after it. Only the cf_user_id != 0 path changes behaviour, and
the client parses "cl<N>" into an ID, so that is the path lfs takes.
This wants LU-20648 beside it. Making a plain user reachable makes
cf_mask == 0 the ordinary answer, and a client without that fix
composes in.cf_mask & 0 and reads the zero as "do not filter": so
"lfs changelog --user cl1 --mask creat" on a plain user would print
every record type where it used to stop at -ENOENT. Landing this one
alone trades a lookup failure for a wrong answer.
Fixes: 5b85a4eb7510 ("LU-19296 changelog: retrive changelog user info from MDT")
Test-Parameters: testlist=sanity env=ONLY=160y
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools
Change-Id: I12ef76c773ce31e4548db9022f381cabfe7f5ddc
| failed enforced test | platform | detail | |
|---|---|---|---|
| review-dne-zfs-part-1 | RHEL 9.7 / x86_64 | ran 3 tests. 1 tests failed: sanity. | session |
| review-dne-zfs-part-7 | RHEL 9.7 / x86_64 | ran 6 tests. 1 tests failed: sanity-ec. | session |
(minor) This does not seem to hold, and it argues against the Fixes: tag just below it.
sptlrpc_sepol_get() already returned genuine error pointers before 8a6788b508db:
if (imp_sec == NULL)
RETURN(ERR_PTR(-EINVAL));
...
out = sptlrpc_sepol_get_cached(imp_sec);
if (!out)
RETURN(ERR_PTR(-ENODATA));
sepol_helper() also returns `ret >> 8` of call_usermodehelper(), so when the helper cannot be exec'd at all (missing /usr/sbin/l_getsepol gives -ENOENT) rc comes out as -1, which IS_ERR() accepts.
What 8a6788b508db changed is the l_getsepol-exits-nonzero case, where the status was positive. Stating that the leak is only reachable after it may keep the fix from being picked up on branches that predate it. Would it be more accurate to say 8a6788b508db made the path much easier to hit?
(suggestion) The other sptlrpc_sepol_get() callers have the same sequence: mdc_request.c:373, mdc_reint.c:199/345/409/487 and mdc_locks.c:326/481/545 all jump straight from the IS_ERR() branch to err_free_rq, which calls ptlrpc_request_free(), so they leak the import reference and the imp_reqs count in the same way. That is pre-existing and this patch does not have to carry it. It is not a one-line swap either, since err_free_rq is shared with the post-pack failures reached through err_put_sepol and those must keep ptlrpc_request_free(). Is a follow-on change under the same ticket planned for them?
LU-20050 ptlrpc: fix import ref leak on sepol failure
ptlrpc_connect_import_locked() calls ptlrpc_request_alloc(), which
takes a reference on the import via class_import_get(). When
sptlrpc_sepol_get() fails, the IS_ERR() branch calls
ptlrpc_request_free(). That only returns the request to its slab
and does not drop the import reference.
The leaked reference keeps client_obd_cleanup() from running, so
the LDLM namespace kobject is left in sysfs and the next mount
fails with -EEXIST.
Use ptlrpc_req_put() on this path instead. It drops rq_refcount to
zero and calls __ptlrpc_free_req(), which releases the import.
The second failure path, after ptlrpc_request_bufs_pack() returns
an error, still uses ptlrpc_request_free(). That is correct: the
out_free label inside ptlrpc_request_bufs_pack() has already
dropped the import reference, so only the slab is left to free.
Both paths now carry a comment saying which case they are.
This bug is only reachable after 8a6788b508db, which stopped a
positive errno from reaching ERR_PTR() and so made the IS_ERR()
branch above actually trigger.
Fixes: dd200e5530fd ("LU-8955 ptlrpc: manage SELinux policy info at connect time")
Assisted-by: Claude:Opus-5 claude-code
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Change-Id: Ia4267f87219e2a7e4fbcb996978543174bcb7197
| failed enforced test | platform | detail | |
|---|---|---|---|
| review-dne-zfs-part-4 | RHEL 9.7 / x86_64 | ran 9 tests. 1 tests failed: sanity-quota. | session |
| review-dne-zfs-part-7 | RHEL 9.7 / x86_64 | ran 6 tests. 1 tests failed: sanity-ec. | session |
(minor) Does this second Fixes: tag point at the right commit? 8e86156c34f3 added ptlrpc_req_put() to osp_sync_new_unlink_job() and osp_sync_new_unlink64_job() on paths that run *after* a successful pack, where dropping rq_import is correct. The ptlrpc_req_put() on the pack error in osp_sync_new_job() itself goes back to 1d371ca47e67 ("LU-1303 osp: OSP logging functionality"), and the class_import_put() at out_free that leaves rq_import set is older still.
So the double release predates 8e86156c34f3. Either drop this tag or point it at the commit that actually introduced the imbalance.
(minor) The comment promises more than the function delivers. out_ctx falls through to here after sptlrpc_cli_ctx_put(request->rq_cli_ctx, 1) without clearing rq_cli_ctx, so a caller that uses ptlrpc_req_put() reaches sptlrpc_req_put_ctx() in __ptlrpc_free_req() and drops the same context reference a second time. That is exactly gss_cli_ctx_fini_rpc(): it passes a non-NULL ctx, so out_free is unreachable for it and out_ctx is its only failure exit, and it disposes of the request with ptlrpc_req_put(). It is pre-existing rather than introduced here, but the commit message says clearing rq_import "lets callers use either free or put", which isn't true while the ctx is still dangling. Would sptlrpc_req_put_ctx(request, 1) at out_ctx (it does the put and NULLs the field) make the claim hold?
(defect) Is ptlrpc_req_put() safe here for a request that came from the pool?
__ptlrpc_request_alloc() falls back to ptlrpc_prep_req_from_pool() when the slab allocation fails, and that request keeps the pool's preallocated rq_reqbuf while rq_cli_ctx is still NULL (nothing has packed yet). __ptlrpc_free_req() then does:
if (request->rq_reqbuf || request->rq_clrbuf)
sptlrpc_cli_free_reqbuf(request);
and sptlrpc_cli_free_reqbuf() starts with LASSERT(ctx) on req->rq_cli_ctx, so this looks like an LBUG rather than a clean free.
The path seems reachable: osc_brw_prep_request() uses ptlrpc_request_alloc_pool() for OST_WRITE, so under memory pressure the pool fallback is taken; if the OSC import is idle at that point, ptlrpc_reconnect_if_idle() -> ptlrpc_connect_import_locked() itself does ptlrpc_request_alloc() and returns -ENOMEM under the same pressure. The old code went through ptlrpc_request_free(), which just returned the request to the pool.
The request also has no capsule yet (req_capsule_init() runs below), so it is not really in a state ptlrpc_req_put() expects. Would keeping the explicit atomic_dec()/class_import_put() plus ptlrpc_request_free(), or clearing rq_import and then freeing, be safer than routing this through the full teardown?
LU-20546 ptlrpc: fix import ref on request setup failures
ptlrpc_request_alloc() takes a reference on the import. Two
setup paths get its release wrong, in opposite directions.
ptlrpc_request_alloc_internal() open-codes atomic_dec() on
imp_reqs and then calls ptlrpc_request_free() when an idle
reconnect fails, so class_import_put() is never reached and the
reference leaks. Call ptlrpc_req_put() instead, which does both.
ptlrpc_request_bufs_pack() has the reverse problem: out_free
drops the reference but leaves rq_import set, so a caller that
disposes of the failed request with ptlrpc_req_put() releases it
a second time. gss_cli_ctx_fini_rpc() and osp_sync_new_job() both
do that on a pack error. Clear rq_import once it has been
dropped, which also lets callers use either free or put.
Fixes: 93d20d171c20 ("LU-11128 ptlrpc: new request vs disconnect race")
Fixes: 8e86156c34f3 ("LU-10486 osp: fix request leak on error in osp_sync")
Assisted-by: Claude:Opus-5 claude-code
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Change-Id: Iec07b093e380f262b452ff4fc5f5fac64f1c9fb0
LU-20050 ptlrpc: fix sepol_helper status decode
sepol_helper() declared ret as signed short while
call_usermodehelper() returns int, and decoded it with a single
"rc = ret>>8". That shift is only correct for a normal exit. A
kernel launch failure returns a negative errno, not a wait
status, and shifting collapses it (-ENOMEM becomes -EPERM). A
helper killed by a signal leaves the signal in the low 7 bits,
so ret>>8 is 0 and the failure is reported as success.
Widen ret to int and decode the three cases separately, with raw
bit tests since WIFEXITED/WEXITSTATUS are userspace macros:
ret < 0 -> kernel failed to launch the helper
(ret & 0x7f) == 0 -> normal exit (WIFEXITED)
(ret >> 8) & 0xff -> exit code (WEXITSTATUS)
The exit code must be sign-extended, not negated: l_getsepol
returns -errno whenever stdin is not a tty, always the case
under call_usermodehelper(), and exit() keeps only the low byte.
So -ENODEV arrives as exit byte 237, and (signed char)237 is
-19. sptlrpc_sepol_get() special-cases -ENODEV as "SELinux is
disabled, report nothing", so negating it made that branch
unreachable and turned those mounts into "Unknown error 237".
This extends 8a6788b508db.
A positive exit status is not an errno either, so negating it
invented plausible but wrong errors: 127 became EKEYEXPIRED and
126 ENOKEY, pointing at key management rather than a broken
helper. Report -EPROTO and log the raw status. sepol_helper()
can then no longer return a positive value, so drop the
unreachable rc > 0 case in sptlrpc_sepol_get().
The eight sptlrpc_sepol_get() error paths in mdc_locks.c,
mdc_reint.c and mdc_request.c leaked the request's import
reference and the LDLM locks gathered for early cancel: the
sepol check runs before the prep call, so the request still owns
both, but the error tail only called ptlrpc_request_free(). Keep
one exit per function and make it correct on both sides of the
prep call, through the new mdc_elc_req_put() helper, clearing
@count once the prep call owns the locks. This needs LU-20546,
which clears rq_import when packing fails so the reference is
dropped exactly once.
Add sanity-selinux test_21d: stub l_getsepol, mount with
send_sepol=-1, and check that exit 237 still mounts while 234
and a missing helper fail with the expected errno text. It
creates no nodemap on purpose -- with nodemap.<nm>.sepol set,
req_check_sepol() would refuse the -ENODEV connect for an
unrelated reason.
Suggested-by: Sebastien Buisson <sbuisson@ddn.com>
Test-Parameters: trivial testgroup=review-dne-selinux-ssk-part-1
Test-Parameters: trivial testgroup=review-dne-selinux-ssk-part-2
Fixes: c61168239eff ("LU-8955 sec: create new function sptlrpc_get_sepol()")
Fixes: 0a773f04b288 ("LU-8955 ptlrpc: manage SELinux policy info for metadata ops")
Assisted-by: Claude:Opus-5 claude-code
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Change-Id: If9312fce842fca133de21c4898cafd949ace6aa6
| failed enforced test | platform | detail | |
|---|---|---|---|
| review-dne-part-2 | RHEL 9.7 / x86_64 | ran 11 tests. 1 tests failed: sanity-lfsck. | session |
| review-dne-part-4 | RHEL 9.7 / x86_64 | ran 9 tests. 2 tests failed: sanity-quota, sanity-dom. | session |
| review-dne-part-5 | RHEL 9.7 / x86_64 | ran 5 tests. 1 tests failed: recovery-small. | session |
| review-dne-selinux-ssk-part-2 | RHEL 8.10 / x86_64 | ran 7 tests. 1 tests failed: sanity-sec. | session |
LU-20643 utils: clear stale lmd fields on reuse
get_lmd_info_fd() reuses one struct lov_user_mds_data for every object
and convert_lmd_statx() writes only the fields lstat(2) supplies, so
stx_attributes, stx_attributes_mask, stx_btime and the mask bits above
STATX_BASIC_STATS keep whatever was there before.
Two ways that is not this object's data. On the V1 ioctl path the
lstat_t is copied over the front of the record, so stx_attributes_mask
ends up holding st_atim.tv_sec -- never 0 in practice -- and stx_mask
picks up bits from st_nlink, which reads as STATX_BTIME on a directory
whose link count has 0x800 set. It does not reach as far as it looks:
lmd_st is 144 bytes against 272 for lmd_fid and lmd_stx, so it stops
at the end of stx_mtime and stx_rdev_major onwards is still the
previous conversion's. On the lstat fallback the buffer holds the
previous object's data outright, or the file name written in for the
ioctl.
So a walk that crosses onto a non-Lustre subtree can answer -btime and
the attribute predicates from a file name.
Clear what is about to be reused on both paths, with the same line:
everything up to lmd_lmm, which is what the record's own fields amount
to. That takes lmd_fid, lmd_flags, lmd_lmmsize and lmd_padding with
it -- nothing in lustre/utils reads lmd_fid today, but a short name
there passes fid_is_sane() as an IGIF, and llapi_get_lum_file_fd()
copies by lmd_lmmsize. On the V1 path it goes after the memmove, so
the layout it has just moved up is not touched. The glimpse path is
left alone: there the MDT's answer is still wanted.
Fixes: 11aa7f8704c4 ("LU-11367 som: integrate LSOM with lfs find")
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools
Change-Id: I6126e73bb120bd6a12a904563e13303d63e75c1c
LU-20119 dbg: test lustre_helper labeling fix for test_21a fortestonly patch validating the fix for the sanity-selinux test_21a failure on the DNE + RHEL 8.10 server + RHEL 10.0 client config, where the client mount fails because l_getsepol returns -EACCES (-25 before the errno fix below) from the sepol connect upcall. Root cause: the l_getsepol/lctl/l_getidentity/l_getauth helpers were left labeled bin_t instead of lustre_helper_exec_t, so the kernel execs them into the unprivileged kernel_generic_helper_t domain (via the base "kernel_t bin_t:process kernel_generic_helper_t" transition) rather than lustre_helper_t. That domain cannot read selinuxfs or the on-disk SELinux policy, so l_getsepol fails with EACCES. Two reasons the label was never applied: 1. lustre.spec.in %post loaded the policy module with "semodule -i" but never ran restorecon. semodule -i does not relabel already-installed files, and the binaries were labeled bin_t at install time (before the module existed). Add a restorecon of the four helpers to %post (the make-install path already does this, but it is skipped for staged/RPM builds where DESTDIR is set). 2. lustre_helpers.fc labeled only /usr/bin/*. On RHEL/Rocky 10.2+ the /usr/sbin=/usr/bin file-context equivalency makes that work, but on el8/el9 and el10.0/10.1 /usr/sbin is a real path with no such equivalency, so restorecon of the /usr/sbin binary finds no rule. List both /usr/bin and /usr/sbin. The sepol_helper() CERROR and the errlog() errno fix from the prior diagnostic patch are retained so that, if the mount still fails, the kernel log shows l_getsepol's real exit code. Not intended to land; validates the fix before it goes on the LU-20119 review. Test-Parameters: fortestonly Test-Parameters: livedebug mdscount=2 mdtcount=4 osscount=1 ostcount=8 clientcount=2 testlist=sanity-selinux clientdistro=el10.0 serverdistro=el8.10 env=ONLY="21a",SHARED_KEY=true clientselinux Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com> Change-Id: Ifd207da02862849c90500a40b4ff71dff5bc711b
| failed enforced test | platform | detail | |
|---|---|---|---|
| review-dne-part-2 | RHEL 9.7 / x86_64 | ran 11 tests. 1 tests failed: sanity-lfsck. | session |
| review-dne-part-5 | RHEL 9.7 / x86_64 | ran 5 tests. 1 tests failed: lustre-rsync-test. | session |
LU-20238 ec: branch-free byte-indexed scalar base path Replace the per-byte gf_mul() in the scalar *_base hot paths with a byte-indexed per-coefficient region table: tbl[b] = c * b in GF(2^8), built once per coefficient gf_region_mul(tbl, x) = tbl[x] (single load, inner loop) ec_encode_data_base() at k=6/p=2, gcc -O2: 0.79x at 256 B, 1.08x at 1 KiB, 1.32x at 64 KiB. gf_mul() alone is ~20% slower than the 64 KiB direct lookup it replaces, so regions below GF_REGION_TBL_MIN_LEN (1 KiB) keep a scalar fallback. Byte-indexed lookup (gf-nishida-16 "FullByte"-style) is also patent-clean: no PSHUFB, no nibble split, no 16-byte gftbl, so it is structurally distinct from the ISA-L SIMD pattern that maps onto US 8,683,296 claim 21. ec_base.c carried an unconditional "#define GF_LARGE_TABLES", so both ec.ko and libec.a compiled the 64 KiB gf_mul_table_base and the small-table branch was dead code. With that define and the inert ec_base_o_CPPFLAGS in ec/Makefile both gone nothing could set it, so the block is deleted outright: .rodata 65792 -> 1536, .text 2217 -> 3591 for the fallbacks. gf_vect_mul_init is renamed gf_vect_mul_init_base, with a wrapper in ec_base_aliases.c; its 64-bit path now selects on BITS_PER_LONG for kernel builds, since __WORDSIZE is glibc-only. No existing entry point changes behaviour. Validated on RHEL 10.1 (AVX2+GFNI): exhaustive gf_mul + gf_inv, k=6/p=2 round-trip KAT, and gfni_ec_test byte-identical parity. Test-Parameters: testlist=sanity-ec Test-Parameters: testlist=sanity env=ONLY=910 Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com> Change-Id: I905e365d145d0018cbcd2bbc919eb4e516fafe8e
| failed enforced test | platform | detail | |
|---|---|---|---|
| review-dne-selinux-ssk-part-2 | RHEL 8.10 / x86_64 | ran 5 tests. 1 tests failed: sanity-selinux. | session |
LU-20147 ptlrpc: module-level cache for SELinux policy upcall Replace the per-import call_usermodehelper model with a single module-level upcall_cache (hashsize=1, key=0) shared by all imports. The original LU-8955 design called l_getsepol via call_usermodehelper() on every RPC from every import independently. On RHEL/Rocky 10.1 this fails: the kernel_generic_helper_t SELinux domain is blocked from reading /sys/fs/selinux/policyvers and writing to debugfs, causing a 30-second timeout on every first RPC. Cache lifecycle and freshness: - upcall_cache.h: add struct sepol_cache_data and union member in upcall_cache_entry. - lustre_sec.h: remove ps_sepol and ps_sepol_checknext from struct ptlrpc_sec. - sec.c: add sptlrpc_sepol_cache (module-level upcall_cache), sptlrpc_sepol_get(), sptlrpc_sepol_do_upcall() (fires l_getsepol), sptlrpc_sepol_parse_downcall(), and sptlrpc_sepol_cache_update() for unsolicited push updates. send_sepol changed from static to extern. - The cache is initialised with entry_expire = S32_MAX so the framework never reaps entries by time; sptlrpc_sepol_get() drives freshness by stat()ing the policy directory on every call and calling upcall_cache_flush_one() if its mtime differs from the value carried by the last downcall. l_getsepol therefore runs only on mount or after an actual policy change. - send_sepol == 0 disables the feature entirely (no policy info on any RPC). Any non-zero value behaves identically; the legacy "send_sepol = N seconds" semantic is preserved as parameter-level backward compatibility but no longer affects runtime behaviour (with the cache in place, the LU-8955 reason for an N-second TTL doesn't apply). Backward-compatible wire format and invocation: - lustre_user.h: keep the pre-LU-20147 layout as struct sepol_downcall_data_v2 / SEPOL_DOWNCALL_MAGIC_V2 = 0x8b8bb843 alongside the new struct sepol_downcall_data / SEPOL_DOWNCALL_MAGIC = 0x8b8bb844. - sec_lproc.c sptlrpc_sepol_do_downcall() and sec.c sptlrpc_sepol_parse_downcall() accept both magics; only the new layout populates the policy-dir mtime tracking used by the freshness check. - l_getsepol gained -2 / --new-format: with -2 it writes the new layout, without -2 it writes the v2 layout. Default is v2 so a new userspace stays compatible with an old kernel. - sec.c sptlrpc_sepol_do_upcall() keeps the pre-LU-20147 argv shape "--obd_type X --obd_name Y" (so old l_getsepol still recognises it) and just appends "-2" as a new flag. X/Y are the type and name of any active OBD on the client (MGC preferred); the per-OBD srpc_sepol write handler funnels the result back through sptlrpc_sepol_do_downcall() into the module-level cache, so it doesn't matter which OBD is used. We invoke under UMH_WAIT_PROC and on non-zero exit (eg. old userspace that doesn't recognise "-2") retry without "-2". - sec_lproc.c: add module-level srpc_sepol write-only debugfs file at sptlrpc/srpc_sepol (LDEBUGFS_FOPS_WR_ONLY) so admin tools can feed the cache directly; per-OBD srpc_sepol kept for backward compatibility and used by the kernel upcall above. l_getsepol changes: - Replace file-scope static globals with per-call lgetsepol_ctx. - Add -k/--kernel flag: writes downcall to module-level path sptlrpc/srpc_sepol; includes policy dir path and mtime. - Add -2/--new-format flag (see above). - Add -w/--watch flag: inotify daemon mode for policy change detection (retained as alternative; community feedback welcome). - Add find_policy_with_highest_ver() helper used by the existing RHEL/Rocky 10.1 fix in LU-20045's stack; the helper itself is duplicated locally so this patch is self-contained against an older LU-20045 base. When LU-20045 lands on master this duplicate can be dropped. - Use fstatat(dirfd(dp), ...) in the policy version scan loop. Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com> Change-Id: Icf813c252b7359c3dc17d80405579b50b57ab025
LU-20084 osp: fix LDLM ns kobject leak in lwp_device_fini() The LWP teardown path does not call ldlm_namespace_free_prior() before client_obd_cleanup() -> ldlm_namespace_free_post(). This affects two paths: Normal teardown (LCFG_CLEANUP -> lwp_process_config -> lwp_disconnect -> lwp_device_fini): lwp_disconnect() calls ptlrpc_disconnect_import() and ptlrpc_invalidate_import() but never calls client_disconnect_export(), so ldlm_namespace_free_prior() is never reached. lwp_device_fini() then calls class_disconnect() and client_obd_cleanup() directly, skipping free_prior entirely. Failed-connect path (lwp_obd_connect failure -> out_dis: class_disconnect -> lpd_exp=NULL -> lwp_device_fini): the same lwp_device_fini() is reached with lpd_exp NULL, so class_disconnect is skipped and client_obd_cleanup() is again called without free_prior. Without ldlm_namespace_free_prior(), LDLM_NS_STOPPING is never set. If the pool recalc task runs concurrently with ldlm_namespace_free_post(), the race can corrupt the pool kobject cleanup path and cause ldlm_namespace_sysfs_unregister() to hang, stranding the namespace kobject under /sys/fs/lustre/ldlm/namespaces/. Fix by calling ldlm_namespace_free_prior() in lwp_device_fini() before client_obd_cleanup(). This is symmetric with lwp_setup() -> client_obd_setup() where the namespace is created, and covers all LWP teardown paths. The import is still valid at this point since obd_cleanup_client_import() runs inside client_obd_cleanup() afterwards. Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com> Change-Id: Ie9a290ca529d18b0f837669a53c0c06cda4f767f
| unique failing test | history |
|---|---|
| conf-sanity4@zfs:test_302 | NEW unique failure for this branch in the last 30 days, and was seen 0 times across 0 other branches 0 reviews |
LU-20637 llapi: name a device scan's objects
A scan of a target holds names and parent FIDs but no paths, so lfind
prints FIDs and a user has no way to turn them into pathnames. The
HLD names an Output Format for exactly this, beside the FID printing
that is already there.
Add llapi_scan_rec_path(), which uses the path a namespace walk
already put in the record and resolves the FID through a client mount
when there is none. lfind gets --fid2path MOUNT for it.
The cost is one ioctl per object, so it is the consumer's to ask for
rather than the scanner's to pay: it falls on the objects a search
kept, not on every object it saw. That is also why the HLD prices
pathname generation separately from the scan rate.
An object with no pathname is counted and reported at the end rather
than printed: the objects a target keeps for itself have none, their
FIDs being below FID_SEQ_NORMAL, so a search under --internal is
expected to report some. An OST data object does have a pathname --
its FID resolves through the OSC to the file that owns it, which
sanity test_154C asserts -- so a scan of an OST names that file once
per stripe it has on the target. A hardlinked object is printed once,
under its first name. A FID that cannot be looked up at all -- no
CAP_DAC_READ_SEARCH, which ll_fid2path() checks first -- is an error
and not a missing name, so a broken search cannot read as a complete
one.
Neither half of the resolution asks anything of the target being
scanned, which is what lets it name objects on a target that is out of
service -- and a ZFS target can only be scanned out of service, its
pool being exported.
An OST data object is named by the file that owns it, read from the
object's own trusted.fid. Its own FID is an OST-sequence one, which
__ll_fid2path() answers only by asking the OST holding it, through the
OSC: exactly the target the scan is reading. ff_parent is the owner's
MDT FID, and the stripe index filter_fid keeps in its f_ver is
dropped,
that field being an index and not a version. An object no file owns
yet -- precreated and never written, of which a target holds many --
carries no trusted.fid and is reported as having no name rather than
being chased through the OSC.
trusted.fid is not the only place that FID lives. An OST whose inodes
are 256 bytes or smaller has none at all: osd_xattr_set_pfid() packs
the parent into the LMA as a struct lustre_ost_attrs and removes the
separate xattr, saying so with LMAC_STRIPE_INFO. Reading only
trusted.fid there leaves every object on the target without an owner,
so --fid2path prints nothing and reports them all unnamed -- measured
on an OST formatted -I 256: 33 objects, no owner on any of them,
against four files whose paths it should have printed. mkfs.lustre
defaults to -I 512 now, but -I 256 is still accepted and older OSTs
carry it. The LMA buffer is already in hand for the classification,
so the fallback costs no second xattr, and ll_decode_filter_fid(8)
reads the same two places.
An MDT object is named from its trusted.link and a map of the target's
directories, FID to parent and name, built by a pass over the target
before the search. A pass of its own because objects arrive in inode
order, so an object is usually delivered before its ancestors are;
resolving inline would mean holding every match until the scan ended.
The pass asks for no layout, no SOM and no HSM, and a filesystem holds
far fewer directories than files. On an OST it stops at the first
object and leaves the map empty.
There is no fall back to the lookup once the map is in use. The map
is
used precisely when the target cannot answer, so one object it could
not place would stop the whole search there. Two things are nameless
in consequence and counted with the rest: an ownerless OST object, and
on DNE an object whose ancestors live on another MDT, the map covering
only the target scanned.
--paths is the same composition with no mount at all. --fid2path
still needs one, not to resolve anything but as the filesystem the
paths hang from and for the comparison below, and a filesystem whose
only MDT is the target being scanned has none to give -- which for a
single-MDT ZFS filesystem is every scan of its MDT, the pool having
to be exported. --paths prints filesystem-root-relative names
instead, asks nothing of any running service, and is refused on an
OST, whose objects are named by files that live on an MDT.
A mount whose own filesystem cannot be read is refused rather than
run unguarded: --fid2path was asked for explicitly, and the fallback
was the very outcome the comparison exists to prevent, another
filesystem's pathnames printed with a zero exit.
llapi_scan_rec_path.3 says the same thing to a consumer pairing that
call with llapi_scan_device() directly, because nothing in the
library can make the check for them: the call is handed a descriptor
and a record and never sees the target the record came from. lfind
compares the mount's fsname with the target's label; anyone else has
to do that too, or know by construction that the two agree.
A MOUNT of another filesystem is refused when the target is opened,
its fsname compared with the one the target's label names. The
comparison sits inside the scan's own open rather than in a probe
ahead of it: a second open is not free on every backend, and opening
a target twice buys nothing the one open cannot answer. It cannot be
caught later: FID sequences are not unique across filesystems.
FID_SEQ_ROOT and FID_SEQ_DOT_LUSTRE are constants, so the root of
every filesystem has the same FID, and normal sequences overlap
because each filesystem's controller starts from FID_SEQ_NORMAL
(LUSTRE_SEQ_SPACE_RANGE). fld_client_lookup() on the wrong mount
therefore finds a range and __ll_fid2path() answers with a pathname
from it, so the run would have printed another filesystem's paths as
though they were the target's and exited 0.
The mount point is resolved before use: a bare fsname, a
subdirectory and a trailing slash all open the same root, and none of
them names a file when prefixed to what fid2path answers.
conf-sanity test_302 stops an OST, exports its pool where the backend
is
ZFS, and compares the names a scan of it gives against what the client
saw before it went down -- a scan that resolved nothing and one that
resolved everything both exit 0, so the set is what is asserted. Its
files are written rather than merely created, a precreated object
being
rightly nameless.
conf-sanity test_301 diffs the resolved paths against what the client
sees, and checks that a hardlinked object is named once. It mounts a
client on the MDS for the duration, because --fid2path resolves
through a mount on the node running the scan and a server is not a
client; a configuration that cannot mount one skips. It is ldiskfs
only: reading a target in service is what ZFS refuses.
scan_device_run()'s comment says what a second open costs rather than
calling it a deadlock, because it is not one: llapi_find_device()
builds the directory map in a pass of its own, so every --fid2path and
--paths
run already opens and closes the target twice in one process, and
conf-sanity test_302 does exactly that against a stopped OST with its
pool exported and passes on conf-sanity4@zfs. The reason to test the
fsname inside the scan's own open is the cost of the second import and
export, not a hang. The wasted pass on an OST -- where
scan_dirmap_cb() stops at the first object -- is left as it is;
folding the map into the search's own open would serve both from one
open and is worth doing separately.
The resolve-failure message names the FID that was looked up and the
mount only when there is one. An OST object is resolved by its
owner's FID rather than its own, so the message named the wrong one;
and under --paths there is no mount at all, yet -ERANGE and -ELOOP
still arrive here and reported a failure "through the mount given".
lfind(8) says what --fid2path costs, which differs by target and is
easy to state as one number and be wrong. One lookup per kept object
is the OST's cost; on an MDT there is no lookup and a whole extra pass
instead, to build the map the page describes four blocks further down
-- the more expensive of the two, and paid before the search rather
than per match. Where a name cannot be composed the page points at
lfs-fid2path(1), which is a tool that exists, rather than at a
lookup-only mode of this one, which is not.
--paths and --fid2path together goes through the same exit as every
other error in that function. Returning CMD_HELP from main() exits 4
and prints no usage, where "ret = CMD_HELP; goto out" prints it and
exits EXIT_FAILURE.
fp_paths takes fp_unused_bit1. struct find_param keeps those bits for
exactly this, and a trailing byte would have cost 8 with padding.
The directory map's names stay inline. At NAME_MAX + 1 an entry is
~288 bytes, so a million directories is ~288 MB and 1.5x that while
scan_dirmap_grow() holds both tables, and interning them in an arena
would cut it several-fold. Left for now: it is a change to the map's
memory layout for a case no measurement here has reached, and a
calloc() failure already fails the search cleanly rather than
corrupting it. Worth revisiting when --paths is pointed at a target
big enough to need it.
conf-sanity test_303 covers --paths, where 301 and
302 both exercise only --fid2path, so the composition --paths exists
for was never run. It scans an MDT in service with no mount given,
which is the case a filesystem whose only MDT is the target has to be
named in, and asserts the answer is the client's set: one path per
object rather than one per name, the hardlinked object once, no FID
where a path was asked for, and no mount point in the answer -- root-
relative being what separates it from --fid2path, and a mounted prefix
meaning the wrong composer ran. It also asserts the two refusals:
--paths on an OST, whose objects are named by files that live on an
MDT, and --paths together with --fid2path. ldiskfs only, an imported
ZFS pool answering EBUSY by design.
lfsp_fsname makes the check a public caller can ask for.
llapi_scan_rec_path.3 tells one they must establish that the mount and
the target are the same filesystem, and nothing in
<lustre/lustreapi.h> let them: scan_device_run() is internal,
llapi_scan_device() passed want_fsname NULL, and the target's label
lives in a private header. The field is the same machinery under a
public name -- the scan refuses a target whose label names another
filesystem with -EXDEV -- and it has to be the scan that does it,
because only the scan reads the label and a lookup on the wrong mount
succeeds and answers with that filesystem's pathname.
The usage agrees with its own option list. "The output is a
FID because a target has no paths" sat two lines below --paths and
--fid2path, both of which print pathnames; it now says the output is a
FID unless one of them is given.
test_301 and test_303 sync the MDS, not only the client.
lustre_super_operations has no .sync_fs, so a client sync writes back
page-cache data and leaves the MDT's ldiskfs to commit on its own
schedule -- and both tests scan that ldiskfs directly while it is in
service, so an object still only in memory is one the scan cannot see
and the comparison against the client's answer is a race. test_302
needs none of this: its target is stopped, which flushes it.
test_302 also tells the scan where the OST's pool devices are. An
exported pool is found by reading vdev labels under a search path,
which defaults to /dev, and the test framework's ZFS vdevs are files
in $TMP -- so without --search the pool is not found at all and the
test failed on every ZFS configuration with ENOENT. On ldiskfs the
argument is empty and nothing changes.
test_301's one bare lfs becomes $LFS, as every other call in the file
is. An interop run that points LFS at another binary would have used
whatever PATH found.
The EXAMPLES fragment in llapi_scan_rec_path.3 escapes its newline as
n, not n: troff reads the single form as a register reference,
which rendered the line as printf("%s0, path). llapi_fid_parse.3
escapes it the same way.
The three places that said a target scan needs no client now say what
--fid2path changes. lfind.c's file header and llapi_find_device()'s
block comment both stated it flatly, and llapi_find_device.3
documented neither fp_fid2path_mnt nor the -ENAMETOOLONG an over-long
mount path answers with; its SEE ALSO gains llapi_scan_rec_path(3),
which is the call that does the resolving.
llapi_scan_rec_path.3's NOTES point a consumer at lfsp_fsname rather
than telling it to compare the mount and the target itself.
lfsp_fsname is that comparison -- the scan refuses a target of another
filesystem with -EXDEV -- and saying otherwise on the page that
introduces it would send a caller to write what the library already
does.
test_303's one bare lfs becomes $LFS, as test_301's did. It was added
after that fix and reintroduced it.
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools
Change-Id: I6476abfd28c91b2ccc04fb08c4e5860aef8fe009
| unique failing test | history |
|---|---|
| sanity2@ldiskfs+DNE:test_157d | seen in 1 other review |
| sanity2@zfs:test_157d | NEW unique failure for this branch in the last 30 days, and was seen 0 times across 0 other branches 0 reviews |
LU-20649 llapi: a changelog as an Object Stream The two scanners we have enumerate what exists. A changelog enumerates what happened, and llapi_scan_changelog() delivers it in the same record, so a consumer written against llapi_scan_namespace() or llapi_scan_device() can be fed by a delta without being rewritten. A changelog record carries the FID, the parent FID, the name, the event and its time, and -- where the server recorded them -- the uid and gid of whoever caused it. That pair is lfsr_event_uid and lfsr_event_gid, not lfsr_uid: mdd_changelog_ns_store() fills the uidgid extension from lu_ucred(env), the acting client's credentials, so it says who acted and not who owns the object, which is what lfsr_uid means in every scanner that fills it and what lfs find -uid matches. Delivering it as lfsr_uid would have answered "-uid alice" from the identity of whoever last touched her file. The record carries no size, mode, owner, times, layout or HSM state at any setting, so those arrive absent with their lfsr_valid bit clear. With LLAPI_SCAN_CL_F_RESOLVE they are filled by opening the object through a client mount, which is one open and one stat each, or two of each for a regular file whose size was asked for; the demand mask decides whether that happens at all, so a consumer reading only what an event carries pays nothing. Resolution fills what the stream lacks and never restates what it has, which is what lets a caller ask what an event recorded rather than what is true now. Two modes. Event mode delivers one record per event, in order. Object mode holds events in a FID-keyed cache and delivers one record per object once it has been quiet, so a file written a hundred times is one record and one lookup rather than a hundred of each. Clearing is off by default and never runs ahead of the callback: it is irreversible and it purges the registered user's whole backlog, not this reader's alone. Registering that user stays the caller's, since registering without consuming is how an MDT fills up. The record gains lfsr_event_type, _flags, _time, _index and _prev, the rename source, and a job id, behind three new validity bits. An event time is not an object time, so it does not land in lfsr_mtime: a search asking for one must not be handed the other. Only an event that implies a type answers for it -- the three creations, and CL_RMDIR, whose target is a directory by definition -- so the mode and its validity bit are set together, and a later event leaves both alone. Setting them as a pair is what keeps "cannot answer" apart from "the answer is zero": a mode written on every event, with the bit only ever OR'd in, would leave a coalesced object claiming a type while carrying zero for it. An object's type does not change, so the first answer stands for every event after it. lfsr_parent_fd and lfsr_fd are set to -1 rather than left as the memset found them. Zero is stdin, and a consumer written against llapi_scan_namespace() is told to test lfsr_parent_fd and then fstatat() on it. The Changelog reader is finished on every path out, the error one included. llapi_changelog_start_user() opens the device and sets *priv before it issues OBD_IOC_CHANGELOG_FILTER, so a failure there -- an unregistered user, an unreachable MDT -- hands back an error with the reader live, and the fd, its buffer and the kernel-side reader state would stay pinned for the life of the process if nothing closed them. sc_user, sc_mnt and sc_stats are read from the bounded copy rather than the caller's structure, and the advertised minimum sc_size reaches through sc_mdtname. A minimum stopping short of it would let a caller declare a structure without the field the call cannot work without, and reading that field off the caller's own memory would go past its end before the size check had a chance to refuse it. Clearing stops at the oldest record any cached object still needs, not at the last index delivered. Coalescing puts those apart: create B at 10, create A at 11, touch B at 1000, and A can age out and be accepted while B is still held with a record at 10. Clearing to 11 there and then stopping the scan loses B entirely -- no callback saw it, and a restart from sc_startrec cannot read it back, because the record is gone. An event with no target object is delivered per event even in object mode. mdd_changelog_ns_store() zeroes cr_tfid when its target is NULL, which mdd_rename() passes whenever the destination name did not already exist, and cr_markerflags -- a CL_MARK's union with the same bytes -- is not a FID either. Coalescing keys on the FID, so all of them shared one cache entry and only the last survived: an ordinary `mv a b` was enough to lose the one before it, and with clearing on, the record it came from is gone as well. The parent FID is taken only from an event that carries one. CL_CLOSE does not -- mdd_changelog_data_store() is called with pfid NULL -- so assigning it from every event while only ever OR-ing the bit in left create+write+close reporting LLAPI_SCAN_PARENT over a zero. A name or job id whose copy failed leaves its bit clear for the same reason: a bit that promises a string must not be set over a NULL. A resolve is skipped only for a demand mask every event can answer, which LLAPI_SCAN_TYPE is not: scan_cl_mode() answers it for the four record types that imply one and no others, so counting it there would leave a consumer asking for FID and type with neither a lookup nor a type on a CL_CLOSE. Resolution opens O_PATH and takes a real open only for the regular file whose size it came for. llite gives a special file init_special_inode(), so opening every object for read opened the driver behind a device node stored in the filesystem; O_NONBLOCK holds off the wait, not the rest of what an open does. The uidgid extension is asked for when sc_want is 0, and that case is tested on its own rather than by a mask. Zero means "everything", so it is the caller that most needs the extension -- and `0 & ~LLAPI_SCAN_EVENT` is 0, so any test of that shape would exclude precisely the caller that wanted it, leaving LLAPI_SCAN_EVENT_UID out of lfsr_valid. A size is reported strict only where the OSTs were actually glimpsed. The second open runs for a regular file whose demand mask names a size and can still fail, so a consumer asking for an mtime alone reaches the fill on the O_PATH descriptor; that is the MDT's lazy answer, and LLAPI_SCAN_LAZY_SIZE and LLAPI_SCAN_LAZY_BLOCKS are what say so. liblustreapi_pfind.c maps the two to OBD_MD_FLSIZE and OBD_MD_FLLAZYSIZE, so they are not interchangeable. LLAPI_SCAN_CL_F_RESOLVE is one open and one fstat, so the page says which six of the ten fields it fills. The project id, the layout, the directory stripe and the HSM state each cost an ioctl or an xattr of their own and are not filled at any setting; the page said otherwise by listing all ten and then naming the flag. The checkpoint gets its caveat, in the header and the page both. Under coalescing a record naming no object goes out as it arrives while an object whose earliest record sits lower is still held, so the delivered indexes are not a watermark. A run that reaches the end of the log flushes what it holds and the highest index is safe; a run the consumer stopped drops it, so that run must leave the checkpoint alone. sc_startrec and sc_endrec are both inclusive and the page now says so, the example resuming from the saved index plus one rather than re-delivering the record the job already processed. lfsr_event_flags is cr_flags & CLF_FLAGMASK, so CLF_RENAME, CLF_JOBID and CLF_EXTRA_FLAGS are masked off -- they say which extensions a record carries, not what happened. Three smaller things. The second fstat() is taken only when the glimpse replaced the descriptor. scan_cl_bucket() drops a modulo that cannot change llapi_fid_hash()'s value, that being hash_long() on the same shift. The sc_mdtname test after the memcpy goes: the minimum sc_size reaches through that field and the caller's own struct was tested at the top, so it could never fire. Four comments described an earlier revision of this patch rather than the code and now describe the code. The COALESCE flag says which event's fields the one record carries. It promised one record per object and left open which of the object's events that record spoke for, so a consumer matching on lfsr_name had no way to know it sees one name per object rather than every name the log recorded -- an object created under one name and hardlinked under another arrives carrying only the last of them. lfs find --changelog -name is that consumer, and the gap showed there first. sc_type_mask without sc_user is refused. The mask reaches the server only through llapi_changelog_start_user(), which needs a registered user to filter for; on the other branch llapi_changelog_start() never sees it, so a caller asking for CL_UNLINK alone read every event type and nothing said so. Refused rather than applied client-side, because that is a different thing -- the records still cross the wire -- and sc_filter is already the client-side spelling. The man page said only that the mask is intersected with the user's own, which does not tell a caller what happens when there is no user. llapi_scan_changelog_test covers the five things the shape of this stream turns on: that a record describes the event it came from, that object mode collapses a burst into one record, that the demand mask keeps a lookup from happening at all, that a consumer's own return value comes back unchanged, and that bad parameters are refused rather than crashed on. It takes -m for the MDT, -u for a registered user and -d for a client mount, since three of those need a live log. Nothing in lustre/tests drives it yet, unlike its two siblings, which land their suite case with the scanner: that case wants a changelog registered and cleared around it and a skip where the MDT records nothing, and is left for its own patch rather than grown here. sanity 157d runs it, so the binary is not merely built. Its events have to be on the MDT whose log it reads, which under DNE a mkdir below a striped root does not guarantee, so the test makes the directory on MDT0 itself rather than leaving the binary to notice and refuse. -u passes the registered Changelog user the clearing cases need; without one they skip themselves. Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com> Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools Change-Id: I12efd3bd2150592a4507533129ab56573ef924d9
LU-20650 llapi: fill a scan record for one FID llapi_scan_namespace() walks what exists, llapi_scan_device() reads what exists on a target, and llapi_scan_changelog() reads what happened. A consumer holding a FID had no way in, so the changelog module resolved privately and with fstat() alone: mode, nlink, uid, gid, three times, size and blocks, and nothing else. A record filled that way answers -size and -mtime but not -stripe-count, --hsm-state or -projid, so "lfs find --since -stripe-count 4" would answer differently from a plain find on the same filesystem. That is a defect rather than a difference of provenance: --since promises what is true now, verified through the mount. This patch adds the call; the search moves onto it in "lfs: find --changelog, the log as source", which is where that example starts answering. The default demand mask is the walk's, so --hsm-state and -projid still want naming in lfsp_want; what the call fixes without being asked is the layout, which is what the example above turns on. llapi_scan_fid() is the fourth way into the record and fills it the way the walk does, because it is the walk's own gather. It resolves the FID to a pathname and gathers there rather than gathering from a descriptor opened by FID: scan_rec_gather() is path-oriented throughout -- get_lmd_info_fd() wants a parent descriptor and a name, get_projid() opens by path, and the HSM and MDT-index branches open the path themselves -- so the alternative is an fd-shaped twin of every one of those, which is the second copy of the knowledge LU-20611 existed to remove. Resolution is not an added cost on the caller that wants this: --since prints pathnames and prefix-matches them for its subtree restriction, so it needs the pathname anyway. What that costs is stated rather than buried: per object one fid2path, one type-only statx and one open of the object or its parent, and CAP_DAC_READ_SEARCH unless llite.*.user_fid2path is set. The statx asks for STATX_TYPE alone deliberately: anything touching size, blocks or mtime keeps need_glimpse set in ll_getattr_dentry() and buys an OST glimpse per regular file that the gather then discards. An object unlinked since the caller learned of it no longer resolves and comes back -ENOENT, which for a consumer asking what is true now is the answer and not an error. The call fills a fresh record and never reads one in. A consumer merging it into a record it already holds decides which fields it lets this one supply, which is what will keep --changelog --resolve filling only what the stream lacks. A filter that returns a positive value skips the object, as it does for the other three scanners, and the call returns 0 rather than handing that value back to a caller testing `if (rc)`. The gather runs against a pathname, so the FID it found is checked against the one asked for and a name unlinked and recreated underneath it answers -ESTALE rather than a record for a different object. Only lfsp_want and lfsp_filter are read from the parameter block. lfsp_max_depth, lfsp_stats, lfsp_search and lfsp_fsname describe a traversal there is none of and are ignored; lfsp_thread_count above 1 is refused rather than ignored, because a caller setting it has misunderstood the call. It widens a lfsp_want of 0 through LLAPI_SCAN_WANT_DEFAULT, the definition llapi_scan_namespace() already uses, so the two mount-side entry points mean the same thing by 0. A trailing slash on mnt_path is trimmed before the pathname is composed. The leading slashes of fid2path's answer were already stripped, but "/mnt/lustre/" with the root FID composed "/mnt/lustre/" and left lfsr_name on the terminating NUL -- which lustreapi.h says means NULL, not empty -- and every other FID got a doubled separator in the published lfsr_path. The kernel-doc no longer lists lfsp_flags among the fields that are ignored. Every flag it defines is, but a bit outside them is refused with -EINVAL, as the code does and llapi_scan_fid.3 already says. llapi_find_device() clears lfsp_got. scan_param_copyin() copies the caller's block whole, so the caller's pointer came through and was answered with find's own demand mask -- a mask for a question the caller never asked. The field's kernel-doc is rewritten with it, to say that it is per scan rather than per library and why it is reached through a pointer. The syscalls go through mnt_fd, not through the composed pathname. The FID was already resolved through that descriptor; the statx, the object's open and the parent's now use it too, at the name fid2path answered, so mnt_fd alone says which filesystem is read. An fd pins its mount where a path string is resolved afresh every time, and a mount replaced between the open and the scan would otherwise have the resolve read one filesystem and the gather another, with rc 0 and nothing said. It also takes the mount prefix off every lookup, three per object, which over a changelog's worth of FIDs is what this entry point is for. mnt_fd has to be the mount root now. The ioctl behind llapi_fid2path_at() takes any descriptor in the filesystem, and the page always named llapi_root_path_open(), which returns the root; a caller that passed something else answered before and answers -ENOENT now. mnt_path stays required and stays what lfsr_path is built from, so a wrong one still mislabels the answer -- it just no longer decides what was read. llapi_scan_test gains a case for it, so sanity 157c runs it: one record for a known FID, checked against the pathname and the basename the client sees; a filter that skips, which is a success with nothing delivered; the argument refusals, including the -EBADF that a negative mnt_fd has to answer rather than the -EINVAL that also means "this FID has no name"; and a FID that resolves to nothing, which must fail and deliver no record whichever way it fails. Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com> Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools Change-Id: I796db84d3fe596b1cfa93658dcfeb240db8e33a5
LU-20650 lfs: find --since, from the changelog The three pieces were already here. llapi_scan_changelog() says which objects had an event, llapi_scan_fid() fills each one's record the way a walk fills it, and find_decide() judges it with the predicates a walk uses. --since composes them, so it answers over fewer objects and judges each the same way: the objects are a strict subset, narrowed and never different. The lines are not a subset of the lines -- a walk reaches a hardlinked file once per directory entry and prints it once per link, while --since reaches it once per FID and prints it once, under the first of its names that is under the root and matches. No new predicates and no second copy of the vocabulary. The argument is never guessed. A bare integer is an index, which is what "lfs changelog MDT <startrec>" already takes; a single-unit duration (30m, 2h, 7d) is that far back from now; a timestamp or @<seconds> is that time; anything else is an error naming the four spellings. A leading sign is refused rather than wrapped through strtoull(), and a duration reaching past the epoch is a typo rather than a window. cr_index belongs to one MDT, so a bare index is refused on a filesystem with more than one and the message says which time spellings do work. That count has to be right, and the obvious source is not: LL_IOC_GETOBDCOUNT answers with the size of the LMV's target array, so a single-MDT filesystem reports 64. It is used as an upper bound and each index confirmed with llapi_search_tgt() before its changelog is read. A changelog covers the whole filesystem, so the subtree the caller named is enforced against the pathname llapi_scan_fid() resolved, and the depth the walk would have counted is recovered from it for --maxdepth and --mindepth. Without that, a find rooted at one directory would answer for its siblings. Against every name the object has, not just the resolved one. llapi_scan_fid() resolves linkno 0, so a hardlinked file arrives under whichever name fid2path answered with first: with /a/f and /b/f the same inode, "lfs find /a --since 2h" would be handed b/f, reject it, and lose a file a walk of /a prints. -name has the same shape, which is why a target scan walks every linkea name. The names come from fid2path here, one ioctl each, and only a file whose link count says it has another name pays for any of them. A time anchor is cut on the record rather than at the start index, because a time cannot be turned into a per-MDT index without reading the log; coalescing has already reduced an object to its latest event, so an object whose latest event is older than the window did not change in it. An object unlinked between its event and now cannot be verified, so it is not in the answer -- that is what the flag gives up -- and the run says how many were dropped that way. --since is refused for a target scan rather than ignored. The option table is compiled into lfind as well, so `lfind --local --since 2h` parsed and then scanned the whole target: llapi_find_device() has no changelog to anchor on and never reads fp_since_kind. It joins the list find_device_supported() already keeps for --maxdepth, --threads and -printf. llapi_find_since() refuses fp_thread_count above 1 for itself, before it looks at anything else: this source reads one MDT's changelog at a time and has no work to divide. That is why lfs_find() now computes the default thread count on the walk's arm of the dispatch rather than above it -- computed for every search, the default alone was above 1 and the refusal fired on every --since run, which is not a setting the caller made. The mount point is found from the resolved root and not from the spelling, which are the same string only when the caller already gave a canonical absolute path. /home/user/data -> /mnt/lustre/data answered -ENODEV, llapi_search_mounts() matching mnt_dir literally, and a relative path went through the fsname to the *first* mount of that filesystem -- so under $MOUNT2 every object fell outside the subtree and the search printed nothing and exited 0. What is printed is then the caller's spelling of that root with the resolved remainder appended, because a walk prints the path as it was given -- param_callback() copies the argument and llapi_semantic_traverse() appends to it. Named from the mount every time, "lfs find . --since 2h" would answer /mnt/lustre/sub/f where "lfs find ." answers ./sub/f, and --since promises the same answer as the search without it. ONLY=56 is the regression check that the shared paths still answer as they did; the subtests for the new options arrive with the tests patch at the end of this series. Two things the page is careful about. An MDT with no registered user does not record at all, rather than recording and purging: mdd_changelog_off() clears CLM_ON when the last user deregisters and mdd_changelog_enabled() requires it. And the usage string spells the argument TIME|INDEX rather than TIME, because a bare integer is a changelog index -- the one distinction the parser goes out of its way to make, and the one a reader is most likely to get wrong. Test-Parameters: testlist=sanity env=ONLY=56 Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com> Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools Change-Id: I13426ef1ed12cd08d78aa5a5da9f8ba4465690d2
LU-20650 lfs: find --changelog, the log as source
--since treats the changelog as a candidate set and verifies each hit
against the live object, so it answers as it is now. --changelog
replaces the source: it reads the log and nothing else, and answers as
it was recorded. That is why -name here matches the name in the event
rather than the object's current one, and why an object unlinked since
the event is in this answer and cannot be in --since's.
It takes an MDT name or "all", required rather than optional because
getopt_long() accepts an optional argument only as --changelog=VALUE.
One named MDT is also what makes a bare --since index unambiguous, the
index being paired with the log it came from.
Every predicate a record cannot answer is refused naming itself,
rather than answered from a zero. Without --resolve that is -size,
-blocks, -perm, -links, --attrs, --xattr, the layout options, -projid,
-printf, --mdt-count and --mdt-hash; --xattr was the loud one,
reaching llistxattr(NULL, ...) once per object, and the rest were
quiet and worse, "--perm 644" matching nothing and its negation
everything. -uid and -gid are refused for a different reason and at
every setting short of --resolve: a changelog records who caused an
event, from the acting client's credentials, which is not who owns the
object. An object time is refused outright, a record carrying an
event time and the two being different questions; the message says
--since 1d is how to ask. Only --mdt, --maxdepth and --mindepth are
refused at every setting -- they describe a walk, or need something a
per-MDT log does not have. --ost and the object times are refused
without --resolve alone, the lookup supplying both.
-type is the one field with a third answer. A record says what kind
of object was created and only for four record types, so an object
whose event does not carry one is counted undecided rather than
compared against a mode of zero, which reads as "not a file" and "not
a directory" at once.
Output is a pathname where the FID still resolves to one and the FID
where it does not, which is the whole point of asking: an object
unlinked since its event is the case --changelog exists for, and
counting it as "no pathname" the way a target scan counts its own
objects would drop exactly that case in silence. The count of those
is reported at the end, so a caller knows some lines are FIDs.
The path argument names the filesystem, not a subtree, a changelog
knowing nothing about where an object sits; a path below the mount is
refused unless --resolve makes a pathname to match against.
--resolve looks each object up through the mount and hands
find_decide() what it found -- the path, the parent descriptor and the
object's own -- so -size glimpses the OSTs, -projid opens through a
descriptor of ours, and -printf gathers as a walk gathers. It fills
what the stream lacks and never restates what it has: the merge starts
from the looked-up record and puts every field the event answered for
back over it, taking only the type bits of the mode, so -perm reads
the live one. The subtree is tested on that pathname, over every name
the object has rather than the one linkno 0 answered with.
An object that has gone between its event and the lookup keeps the
answer honest in the other direction. It is still in this answer --
resolution fills and never removes -- but only where the record can
carry the question: where the search asks something only a lookup
supplies, or names a subtree the object can no longer be placed in, it
is counted undecided instead.
find_device_supported() refuses --changelog for the same reason it
refuses --since: the option table is compiled into lfind too, so it
parses there, and llapi_find_device() never reads it -- `lfind
--changelog all` would otherwise have run the target scan, printed
everything the other predicates kept, and exited 0.
-name joins -type as a predicate the record cannot always answer.
cr_namelen is 0 for an event on a FID whose name the log never held,
so CL_CLOSE, CL_SETATTR, CL_TRUNC and CL_LAYOUT all
arrive nameless -- a file created before the window and written inside
it is the ordinary case. Matching those against "" decided them both
ways and said nothing about it, and is not even reliably a non-match,
fnmatch("*", "") succeeding. They are counted undecided instead.
The first resolve failure that is not "this object has no name"
decides the exit status, as it already does for a target scan, so a
run that could resolve nothing cannot report itself complete.
fc_d is set only for a directory, as find_since_rec_cb() already had
it. printf_format_lustre() reads d == -1 as "not a directory", and
sr_fd is not always -1 for a regular file: under -printf the gather
widens, scan_rec_gather() opens the object for the MDT index and
leaves that descriptor in sr_fd. Passing it through sent every
striped file down the directory branch and onto an lmv buffer
find_rec_to_lmv() had just zeroed, so %Lc and %Li printed 0, %Lo
[0] and %Lp nothing.
-name is answered against the recorded name under --changelog.
find_since_pick() walks the object's other links to place it in the
subtree, and was substituting each link's basename into the record
before testing: right for --since, which asks what the object is now,
and wrong here, where the record is the answer. The alternate path
still settles the subtree; only the name stays as recorded.
The pathname a match prints goes through the caller's spelling
whichever way it was composed. Without --resolve it comes from
llapi_scan_rec_path(), which builds an absolute name from the mount,
so "lfs find ." answered /mnt/lustre/sub/f there and ./sub/f
everywhere else. find_since_respell() splits into a find_respell()
taking the two roots, and the print site uses it.
The demand mask -printf widens is the one handed to llapi_scan_fid():
find_want() builds fss_want with the -printf flag set and the resolve
passes it as sp_want. llapi_scan_changelog()'s own sc_want is a
separate literal and does not depend on -printf. What the two sources
share is fss_want, which is why --since and --changelog widen
together.
The comment above it claimed %s, %b and %LP as the reason; those come
from find_decide()'s own stat and find_get_projid(), gated on
fc_gather_all, and find_want() clears LLAPI_SCAN_PROJID on its way
out regardless. What the widening actually buys is LLAPI_SCAN_LMV,
for %Lc and its neighbours on a directory.
The cookie is not mentioned any more. fp_since_cookie arrives with
--since-cookie in the next patch, and this one documented it in
llapi_find_since()'s Return: block and in llapi_find_since.3 --
an -EINVAL for giving both an anchor and a cookie, and an -ESTALE,
neither of which exists here. -ENOTSUP does exist and was missing
from both.
A CL_MARK is dropped rather than delivered.
mdd_changelog_write_header() writes the marker flags into the union
with cr_tfid, so cr_tfid.f_seq would read back as CLM_ON|CLM_START,
0x10001 -- and fid_seq_is_igif() accepts anything in [12, 0xffffffff],
so the FID gate would pass a value that is not a FID and find_decide()
would print it as a FID with no pathname.
No such record can arrive today, so this is a guard and not a fix for
an observed answer: CHANGELOG_MINMASK is BIT(CL_MARK) and
mdd_changelog_mask_seq_write() hands it to cfs_str2mask() as the
floor, so CL_MARK cannot be masked out -- and
mdd_changelog_write_header() returns early whenever CL_MARK is in the
current mask, so no marker record is ever written. Asking for "creat
mkdir" gives back "MARK CREAT MKDIR", and deregistering every user and
re-registering leaves an empty log. The guard costs one test per
record and stops the flags being read as a FID if that ever changes.
It sits at the top of find_since_cand_cb(), above the FID gate. The
anchor is not taken there but in find_since_filter_cb(), which runs
first on every record the log delivered, marks included: it has to see
them, or it lags the end of the log by whatever trailing run of them
the scan finished on.
-printf's widening now reaches --since. find_want() clears
LLAPI_SCAN_PROJID on its way out and find_decide() gates
find_get_projid() and the OST glimpse on fc_gather_all rather than on
the demand mask, so a flag this patch sets for --changelog and not for
--since meant "--since -printf %LP" answered DEFAULT_PROJID for every
object where a walk answered the real one, and %s/%b answered from the
MDT instead of a glimpse.
The comment on the nameless case said cr_namelen is 0 for a MARK. It
is not: mdd_changelog_write_header() sets it to strlen(obd->obd_name)
and copies the device name in, so a mark arrives named "lustre-
MDT0000" -- which is also why "-name 'lustre-MDT*'" matched one before
the record was dropped above. A data event on a FID whose name the
log never held is the real case, and the sentence now says only that.
The undecided warning no longer claims a missing field.
find_since_cand_cb() also counts an object undecided when it has gone
and the search needed a lookup or named a subtree, and there the
record answered -- the object is what is missing. "could not be
decided from their changelog record" covers both.
LLAPI_SCAN_CL_F_ONCE is set for --since and not for --changelog. Once
means once per scan, which is what "as it is now" wants; a source
answering as it was recorded has a second burst to report, so an
object touched in two of them prints twice. The man page says so.
The dropped-objects warning names what the count is of -- "%llu of
%llu changed objects could not be read" -- rather than leaving a bare
number beside the errno it belongs to.
Test-Parameters: testlist=sanity env=ONLY=56
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools
Change-Id: If9ead118b5fc1dee86cfac6646106e982841d8e2
LU-20650 lfs: find --since-cookie, per-MDT anchor cr_index belongs to one MDT, so "where I got to" on a DNE filesystem is one number per MDT and not one number. --since <time> is the human spelling and means the same thing everywhere, but only as far as the clocks agree; a cookie is exact on every MDT, and is what a repeated job should use. The file is a line per MDT, "<mdtname> <index>", read as the anchors and rewritten with the indexes this run reached. It is written whole and renamed over, so an interrupted run leaves the previous anchors rather than half of the new ones -- a half-written cookie is worse than a stale one, because the stale one is caught. Which is the reason a cookie is worth having. If an MDT has purged past its anchor the records in between are gone, and reading from the oldest one left would return a short answer with nothing to say so. So the oldest surviving index is read first, by a one-record scan that stops at the first callback, and an anchor older than it is refused naming both numbers. Every MDT is checked before any of them is scanned: refusing from inside the scan would leave the earlier MDTs' matches on stdout with no cookie written, and the next run would repeat them. An MDT with no anchor is not probed at all -- 0 has nothing to be stale against, and that is every MDT of a first run. Zero is reserved for "the log is empty", which is why a record that arrives without an index is reported rather than recorded as index 0. Changelog indexes start at 1, so storing 0 for a record that has one would make a log holding records look like a log holding none, and 0 compares false against every anchor -- the check would go on being skipped and say nothing about it. An empty log does proceed: from a client, "nothing happened since the anchor" and "everything since it was purged" look the same, and refusing on that would fail on every idle filesystem. A cookie from another filesystem would anchor this one at indexes that mean nothing here, so a line whose name does not match the filesystem is ignored, and a file whose header names another filesystem is refused. Ignoring every one of its lines instead would leave no anchors at all, rescan this filesystem's whole log, and then drop the other one's anchors in the rewrite -- three wrong answers, none of them said out loud. The rest of the file is input the caller does not control either. The scanf width is not optional: %s into a fixed buffer is how such a file overruns it. Nor is the type of the bound test, since %x accepts a sign: "<fsname>-MDT-1" reads back as 0xffffffff, which passes a test made against an int and then indexes the array far past its end. So the sign is refused at the parse and the bound is compared against the value that does the indexing. --since and --since-cookie are two ways to say where to start, so giving both is refused rather than one of them silently winning. More than one path is refused for a related reason: lfs find runs the search once per path and every run advances the anchors, so the second path started past the end of the log, printed nothing, and left the records it should have matched behind the anchor for good. The cookie is proved writable in the same pre-pass that proves the anchors are not stale, and for the same reason -- a cookie that cannot be written leaves the matches on stdout and the next run repeating them. A missing directory component reads as ENOENT, which find_cookie_read() takes for a first run, so the typo was not noticed until every log had been scanned. The temporary file is fsync()ed before the rename, and the directory after it. The rename makes the replacement atomic against another process, which is not the same as durable: after a host crash the rename can be on disk while the contents are not, and an empty cookie parses as no anchors at all. The pre-pass probes the temporary name the write will use rather than the cookie itself, because the write creates "<cookie>.new" and renames over: what it needs is permission on the directory, which opening an existing cookie for append does not test. A cookie owned by the job user in a root-owned directory passed and then failed EACCES with every match already printed. The anchors are left where they were when a run did not finish. A resolve failure that is not "this object has no name" drops matches, and writing the cookie anyway would advance past the very records a rerun needs, so the exit status is settled before the rewrite rather than after it. An index is read with strtoull() and a leading sign refused, the MDT number having been refused for the same reason: "%llu" negates, so "-2" would anchor a run at 0xfffffffffffffffe, match nothing, and write that value back. find_device_supported() refuses --since-cookie along with --since: the option table is compiled into lfind too, so it parses there, and llapi_find_device() never reads it -- a target scan would have printed every object the other predicates kept and exited 0, without even reading or writing the cookie it was given. ONLY=56 is the regression check that the shared paths still answer as they did; --since-cookie's own subtests are in the tests patch at the end of this series. The cookie names the search as well as the filesystem, and a mismatch is refused the way a wrong fsname already was. The anchors say how far each log was read, not how far a subtree was: a record is counted before the FID and the subtree are tested, so a run over one subtree carries every anchor to the end of the log and a second subtree sharing the cookie starts past its own changes and prints nothing. That is the collision lfs_find() refuses when two paths are given to one command, reached by running the command twice. A cookie written before this carries no root and is still accepted. A cookie that exists and is not a regular file is refused up front. The probe settles the containing directory, not the name: the write ends in rename(), which answers EISDIR over a directory, while the read succeeds and returns no anchors -- so naming a directory scanned every log to the end, printed every match, and only then said it could not rewrite. An empty name is refused with it. The comment on the file format said a comment or a blank line costs nothing. It costs the read nothing; the rewrite emits the header and the anchors and nothing else, so it does not survive the run. Said so, the sentence otherwise reading as an invitation to annotate the file. The documentation for fp_since_cookie arrives here rather than in the previous patch, which described the field, its -EINVAL and its -ESTALE before any of them existed. The root is the rest of the header line rather than a field. A pathname can hold a space and %s cuts it at the first one, so a search under "/mnt/lustre/my data" was recorded whole and read back as "/mnt/lustre/my": the comparison then failed for every run after the first, and the message named a path the caller never gave. The root is whatever realpath() returned for the caller's path, so a directory with a space in it was enough to reach it. And the root is escaped rather than written raw, because the rest of a line still cannot hold a newline. Only '/' and NUL are excluded from a name, so "/mnt/lustre/anb" was written whole, read back as "/mnt/lustre/a", and refused for every run after the first naming a path the caller never gave -- the space case one character further out, with the remainder becoming its own line and being dropped. Both sides escape and the reader compares the escaped forms, so nothing decodes: the mapping only has to be injective for a comparison, which is also why backslash is escaped. The refusal names both sides in that spelling, or a newline would break the line meant to name it. find_cookie_check()'s two other refusals say so as well. An empty --since-cookie and a name too long to append ".new" to both returned the errno alone, beside a "is not a regular file" case that reports itself, so the run ended at the caller's "failed for '<path>': Invalid argument" with nothing naming the argument at fault. Two diagnostics that this patch makes reachable are corrected with it. The refusal of --resolve without --changelog said "--since already reads the object", which names an option the caller need not have given once --since-cookie exists; it names both anchored spellings now. And a cookie refused for its filesystem or its root was reported twice, the second line saying "cannot read" over a file that read perfectly well -- find_cookie_read() has already named which refusal it was, so the wrapper is left with what fopen() passed up. The MDT number is exactly the four hex digits find_cookie_write() emits. %4x converts the leading digits and ignores what follows, so "<fsname>-MDT00001" and "<fsname>-MDT0000_UUID" both anchored MDT0000 at an index never written for it -- the short answer with nothing said that the stale check exists to catch, from a file this function's own comment says it ignores anything but "<mdtname> <index>" in. The span refuses the sign as well, so the separate test for it goes. ERRORS gains -ESTALE, which the previous patch dropped on the understanding it would arrive with the cookie and which the kernel-doc Return: block has listed since. It is the error the whole option is built around, and the man page was the one place a caller of the API could not find it. Test-Parameters: testlist=sanity env=ONLY=56 Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com> Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools Change-Id: Ia8ce93368a2c2c8c2ea2ee88c8e7f7e099b1bfa5
LU-20650 tests: sanity cases for find's changelog flags lfs-find.1 gains --since, --since-cookie, --changelog and --resolve: what each answers, that --since is a strict subset of the same search without it, that a bare index belongs to one MDT and is refused where that is ambiguous, that a number which could be an index or an epoch second is never guessed at, and that reading a changelog neither consumes it nor needs a registered user while records exist only while some user is registered. TIME's spellings are given in full, since set_since() takes eight of them and its error text offers one the page would otherwise not list. The page says what --resolve does not answer for -- an object gone between its event and the lookup is counted undecided and dropped, so --resolve can leave out an object the same search without it reports by FID -- and which name -name is tested against. The log is coalesced to one record per object before the search sees it, so the name that can match is the one the object's latest event to carry one carried: a file created as f and hardlinked as g is matched by "-name g" alone, and the pathname printed is its first name rather than the one that matched. A plain walk meets each name in turn and matches all of them. The behaviour is deliberate -- substituting a current name would answer -name against something the record never said -- but a reader had no way to know it. Four sanity cases. 160aa asserts the promise --since is worth having -- that its answer is a subset of the same find without it, and a smaller one -- rather than asserting a particular file, so it does not depend on what else the filesystem is doing. It names the files it expects rather than only counting them, since a count that merely shrank is also satisfied by returning nothing, and it widens the gap around its window so a node that spends seconds in sync does not change what the test means. Its locals stay separate from the assignments that fill them, and it sorts off the pipeline rather than in it: "local x=$(...)" takes local's own exit status, which is always 0, and a command piped into sort reports sort's, so either shape would let a failing lfs find read as the empty answer the index case asserts. 160ab walks the refusals: -size, an object time, --maxdepth, a path below the mount, an unknown MDT, -perm, -links, --mdt-count, --mdt-hash and --xattr are each refused, --resolve on its own is refused rather than dropped, and a named MDT is accepted as the other half of --changelog's argument. It asserts the --resolve answer rather than the exit status alone: -size needs a strict size, the MDT holds only the lazy one, and a resolution that did not glimpse left every object "could not be decided" while still exiting 0. It also asserts -printf's %s, which reads the same size through a widened gather, that --projid is answered rather than failing on the first record, and that a subtree search under --resolve answers for that subtree and not the whole filesystem. 160ab covers the two things that make --changelog worth having over --since, which the man page leads with: an object unlinked since its event is still in the answer, and its FID names it once no pathname does. It ends on -name, which is answered from the record: the log is cleared, one file that predates the clear is written to -- data stores, which carry no name -- and another is created. The named one has to come back and the nameless one must not, so a -name that matched everything fails the second assertion and one that matched nothing fails the first. 160ac runs the cookie three times: the first writes it, the second returns the new object and not the old one, the third returns nothing, and then a stale anchor and both anchors together are refused. It asks for creations only. A wide mask records the test's own reads, and then "nothing changed since" is never true -- which is what hid an off-by-one in the cookie until a narrow mask made every run repeat the last object of the one before it. A test that asserts a precondition instead of creating one only holds where that precondition happens to be true. 160ac's stale anchor is made rather than assumed -- records the cookie has not consumed, then a purge, then one more past the hole -- because writing index 1 is only older than the oldest surviving record on a filesystem that already has changelog history. On a fresh one index 1 IS the oldest record, nothing was purged, and refusing would be wrong. 160ac also feeds the cookie two files it is never written with: one whose MDT index is negative, and one whose header names another filesystem. Neither is a bad anchor to ignore -- the first indexes the anchor array far past its end, and the second is a whole-log rescan of this filesystem plus the loss of the other one's anchors -- so one has to be ignored and the other refused, and the file it refuses has to still be there afterwards. 160ad hardlinks one file into two directories and searches under each. A changelog names an object by FID, and fid2path answers with one of its names -- the first linkea entry, the one the file was created under -- so a search rooted at the other directory is answered about a name that is not under it, and -name about a basename the object does not go by there. Without the links being walked it finds the file under a and loses it under b, which is a silent miss and the worst shape a wrong answer can take. The tag is tests: rather than doc: because that is where the bulk of the change is; the man page rides along because the options it describes are the ones these cases exercise. There is no version gate. Nothing in this series touches server code -- it is lustreapi.h, lustre/utils/ and Documentation/ -- so the options work against any server old enough to have changelogs, and a gate on the MDS would skip interop runs that pass. A client gate would say nothing either: this script ships with the client whose lfs has the options. The third Test-Parameters line below is that claim under test rather than only asserted: an old server, a new client, and these four cases. Test-Parameters: testlist=sanity env=ONLY=160aa,160ab,160ac,160ad Test-Parameters: testlist=sanity env=ONLY=160aa,160ab,160ac,160ad mdscount=2 mdtcount=4 Test-Parameters: testlist=sanity env=ONLY=160aa,160ab,160ac,160ad serverversion=2.15.6 Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com> Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools Change-Id: Id4001c6821e72f15b83a9733bbb28cef4248b822
| unique failing test | history |
|---|---|
| sanity2@ldiskfs+DNE:test_157d | NEW unique failure for this branch in the last 30 days, and was seen 0 times across 0 other branches 0 reviews |
| sanity2@zfs:test_157d | seen in 1 other review |
LU-20603 llapi: pull a scan's records in batches Add llapi_scan_namespace_open(), llapi_scan_next() and llapi_scan_close(): the namespace scan turned around, so a consumer pulls records in batches on its own thread instead of being called once per object on the scan's. A layer over llapi_scan_namespace() and not a second scanner -- the scan runs on a thread of its own, its callback copies each record into the batch being filled, and llapi_scan_next() hands out a full batch while the scan fills the other. The copy is what a batch costs. A callback record points into buffers the scan reuses for the next object, so a record that outlives its callback has to own what it points at: the path, the name, the layout, the directory stripe, the linkea, the rename source and the jobid are copied into an arena the batch owns and the pointers rewritten. The descriptors are not carried and read -1. Two batches suffice, the scan blocking when the one it fills is full, so what is in flight is bounded by the batch size and not by the tree. The caller sees an array of record pointers rather than of records, so its stride is a pointer's and a record that grows does not move its neighbours under a caller built against the older size. A filter in lfsp_filter runs on the scan's threads with the caller's data, which the layer keeps beside its own. An error is reported after every record produced before it has been handed out, so the batches before it are whole; a close on a running scan stops it, and that is not an error. A handle belongs to one consumer thread, and that is enforced rather than only stated: llapi_scan_next() and llapi_scan_close() refuse a second caller with -EBUSY. Without it a second consumer resets the batch the first is still reading and installs one whose sb_count is already sb_cap, and the next append writes past the end of sb_recs -- reproduced under ASan on the unfixed build, five runs out of five, and gone on the fixed one, which serves every object once. It is the opposite of the callback API's rule, where the callback runs on every scan thread at once, so it is said in the header and the man page too. The arena keeps every chunk across a reset rather than the largest. A batch needing two chunks -- about 320 bytes of payload per record at the 1024 default, which a path plus an 8-stripe layout reaches -- otherwise malloc'd and freed the second on every batch, which is what the comment said it did not do. Measured on a 20k-object tree off Lustre, where per-object work is a cached lstat() and the copy is at its largest share: a batch of 1024 costs 76 ns per object over the callback at 4 threads (+8%), 135 ns at one (+5%); a batch of 1 costs 2-4 us, the lock round trip per record. On a scan whose per-object work is an ioctl the same copy is a fraction of a percent, which the man page says. test16 rides along: it asks llapi_scan_namespace() what lfsp_got reports, which is the callback API and not this one, and belongs with whichever change lands lfsp_got. llapi_scan_next.3 says when a batch stops being the consumer's, not only that it does. The release happens at the top of the following llapi_scan_next(), before that call waits for the scan rather than when it returns, so a consumer that hands a batch to another thread and calls again to overlap the next fill has released what that thread is reading -- and the -EBUSY that refuses a second caller does not refuse a reader racing one. The arena is rewound rather than freed, which is what makes a steady-state scan allocate nothing per batch, so that reader finds the following batch's bytes and not a fault. The -EBUSY case is made rather than waited for. A second consumer is refused only while the first is inside llapi_scan_next(), and a test that calls from between two of them never reaches the guard it is about -- the ASan overflow above cannot fail it. A filter that sleeps holds the scan back so the third call blocks; two calls come first, the layer filling one batch ahead and the consumer not blocking until it has drained that. The overlap is then measured, not assumed: a machine slow enough to put the second call outside the window makes the case unreachable, and an unreachable case is reported rather than failed. Test-Parameters: trivial testlist=sanity env=ONLY="56El 157c" Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com> Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools Change-Id: I29aaa31a4c0bd34ca591316601a653f9b5ae70c2
LU-20564 utils: probe md RAID geometry for OST mkfs/mount mkfs.lustre now probes the md array underneath an OST target (through stacked dm devices) and seeds -E stride/stripe_width so mballoc can align and size allocations to full stripes. mke2fs can derive these from the blkid topology, but that is absent on some stacked configurations. raid0, raid4/5/6, raid10 and raidkm (level 71) are decoded; the replication factor of raid10 and the declustered g/m of raidkm come from the md layout word. User-supplied stride/stripe_width values are respected, misaligned partition starts draw a warning at mkfs time, and mount.lustre now warns when an OST on a striped array has no (or mismatched) fs geometry. A stripe_width that is a multiple of the array row is still stripe-aligned and is not reported. mkfs.lustre also warns when the array's data row (k * chunk) is not a power of two. Such a row cannot be written whole by any client: bulk RPC sizes are powers of two in practice, so a row divides some RPC size if and only if the row is itself a power of two. A geometry like k=5 at 64K (320KiB) or k=14 at 64K (896KiB) leaves a partial row at the tail of every RPC, which the array services as a read-modify- write, at every chunk size and with no tuning available. Unlike stride/stripe_width this cannot be corrected afterwards -- it is a property of the array -- so the warning is issued at mkfs time, while the geometry can still be chosen, and is not suppressed by a user-supplied stride/stripe_width. Tested on RHEL 9.7 against live loop-backed md arrays: raid0/4/5/6, raid10 near, far and offset layouts, a non-power-of-two row, user-supplied values, dm-linear over one array and spanning two with conflicting geometry, and aligned and misaligned partition starts. Also tested against live raidkm (md level 71) arrays: plain m=2 and m=3, parity-last, and declustered g=6/m=2 and g=8/m=3 over wider pools. In every case the computed row matches md's own queue/optimal_io_size. Test-Parameters: trivial Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com> Assisted-by: Claude:Fable-5 claude-code Assisted-by: Claude:Opus-5 claude-code Change-Id: I442528cea51a55c6b74f7f2282a1db566eab9783
LU-20542 gss: retry callback RPC when reverse ctx is stale
A server cannot renegotiate a reverse GSS context; one is only
installed from a peer's forward request. So when the peer drops its
contexts, the next callback is answered with GSS_S_NO_CONTEXT,
sptlrpc_req_replace_dead_ctx() returns -ECONNREFUSED, and
ldlm_handle_ast_error() evicts a client that was only mid-refresh.
This is the sanity-sec test_27d/27e failure.
Resend the callback a bounded number of times instead, marked with
rq_gss_rvs_resend so it skips the context refresh in
ptlrpc_check_set() and reaches the peer over the same dead context.
Every other resend still refreshes, so a reverse context that goes
stale on its own is still detected and replaced. Blocking and
completion ASTs stay bounded by rq_no_resend and the lock callback
timeout; for a glimpse AST, which sets neither, the resend count and
its backoff are the bound.
Fixes: 67acf6047e34 ("LU-17317 gss: do not continue using expired reverse context")
Test-Parameters: testgroup=review-dne-selinux-ssk-part-2
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Change-Id: I2a007b42656eb9aa0bed00291b67b6ac7db2d389
| failed enforced test | platform | detail | |
|---|---|---|---|
| review-dne-selinux-ssk-part-2 | RHEL 8.10 / x86_64 | ran 7 tests. 1 tests failed: sanity-sec. | session |
LU-20648 mdc: fix changelog mask composition
A zero cf_mask means two different things and chlg_ioctl() composed
them as if it meant one. On the server it means "this user has no
mask of its own": mdd_chlg_usermask() returns 0 for a record that
carries none. On the client it means "do not filter": a zero
crs_user_mask skips the record type test altogether.
So "lfs changelog --user NAME --mask creat" for a user who has no
mask computed in.cf_mask & 0, which is 0, which the record test then
read as unfiltered. The caller got every record type back and no
error, which is a wrong answer rather than a failure.
Compose the two zeros as what they mean. A user with no mask of its
own is not restricted to nothing, it is unrestricted, so a caller's
mask passes through unchanged; only two real masks intersect.
Registration writes a rec2 when either a mask or a name was given.
It fills cur_mask when a mask was given, and otherwise only while the
server's changelog_mask is still minimal, where a maskless user is
handed CHANGELOG_DEFMASK instead. So the reachable case today is a
registration with a name and no mask made while the server mask is
set to anything wider. A user registered with neither name nor mask
gets the older record type, whose mask reads as zero whatever was
written, and is affected too once LU-20647 makes such a user
reachable by lookup at all.
Two real masks that do not overlap intersect to zero as well, and
zero cannot carry "nothing matches" while it also means "do not
filter". Two absolute masks cannot reach it: cfs_str2mask() seeds an
absolute mask from its minmask argument, and both the registration and
"lfs changelog --mask" pass CHANGELOG_MINMASK, so "-m creat" is really
MARK|CREAT and "--mask mkdir" is MARK|MKDIR -- always sharing
BIT(CL_MARK). A relative one does. llapi_convert_str2mask() seeds
from *oldmask when the first token carries an operator, and
lfs_changelog() seeds that with CHANGELOG_DEFMASK, so "--mask
=-mark,-creat" is DEFMASK without those two: non-zero, and sharing no
bit with a user registered "-m creat". So the wrong answer is
reachable from the shell, and against such a user "lfs changelog
--user clN --mask=-mark,-creat" printed every record it had.
So crs_user_mask now names the record types to deliver: it starts as
all bits, an empty intersection is no bits, and the record test is a
plain bit test with no special case for zero.
test_160z asserts that the user carries no mask before going on,
rather than reasoning that it must: changelog_register() sets +hsm
before registering, and mdd_changelog_mask_seq_write() rebases a
relative mask onto CHANGELOG_DEFMASK while the proc mask is still
minimal, so the branch that hands a maskless user a default one
cannot fire there. The test widens the mask so that CREAT and MKDIR
are recorded whatever an earlier subtest left it at, and only on
$SINGLEMDS, which is the facet its trap restores. It asks for --mask
creat and requires the MKDIR record to be absent, then asks without
--mask and requires it to be present. The second half is what keeps
the fix from turning "no mask" into "no records".
160z covers the disjoint case in its second half, with a user
registered "-m creat" read back through "--mask=-mark,-creat". An
earlier attempt used two absolute masks and was removed after a
control run showed it passing against the unfixed module as well; this
one cannot, the composition there being zero and zero meaning "do not
filter". The control beside it is the same user read without --mask,
so a run that selects nothing because the user has nothing cannot be
mistaken for the fix working.
Fixes: 41b55cf2309d ("LU-19296 changelog: Add user-specific changelog filtering")
Test-Parameters: testlist=sanity env=ONLY=160z
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools
Change-Id: Ib178e4c15be40c415262bf06c08da8b061855cf9
| unique failing test | history |
|---|---|
| sanity-quota@zfs:test_48 | seen in 2 other reviews |
| failed enforced test | platform | detail | |
|---|---|---|---|
| review-dne-part-5 | RHEL 9.7 / x86_64 | ran 5 tests. 1 tests failed: sanityn. | session |
| review-dne-selinux-ssk-part-2 | RHEL 8.10 / x86_64 | ran 7 tests. 1 tests failed: sanity-sec. | session |
LU-20647 mdd: look up a changelog user of either record type
mdd_changelog_user_register() writes CHANGELOG_USER_REC when the
caller gave neither a mask nor a name, deliberately, for
compatibility. mdd_changelog_user_lookup_cb() matched
CHANGELOG_USER_REC2 only, so such a user was never found and
mdd_changelog_user_lookup() returned -ENOENT. That is the shipped
"lfs changelog --user", which reaches the callback through
OBD_IOC_CHANGELOG_FILTER, not only the library call.
Every other MDD walker already takes both types and guards the fields
that exist in only one of them, so the callback was alone in
rejecting the record rather than the field.
The type test cannot simply be dropped. struct
llog_changelog_user_rec is half the size of rec2 and carries neither
cur_mask nor cur_name, and the || short-circuited before strcmp()
reached cur_name; removing the first clause would read past the end
of the record into whatever the llog buffer holds next. So a lookup
by ID matches on cur_id alone, a lookup by name skips an old record
instead of comparing against it, and the reply's username is filled
only from a rec2. The reply's mask already came from
mdd_chlg_usermask(), which returns 0 for the old type, and a zero
mask is what the client reads as "do not filter".
The reply's username comes from the record's type and not from how the
lookup was made. mdd_iocontrol() passes one buffer as both request
and reply, so cf_user_id holds the record's ID by the time the old
"was this a lookup by ID" test read it -- true on every lookup, and
harmless only because a name lookup had matched that same name.
The name comparison is bounded as well. cur_name comes off disk with
no guarantee of a NUL inside its 16 bytes, and
mdd_changelog_name_check_cb() already allows for that on the same
field.
test_160w covers this command but registers both of its users with a
name and a mask, so both records are rec2 and the plain path was
never walked. test_160y registers one user plainly, one with a mask
and one with a name, and asks for the records of each; the masked user
is the control that has always worked. The named user is coverage
rather than a regression case: the old callback rejected a
CHANGELOG_USER_REC in the first clause of its ||, before strcmp() ran,
so a lookup by name walked past a plain record before this patch as it
does after it. Only the cf_user_id != 0 path changes behaviour, and
the client parses "cl<N>" into an ID, so that is the path lfs takes.
This wants LU-20648 beside it. Making a plain user reachable makes
cf_mask == 0 the ordinary answer, and a client without that fix
composes in.cf_mask & 0 and reads the zero as "do not filter": so
"lfs changelog --user cl1 --mask creat" on a plain user would print
every record type where it used to stop at -ENOENT. Landing this one
alone trades a lookup failure for a wrong answer.
Fixes: 5b85a4eb7510 ("LU-19296 changelog: retrive changelog user info from MDT")
Test-Parameters: testlist=sanity env=ONLY=160y
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools
Change-Id: I12ef76c773ce31e4548db9022f381cabfe7f5ddc
| failed enforced test | platform | detail | |
|---|---|---|---|
| review-dne-part-2 | RHEL 9.7 / x86_64 | ran 11 tests. 1 tests failed: sanity-lfsck. | session |
| review-dne-part-4 | RHEL 9.7 / x86_64 | ran 9 tests. 2 tests failed: sanity-quota, sanity-dom. | session |
| review-dne-part-5 | RHEL 9.7 / x86_64 | ran 5 tests. 1 tests failed: recovery-small. | session |
| review-dne-selinux-ssk-part-2 | RHEL 8.10 / x86_64 | ran 7 tests. 1 tests failed: sanity-sec. | session |
LU-20643 utils: clear stale lmd fields on reuse
get_lmd_info_fd() reuses one struct lov_user_mds_data for every object
and convert_lmd_statx() writes only the fields lstat(2) supplies, so
stx_attributes, stx_attributes_mask, stx_btime and the mask bits above
STATX_BASIC_STATS keep whatever was there before.
Two ways that is not this object's data. On the V1 ioctl path the
lstat_t is copied over the front of the record, so stx_attributes_mask
ends up holding st_atim.tv_sec -- never 0 in practice -- and stx_mask
picks up bits from st_nlink, which reads as STATX_BTIME on a directory
whose link count has 0x800 set. It does not reach as far as it looks:
lmd_st is 144 bytes against 272 for lmd_fid and lmd_stx, so it stops
at the end of stx_mtime and stx_rdev_major onwards is still the
previous conversion's. On the lstat fallback the buffer holds the
previous object's data outright, or the file name written in for the
ioctl.
So a walk that crosses onto a non-Lustre subtree can answer -btime and
the attribute predicates from a file name.
Clear what is about to be reused on both paths, with the same line:
everything up to lmd_lmm, which is what the record's own fields amount
to. That takes lmd_fid, lmd_flags, lmd_lmmsize and lmd_padding with
it -- nothing in lustre/utils reads lmd_fid today, but a short name
there passes fid_is_sane() as an IGIF, and llapi_get_lum_file_fd()
copies by lmd_lmmsize. On the V1 path it goes after the memmove, so
the layout it has just moved up is not touched. The glimpse path is
left alone: there the MDT's answer is still wanted.
Fixes: 11aa7f8704c4 ("LU-11367 som: integrate LSOM with lfs find")
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools
Change-Id: I6126e73bb120bd6a12a904563e13303d63e75c1c
| failed enforced test | platform | detail | |
|---|---|---|---|
| review-dne-zfs-part-1 | RHEL 9.7 / x86_64 | ran 3 tests. 1 tests failed: sanity. | session |
| review-dne-zfs-part-7 | RHEL 9.7 / x86_64 | ran 6 tests. 1 tests failed: sanity-ec. | session |
(minor) This does not seem to hold, and it argues against the Fixes: tag just below it.
sptlrpc_sepol_get() already returned genuine error pointers before 8a6788b508db:
if (imp_sec == NULL)
RETURN(ERR_PTR(-EINVAL));
...
out = sptlrpc_sepol_get_cached(imp_sec);
if (!out)
RETURN(ERR_PTR(-ENODATA));
sepol_helper() also returns `ret >> 8` of call_usermodehelper(), so when the helper cannot be exec'd at all (missing /usr/sbin/l_getsepol gives -ENOENT) rc comes out as -1, which IS_ERR() accepts.
What 8a6788b508db changed is the l_getsepol-exits-nonzero case, where the status was positive. Stating that the leak is only reachable after it may keep the fix from being picked up on branches that predate it. Would it be more accurate to say 8a6788b508db made the path much easier to hit?
(suggestion) The other sptlrpc_sepol_get() callers have the same sequence: mdc_request.c:373, mdc_reint.c:199/345/409/487 and mdc_locks.c:326/481/545 all jump straight from the IS_ERR() branch to err_free_rq, which calls ptlrpc_request_free(), so they leak the import reference and the imp_reqs count in the same way. That is pre-existing and this patch does not have to carry it. It is not a one-line swap either, since err_free_rq is shared with the post-pack failures reached through err_put_sepol and those must keep ptlrpc_request_free(). Is a follow-on change under the same ticket planned for them?
LU-20050 ptlrpc: fix import ref leak on sepol failure
ptlrpc_connect_import_locked() calls ptlrpc_request_alloc(), which
takes a reference on the import via class_import_get(). When
sptlrpc_sepol_get() fails, the IS_ERR() branch calls
ptlrpc_request_free(). That only returns the request to its slab
and does not drop the import reference.
The leaked reference keeps client_obd_cleanup() from running, so
the LDLM namespace kobject is left in sysfs and the next mount
fails with -EEXIST.
Use ptlrpc_req_put() on this path instead. It drops rq_refcount to
zero and calls __ptlrpc_free_req(), which releases the import.
The second failure path, after ptlrpc_request_bufs_pack() returns
an error, still uses ptlrpc_request_free(). That is correct: the
out_free label inside ptlrpc_request_bufs_pack() has already
dropped the import reference, so only the slab is left to free.
Both paths now carry a comment saying which case they are.
This bug is only reachable after 8a6788b508db, which stopped a
positive errno from reaching ERR_PTR() and so made the IS_ERR()
branch above actually trigger.
Fixes: dd200e5530fd ("LU-8955 ptlrpc: manage SELinux policy info at connect time")
Assisted-by: Claude:Opus-5 claude-code
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Change-Id: Ia4267f87219e2a7e4fbcb996978543174bcb7197
| failed enforced test | platform | detail | |
|---|---|---|---|
| review-dne-zfs-part-4 | RHEL 9.7 / x86_64 | ran 9 tests. 1 tests failed: sanity-quota. | session |
| review-dne-zfs-part-7 | RHEL 9.7 / x86_64 | ran 6 tests. 1 tests failed: sanity-ec. | session |
(minor) Does this second Fixes: tag point at the right commit? 8e86156c34f3 added ptlrpc_req_put() to osp_sync_new_unlink_job() and osp_sync_new_unlink64_job() on paths that run *after* a successful pack, where dropping rq_import is correct. The ptlrpc_req_put() on the pack error in osp_sync_new_job() itself goes back to 1d371ca47e67 ("LU-1303 osp: OSP logging functionality"), and the class_import_put() at out_free that leaves rq_import set is older still.
So the double release predates 8e86156c34f3. Either drop this tag or point it at the commit that actually introduced the imbalance.
(minor) The comment promises more than the function delivers. out_ctx falls through to here after sptlrpc_cli_ctx_put(request->rq_cli_ctx, 1) without clearing rq_cli_ctx, so a caller that uses ptlrpc_req_put() reaches sptlrpc_req_put_ctx() in __ptlrpc_free_req() and drops the same context reference a second time. That is exactly gss_cli_ctx_fini_rpc(): it passes a non-NULL ctx, so out_free is unreachable for it and out_ctx is its only failure exit, and it disposes of the request with ptlrpc_req_put(). It is pre-existing rather than introduced here, but the commit message says clearing rq_import "lets callers use either free or put", which isn't true while the ctx is still dangling. Would sptlrpc_req_put_ctx(request, 1) at out_ctx (it does the put and NULLs the field) make the claim hold?
(defect) Is ptlrpc_req_put() safe here for a request that came from the pool?
__ptlrpc_request_alloc() falls back to ptlrpc_prep_req_from_pool() when the slab allocation fails, and that request keeps the pool's preallocated rq_reqbuf while rq_cli_ctx is still NULL (nothing has packed yet). __ptlrpc_free_req() then does:
if (request->rq_reqbuf || request->rq_clrbuf)
sptlrpc_cli_free_reqbuf(request);
and sptlrpc_cli_free_reqbuf() starts with LASSERT(ctx) on req->rq_cli_ctx, so this looks like an LBUG rather than a clean free.
The path seems reachable: osc_brw_prep_request() uses ptlrpc_request_alloc_pool() for OST_WRITE, so under memory pressure the pool fallback is taken; if the OSC import is idle at that point, ptlrpc_reconnect_if_idle() -> ptlrpc_connect_import_locked() itself does ptlrpc_request_alloc() and returns -ENOMEM under the same pressure. The old code went through ptlrpc_request_free(), which just returned the request to the pool.
The request also has no capsule yet (req_capsule_init() runs below), so it is not really in a state ptlrpc_req_put() expects. Would keeping the explicit atomic_dec()/class_import_put() plus ptlrpc_request_free(), or clearing rq_import and then freeing, be safer than routing this through the full teardown?
LU-20546 ptlrpc: fix import ref on request setup failures
ptlrpc_request_alloc() takes a reference on the import. Two
setup paths get its release wrong, in opposite directions.
ptlrpc_request_alloc_internal() open-codes atomic_dec() on
imp_reqs and then calls ptlrpc_request_free() when an idle
reconnect fails, so class_import_put() is never reached and the
reference leaks. Call ptlrpc_req_put() instead, which does both.
ptlrpc_request_bufs_pack() has the reverse problem: out_free
drops the reference but leaves rq_import set, so a caller that
disposes of the failed request with ptlrpc_req_put() releases it
a second time. gss_cli_ctx_fini_rpc() and osp_sync_new_job() both
do that on a pack error. Clear rq_import once it has been
dropped, which also lets callers use either free or put.
Fixes: 93d20d171c20 ("LU-11128 ptlrpc: new request vs disconnect race")
Fixes: 8e86156c34f3 ("LU-10486 osp: fix request leak on error in osp_sync")
Assisted-by: Claude:Opus-5 claude-code
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Change-Id: Iec07b093e380f262b452ff4fc5f5fac64f1c9fb0
LU-20050 ptlrpc: fix sepol_helper status decode
sepol_helper() declared ret as signed short while
call_usermodehelper() returns int, and decoded it with a single
"rc = ret>>8". That shift is only correct for a normal exit. A
kernel launch failure returns a negative errno, not a wait
status, and shifting collapses it (-ENOMEM becomes -EPERM). A
helper killed by a signal leaves the signal in the low 7 bits,
so ret>>8 is 0 and the failure is reported as success.
Widen ret to int and decode the three cases separately, with raw
bit tests since WIFEXITED/WEXITSTATUS are userspace macros:
ret < 0 -> kernel failed to launch the helper
(ret & 0x7f) == 0 -> normal exit (WIFEXITED)
(ret >> 8) & 0xff -> exit code (WEXITSTATUS)
The exit code must be sign-extended, not negated: l_getsepol
returns -errno whenever stdin is not a tty, always the case
under call_usermodehelper(), and exit() keeps only the low byte.
So -ENODEV arrives as exit byte 237, and (signed char)237 is
-19. sptlrpc_sepol_get() special-cases -ENODEV as "SELinux is
disabled, report nothing", so negating it made that branch
unreachable and turned those mounts into "Unknown error 237".
This extends 8a6788b508db.
A positive exit status is not an errno either, so negating it
invented plausible but wrong errors: 127 became EKEYEXPIRED and
126 ENOKEY, pointing at key management rather than a broken
helper. Report -EPROTO and log the raw status. sepol_helper()
can then no longer return a positive value, so drop the
unreachable rc > 0 case in sptlrpc_sepol_get().
The eight sptlrpc_sepol_get() error paths in mdc_locks.c,
mdc_reint.c and mdc_request.c leaked the request's import
reference and the LDLM locks gathered for early cancel: the
sepol check runs before the prep call, so the request still owns
both, but the error tail only called ptlrpc_request_free(). Keep
one exit per function and make it correct on both sides of the
prep call, through the new mdc_elc_req_put() helper, clearing
@count once the prep call owns the locks. This needs LU-20546,
which clears rq_import when packing fails so the reference is
dropped exactly once.
Add sanity-selinux test_21d: stub l_getsepol, mount with
send_sepol=-1, and check that exit 237 still mounts while 234
and a missing helper fail with the expected errno text. It
creates no nodemap on purpose -- with nodemap.<nm>.sepol set,
req_check_sepol() would refuse the -ENODEV connect for an
unrelated reason.
Suggested-by: Sebastien Buisson <sbuisson@ddn.com>
Test-Parameters: trivial testgroup=review-dne-selinux-ssk-part-1
Test-Parameters: trivial testgroup=review-dne-selinux-ssk-part-2
Fixes: c61168239eff ("LU-8955 sec: create new function sptlrpc_get_sepol()")
Fixes: 0a773f04b288 ("LU-8955 ptlrpc: manage SELinux policy info for metadata ops")
Assisted-by: Claude:Opus-5 claude-code
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Change-Id: If9312fce842fca133de21c4898cafd949ace6aa6
LU-20119 dbg: test lustre_helper labeling fix for test_21a fortestonly patch validating the fix for the sanity-selinux test_21a failure on the DNE + RHEL 8.10 server + RHEL 10.0 client config, where the client mount fails because l_getsepol returns -EACCES (-25 before the errno fix below) from the sepol connect upcall. Root cause: the l_getsepol/lctl/l_getidentity/l_getauth helpers were left labeled bin_t instead of lustre_helper_exec_t, so the kernel execs them into the unprivileged kernel_generic_helper_t domain (via the base "kernel_t bin_t:process kernel_generic_helper_t" transition) rather than lustre_helper_t. That domain cannot read selinuxfs or the on-disk SELinux policy, so l_getsepol fails with EACCES. Two reasons the label was never applied: 1. lustre.spec.in %post loaded the policy module with "semodule -i" but never ran restorecon. semodule -i does not relabel already-installed files, and the binaries were labeled bin_t at install time (before the module existed). Add a restorecon of the four helpers to %post (the make-install path already does this, but it is skipped for staged/RPM builds where DESTDIR is set). 2. lustre_helpers.fc labeled only /usr/bin/*. On RHEL/Rocky 10.2+ the /usr/sbin=/usr/bin file-context equivalency makes that work, but on el8/el9 and el10.0/10.1 /usr/sbin is a real path with no such equivalency, so restorecon of the /usr/sbin binary finds no rule. List both /usr/bin and /usr/sbin. The sepol_helper() CERROR and the errlog() errno fix from the prior diagnostic patch are retained so that, if the mount still fails, the kernel log shows l_getsepol's real exit code. Not intended to land; validates the fix before it goes on the LU-20119 review. Test-Parameters: fortestonly Test-Parameters: livedebug mdscount=2 mdtcount=4 osscount=1 ostcount=8 clientcount=2 testlist=sanity-selinux clientdistro=el10.0 serverdistro=el8.10 env=ONLY="21a",SHARED_KEY=true clientselinux Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com> Change-Id: Ifd207da02862849c90500a40b4ff71dff5bc711b
| failed enforced test | platform | detail | |
|---|---|---|---|
| review-dne-part-2 | RHEL 9.7 / x86_64 | ran 11 tests. 1 tests failed: sanity-lfsck. | session |
| review-dne-part-5 | RHEL 9.7 / x86_64 | ran 5 tests. 1 tests failed: lustre-rsync-test. | session |
LU-20238 ec: branch-free byte-indexed scalar base path Replace the per-byte gf_mul() in the scalar *_base hot paths with a byte-indexed per-coefficient region table: tbl[b] = c * b in GF(2^8), built once per coefficient gf_region_mul(tbl, x) = tbl[x] (single load, inner loop) ec_encode_data_base() at k=6/p=2, gcc -O2: 0.79x at 256 B, 1.08x at 1 KiB, 1.32x at 64 KiB. gf_mul() alone is ~20% slower than the 64 KiB direct lookup it replaces, so regions below GF_REGION_TBL_MIN_LEN (1 KiB) keep a scalar fallback. Byte-indexed lookup (gf-nishida-16 "FullByte"-style) is also patent-clean: no PSHUFB, no nibble split, no 16-byte gftbl, so it is structurally distinct from the ISA-L SIMD pattern that maps onto US 8,683,296 claim 21. ec_base.c carried an unconditional "#define GF_LARGE_TABLES", so both ec.ko and libec.a compiled the 64 KiB gf_mul_table_base and the small-table branch was dead code. With that define and the inert ec_base_o_CPPFLAGS in ec/Makefile both gone nothing could set it, so the block is deleted outright: .rodata 65792 -> 1536, .text 2217 -> 3591 for the fallbacks. gf_vect_mul_init is renamed gf_vect_mul_init_base, with a wrapper in ec_base_aliases.c; its 64-bit path now selects on BITS_PER_LONG for kernel builds, since __WORDSIZE is glibc-only. No existing entry point changes behaviour. Validated on RHEL 10.1 (AVX2+GFNI): exhaustive gf_mul + gf_inv, k=6/p=2 round-trip KAT, and gfni_ec_test byte-identical parity. Test-Parameters: testlist=sanity-ec Test-Parameters: testlist=sanity env=ONLY=910 Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com> Change-Id: I905e365d145d0018cbcd2bbc919eb4e516fafe8e
| failed enforced test | platform | detail | |
|---|---|---|---|
| review-dne-selinux-ssk-part-2 | RHEL 8.10 / x86_64 | ran 5 tests. 1 tests failed: sanity-selinux. | session |
LU-20147 ptlrpc: module-level cache for SELinux policy upcall Replace the per-import call_usermodehelper model with a single module-level upcall_cache (hashsize=1, key=0) shared by all imports. The original LU-8955 design called l_getsepol via call_usermodehelper() on every RPC from every import independently. On RHEL/Rocky 10.1 this fails: the kernel_generic_helper_t SELinux domain is blocked from reading /sys/fs/selinux/policyvers and writing to debugfs, causing a 30-second timeout on every first RPC. Cache lifecycle and freshness: - upcall_cache.h: add struct sepol_cache_data and union member in upcall_cache_entry. - lustre_sec.h: remove ps_sepol and ps_sepol_checknext from struct ptlrpc_sec. - sec.c: add sptlrpc_sepol_cache (module-level upcall_cache), sptlrpc_sepol_get(), sptlrpc_sepol_do_upcall() (fires l_getsepol), sptlrpc_sepol_parse_downcall(), and sptlrpc_sepol_cache_update() for unsolicited push updates. send_sepol changed from static to extern. - The cache is initialised with entry_expire = S32_MAX so the framework never reaps entries by time; sptlrpc_sepol_get() drives freshness by stat()ing the policy directory on every call and calling upcall_cache_flush_one() if its mtime differs from the value carried by the last downcall. l_getsepol therefore runs only on mount or after an actual policy change. - send_sepol == 0 disables the feature entirely (no policy info on any RPC). Any non-zero value behaves identically; the legacy "send_sepol = N seconds" semantic is preserved as parameter-level backward compatibility but no longer affects runtime behaviour (with the cache in place, the LU-8955 reason for an N-second TTL doesn't apply). Backward-compatible wire format and invocation: - lustre_user.h: keep the pre-LU-20147 layout as struct sepol_downcall_data_v2 / SEPOL_DOWNCALL_MAGIC_V2 = 0x8b8bb843 alongside the new struct sepol_downcall_data / SEPOL_DOWNCALL_MAGIC = 0x8b8bb844. - sec_lproc.c sptlrpc_sepol_do_downcall() and sec.c sptlrpc_sepol_parse_downcall() accept both magics; only the new layout populates the policy-dir mtime tracking used by the freshness check. - l_getsepol gained -2 / --new-format: with -2 it writes the new layout, without -2 it writes the v2 layout. Default is v2 so a new userspace stays compatible with an old kernel. - sec.c sptlrpc_sepol_do_upcall() keeps the pre-LU-20147 argv shape "--obd_type X --obd_name Y" (so old l_getsepol still recognises it) and just appends "-2" as a new flag. X/Y are the type and name of any active OBD on the client (MGC preferred); the per-OBD srpc_sepol write handler funnels the result back through sptlrpc_sepol_do_downcall() into the module-level cache, so it doesn't matter which OBD is used. We invoke under UMH_WAIT_PROC and on non-zero exit (eg. old userspace that doesn't recognise "-2") retry without "-2". - sec_lproc.c: add module-level srpc_sepol write-only debugfs file at sptlrpc/srpc_sepol (LDEBUGFS_FOPS_WR_ONLY) so admin tools can feed the cache directly; per-OBD srpc_sepol kept for backward compatibility and used by the kernel upcall above. l_getsepol changes: - Replace file-scope static globals with per-call lgetsepol_ctx. - Add -k/--kernel flag: writes downcall to module-level path sptlrpc/srpc_sepol; includes policy dir path and mtime. - Add -2/--new-format flag (see above). - Add -w/--watch flag: inotify daemon mode for policy change detection (retained as alternative; community feedback welcome). - Add find_policy_with_highest_ver() helper used by the existing RHEL/Rocky 10.1 fix in LU-20045's stack; the helper itself is duplicated locally so this patch is self-contained against an older LU-20045 base. When LU-20045 lands on master this duplicate can be dropped. - Use fstatat(dirfd(dp), ...) in the policy version scan loop. Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com> Change-Id: Icf813c252b7359c3dc17d80405579b50b57ab025
LU-20084 osp: fix LDLM ns kobject leak in lwp_device_fini() The LWP teardown path does not call ldlm_namespace_free_prior() before client_obd_cleanup() -> ldlm_namespace_free_post(). This affects two paths: Normal teardown (LCFG_CLEANUP -> lwp_process_config -> lwp_disconnect -> lwp_device_fini): lwp_disconnect() calls ptlrpc_disconnect_import() and ptlrpc_invalidate_import() but never calls client_disconnect_export(), so ldlm_namespace_free_prior() is never reached. lwp_device_fini() then calls class_disconnect() and client_obd_cleanup() directly, skipping free_prior entirely. Failed-connect path (lwp_obd_connect failure -> out_dis: class_disconnect -> lpd_exp=NULL -> lwp_device_fini): the same lwp_device_fini() is reached with lpd_exp NULL, so class_disconnect is skipped and client_obd_cleanup() is again called without free_prior. Without ldlm_namespace_free_prior(), LDLM_NS_STOPPING is never set. If the pool recalc task runs concurrently with ldlm_namespace_free_post(), the race can corrupt the pool kobject cleanup path and cause ldlm_namespace_sysfs_unregister() to hang, stranding the namespace kobject under /sys/fs/lustre/ldlm/namespaces/. Fix by calling ldlm_namespace_free_prior() in lwp_device_fini() before client_obd_cleanup(). This is symmetric with lwp_setup() -> client_obd_setup() where the namespace is created, and covers all LWP teardown paths. The import is still valid at this point since obd_cleanup_client_import() runs inside client_obd_cleanup() afterwards. Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com> Change-Id: Ie9a290ca529d18b0f837669a53c0c06cda4f767f
(minor) This paragraph lists what the record grew, but two other pieces of new public API aren't accounted for anywhere in the body: `struct llapi_scan_stats` with the `sp_stats` parameter field, and the `LLAPI_SCAN_F_INTERNAL` flag. Both are things a caller has to know about, and the counters in particular are described in the man page as part of the answer rather than a diagnostic. Worth a sentence each so the diff holds no surprises.
Done. Held back through several rounds as prose-only, but you are right that new public API is not prose. The message now names `struct llapi_scan_stats` with `sp_stats` and says what LLAPI_SCAN_F_INTERNAL is for: the objects a target holds that its namespace never shows -- the OSD's own, the DNE agent inodes, the ones with no LMA -- which a scan otherwise counts in ss_class and keeps back. Checked against `enum llapi_scan_class` and the gate in liblustreapi_scan_device.c rather than written from the flag's name.
API changes are not critical within the development releases.
LU-20606 llapi: scan an ldiskfs target directly
Add llapi_scan_device(), which reads an MDT's or OST's objects
straight off the device with libext2fs and delivers the same records
llapi_scan_namespace() does. No mount, no MDS, no kernel change: it
runs against a stopped target, a snapshot or a failover partner's LUN,
read-only and with no write path.
libext2fs stays off liblustreapi. The backend builds as
scan_osd_ldiskfs.so and is dlopen'ed on first use, named and found the
way mount.lustre's own mount_osd_ldiskfs.so is, so a client build has
no plugin and the call answers ENOTSUP. Three refusals get three
answers, because a caller keys off them: ENOTSUP where there is no
backend to ask, ENOPKG where the backend is there and refused the
target -- stock e2fsprogs will not open a filesystem with dirdata set,
and the package to install is the answer -- and EINVAL for a device
that is not a Lustre target at all. Giving the first two the same
errno would have conf-sanity test_300 skip itself on a node carrying a
real MDT and a build that can scan it.
The record grows what a device answers for and a walk does not: the
object id, the inode generation, the LMA flags, the raw linkea and a
class. The label is read the four ways mkfs.lustre writes it -- '-',
':', '=' and the '+' that tunefs.lustre --nolocallogs leaves on disk
while the target is stopped, which is exactly when this is what there
is to read it with.
Two more pieces of public API, so the diff holds no surprises. struct
llapi_scan_stats is what the new lfsp_stats points at: the caller sets
ss_size and owns the storage, the scan fills the rest on return, and
the man page describes it as part of the answer rather than as a
diagnostic. And LLAPI_SCAN_F_INTERNAL asks for the objects a target
holds that its namespace never shows -- the OSD's own, the DNE agent
inodes, the ones with no LMA -- which a scan otherwise counts in
ss_class and keeps back. Without it "every object on this device" and
"every object a walk would find" would be the same question, and only
one of them is what a target scan is for.
The counters are one accounting rather than several: ss_seen is
incremented once per object, in the pre-filter, so a skip the backend
raises after that point adds to ss_skipped alone. ss_seen equals
ss_filtered plus ss_skipped plus the sum of ss_class on any completed
scan, and llapi_scan_device_test asserts it -- an inequality would not
have caught counting the same object on both sides of the pre-filter.
An object with no LMA is answered with its IGIF rather than with the
parts to build one from. The inode number and generation of such an
object are its FID -- osd_scrub.c builds the same one, and it is not a
reconstruction, which is LFSCK's job -- so lfsr_fid carries it and
LLAPI_SCAN_FID is set. That leaves the generation with no consumer of
its own and it is not a field: what remains is lfsr_ino, which every
object has and which names the ones no IGIF can, a ZFS object id being
wider than an IGIF holds. There is no LLAPI_SCAN_GEN bit for it.
llapi_scan_namespace.3 gains the pointer to this page here rather than
in the patch that wrote it, where it would have referred to a page the
tree did not yet have.
A worker that cannot be initialised degrades the scan rather than
failing it, once one worker exists, as the pthread_create() arm beside
it does: a backend opens the device again per worker,
lfsp_thread_count is a __u8, and the cursor feeds whatever workers
there are, so running out of them is not a reason to turn a complete
result into an error.
The page says what stx_attributes carries across scanners. A device
scan narrows the inode's own flags to the set llite also declares, so
one file answers "lfs find -attrs d" the same way whichever scanner
ran, and so_attrs_mask declares that set in the record's
stx_attributes_mask -- which is what lets a consumer tell "not
immutable" from "this scanner cannot say". ERRORS also spells out the
two EINVAL cases a caller is most likely to trip, a short lfsp_size
and a short ss_size.
A directory's own stripe takes LMV_MAGIC_V1. LMV_USER_MAGIC is how
the tree marks a *default* LMV: cb_get_dirstripe() sets it exactly
when fp_get_default_lmv is asked for and LMV_MAGIC_V1 otherwise, and a
default lives in trusted.dmv, which is never read here -- trusted.lmv
is the directory's actual stripe. llite fills LMV_MAGIC_V1 for
LL_IOC_LMV_GETSTRIPE, so LMV_USER_MAGIC here would have the two
scanners answer one striped directory with two different values in the
same field -- the opposite of what lfsr_lmv promises, which is that
the field means one thing whichever scanner filled it.
No in-tree consumer reaches that difference from a scan record yet, so
this is a contract broken rather than a wrong answer observed.
lmv_dump_user_lmm() is what it would break: the magic decides its
"(Default)" prefix and which fields a bare -v shows, and
llapi_lov_dump_user_lmm() accepts both values, so a caller pointed at
a scan record instead of an ioctl reply would print a device scan's
directories as defaults. The size is unchanged either way,
lmv_user_md_size() growing only for LMV_USER_MAGIC_SPECIFIC.
A parent FID the scan will not vouch for is left cleared rather than
written: LLAPI_SCAN_PARENT is set only where fid_is_sane() accepts
what trusted.link held, and the field follows the bit, so a caller
reading it without testing lfsr_valid does not get the raw xattr.
llapi_scan_device.3 gives lfsr_parent_fid an entry of its own, since
LLAPI_SCAN_PARENT is both what a caller puts in lfsp_want and what it
has to test in lfsr_valid, and a field named in neither place is a
field a caller cannot use.
run_tests() in lustre/tests/llapi_test_utils.{c,h} is split so that
run_test_tbl() takes the table: the new test is handed a device where
the others take a mountpoint. llapi_scan_test, llapi_root_test and
llapi_pool_test share that helper, which is why the split is worth
naming here rather than left to be read out of the diff.
so_flags is narrowed to the attributes a namespace scan can also
report. ll_dir_ioctl() puts only IMMUTABLE, APPEND and, under
HAVE_LUSTRE_CRYPTO, ENCRYPTED into stx_attributes_mask, and
llapi_scan_namespace() takes stx_attributes from that mask -- so
reporting COMPR and NODUMP here would make one file answer "lfs find
--attrs d" differently depending on which scanner ran, where
stx_attributes is meant to mean one thing whichever filled it. The
bit is on the MDT inode either way: making it answerable is llite's to
do, for both scanners at once.
so_attrs_mask carries that same set into the record's
stx_attributes_mask, rather than the scanner assuming one set for
every backend: a bit clear there is "cannot say", which is a different
answer from the attribute itself being clear. It is carved from
so_padding, so every offset above it is unchanged, and a backend that
leaves it zero has its records leave LLAPI_SCAN_ATTRS clear rather
than claim attributes nothing vouches for.
scan_lmv_to_user() clears the shard area, not just the header. One
buffer serves every object a worker meets, and a directory's converted
LMV carries the real lum_stripe_count while the size answered is
header-only -- the shard FIDs being left out on purpose, an MDT index
needing an FLD lookup this has no client to make. A consumer sizing
lum_objects[] by the count rather than by lfsr_lmvsize, which
lmv_dump_user_lmm() does, then read whatever the last object left
there: a preceding foreign directory's opaque value, in full. The
clear now reaches as far as such a read can, bounded by the room
before a count off a device measures anything, so what it finds is
zeroes. lfsr_lmvsize is the measure either way, and the header says
so.
The buffer is a union of the two structures it holds rather than a
bare char[], which carries neither one's alignment while the call site
casts to both.
An unreadable inode is counted by the chunk that owns it and no
other. The skip arm continued without asking either question the
readable path asks below it, so an inode just past end_ino was
consumed by this chunk and read again by the chunk starting there,
and ss_seen and ss_skipped each took it twice; a reserved inode that
could not be read was counted as an object, where a readable one is
not. ext2fs_get_next_inode_full() assigns *ino before returning any
of the three errors this arm catches -- unlike the errors above it,
which leave it untouched -- so both questions can be asked here.
Nothing was ever delivered twice: it was the accounting that
overstated, which is what the counters exist to be trusted for.
fid_is_root() moves to the UAPI header and the scan calls it. It was
in lustre_fid.h by history rather than by need: LU_ROOT_FID and
lu_fid_eq() are both already public, so the helper is a one-line
predicate over two things userspace has, and userspace now has the
same question to ask -- a scan of a target has to tell the root from
the rest of FID_SEQ_ROOT. unlikely() is dropped, that header being
compiled in userspace too, and the definition is removed from
lustre_fid.h, which includes the UAPI one, so every existing caller in
llite, lmv, lod, mdd and mdt is unaffected.
That also settles a test that had drifted. lu_fid_eq() is a
whole-struct compare, so a hand-written test of f_seq and f_oid called
a FID in FID_SEQ_ROOT with FID_OID_ROOT and a non-zero f_ver the root,
where fid_is_namespace_visible() on the MDT would not. The narrower
point the spelling exists for is unchanged: the echo client's root
shares the sequence and is still not visible.
Both scanners fill lfsp_stats. The structure arrives with this patch,
which is why the walk did not fill it before -- the field came in with
the code that needed it, not with the walk -- and there is no reason
for it to stay that way: a walk counts an object seen once, then
filtered, skipped, or delivered and namespace-visible, which is every
class a walk can meet, and the same seen == filtered + skipped +
sum(class) holds. It counts atomically rather than per-worker,
llapi_scan_namespace() running its callback on every thread at once
where the device scan merges per-worker counters at the end.
lfsp_stats is checked against LLAPI_SCAN_STATS_MIN_SIZE there too, and
after the copy-in for the same reason: a short lfsp_size may not reach
the field.
The struct comment says what the field is rather than which scanners
use it. llapi_scan_device.3 also says the stats structure is
extensible -- ss_size negotiates it exactly as lfsp_size does, so
counters appended later leave an older caller reading its own fields
-- which was true and written down nowhere.
The struct comments describe the fields rather than which scanner uses
them. What each scanner offers lfsp_filter, and what each takes 0 in
lfsp_want to mean, were spelled out in the header and again on both
pages, and the header's copy is the one that goes stale: it claimed a
device scan "offers no path and no name", which LU-20637 makes untrue
two patches later by naming an OST object from the parent FID in
trusted.fid. What is left is the part that does not change -- the
filter sees only what the scan already had, and 0 means a default the
two scanners do not share -- with each page saying which fields that
is for itself. llapi_scan_device.3 gains the one sentence it was
missing, what 0 means there.
stack_trap is called without EXIT, which is its default, and the
setupall after stopall carries no justification -- it is the ordinary
shape and needs none. The base tree passes an explicit EXIT eighteen
times of ninety-two; this series had added thirteen more, so dropping
ours leaves the file at the eighteen that were already there.
The EXAMPLES fragment escapes its newline with a single backslash.
troff reads that as a register reference, so the page rendered
printf("%llu files; %llu seen, %llu skipped0, with the escape and the
closing quote swallowed. It needs the doubled form that
llapi_fid_parse.3 and llapi_scan_rec_path.3 already use.
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools
Change-Id: I8a28a8173b0b8f03ba8f9dc5a58a821118ac6296
LU-20613 llapi: ZFS backend for llapi_scan_device Add scan_osd_zfs.so, the ZFS half of llapi_scan_device(), which reads objects out of a pool through libzpool the way zdb does, behind the same llapi_scan_sink the ldiskfs backend is behind. A pool that is still imported answers EBUSY rather than being read underneath its server: reading an imported pool from outside is not supported, so scan a snapshot, an exported pool or a failover partner's copy. sp_search names the directories to look in for a pool's vdevs, since a pool on file vdevs is invisible to a scan of /dev. A dataset that holds no ZPL objects -- a zvol, or a snapshot of one -- answers EMEDIUMTYPE, which is this backend refusing this dataset and not the ENOTSUP the library returns when it has no backend to ask at all. The ldiskfs backend separates the same two with ENOPKG; a caller that skips its work where a build cannot scan must not also skip it where the build can and the target was misnamed. conf-sanity 300 is extended to cover it rather than left ldiskfs-only: its gate becomes ldiskfs-or-zfs, the OST half keys off ost1_FSTYPE == mds1_FSTYPE, and both facets grow an export_zpool and import_zpool pair around the scan, because a pool still imported by its server answers EBUSY. -p and --search are passed so a pool on file vdevs is found. Its own autotest still cannot exercise that half: the build hosts answer no to "zfs libzpool headers usable from userspace", so they install no scan_osd_zfs.so and the ZFS arm of 300 skips there. The Test-Parameters lines below ask for the two sessions that can say something. lustre.spec.in changes with it: %files osd-zfs-mount becomes a generated lustre-osd-zfs-mount.files, carrying its %dir and zed.d entries over, because the plugin is built only when configure found libzpool and a static file list cannot say that. A leading slash separates a path from a dataset name, and that one rule settles where a name goes and which errno comes back. A name beginning with '/' is a path: scan_device_exists() stats it and returns -ENOENT, -ENOTDIR, -ELOOP or -EACCES before any backend is chosen, so a mistyped device answers the error that happened even on a build with no ZFS in it. A name without one is a dataset, which is what the ZFS backend takes, and never reaches the stat. So scan_backend_kind() needs no slash case of its own, and -ENOTSUP keeps the meaning llapi_scan_device.3 gives it and conf-sanity 300 keys its skip on: this build has no backend for the target. SCAN_CHUNKS_MAX is DN_MAX_OBJECT from <sys/dnode.h>, the bound the walk needs, so a ZFS that widens the object id carries this along with it. -EBUSY names both of the things that raise it: a pool another mount holds, and a dataset already owned within this process, which is a second scan of the same target. dmu_objset_own() answers the same errno through dsl_dataset_tryown() for the second, where the pool is exported and there is nothing to export. llapi_scan_device.3 says the same. Test-Parameters: testlist=sanity env=ONLY=157c Test-Parameters: fstype=zfs testlist=conf-sanity env=ONLY=300 Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com> Assisted-by: ClaudeCode:Fable-5 llm_code_and_review_tools Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools Change-Id: I97a6d8815924b2671c818152f42d66cc580a609d
LU-20611 utils: lfind, find over a target Add lfind(8), a server-side command. It runs lfs find's predicates against a Lustre target's own objects, read straight off the device on the MDS or OSS whose disks hold it, and it is built under SERVER so a client package does not carry a command with nothing to read. Not a mode on lfs find, which is a client command by construction: llapi_is_lustre_mnt() requires ":/" in the mount's device name, so a server target mount is deliberately not recognised, and lfs ships in lustre-client while the scan backend does not. The predicates are the same ones, not a second copy. What differs is in lfind(8): it prints FIDs because a target has no paths, one invocation covers one target, and a predicate the target cannot answer is refused rather than quietly matching nothing. The target is named with --device, --target, --local, or as a plain argument; naming two is an error rather than a scan of the first. conf-sanity 300 grows the other half of the same question, which is a third of this diff. It already compared a target scan's FIDs against what a client walk sees; now it runs lfind over that scan with a predicate -- --type f -- and requires every regular file the client listed to come back and no directory with it, taking the subdirectory's own FID from the client first because after stopall there is nothing left to ask. The test is renamed to say so. Its capability gate moves rather than goes. The old check was [[ -x $scanner ]] against the local build tree, which is the wrong node: the scanner and lfind both run on mds1, so both are now checked there, with do_facet mds1 "test -x $scanner" and a which for lfind. Both are tested up front rather than around the block that uses them, so a build without either leaves a skip in the results instead of a pass that ran none of the predicate half. main() seeds the parameter block through lfs_find_parse_init() rather than repeating lfs_find()'s initialiser, which is the second copy that would otherwise have to be kept in step -- and was already one field short of it here, fp_min_depth having been left to the designated initialiser rather than named. main() clears opterr, as lfs.c's does. lfs_find_parse() prints its own diagnostic for an unrecognised option and asks the caller for opterr == 0 so getopt_long_only() does not print a second one; glibc defaults it to 1, so every option error here was printed twice where the same mistake under lfs printed it once. optind wants no such setting: lfind_parse_target() compares strings rather than calling getopt(), so it is still 1 when the parser is reached. conf-sanity 300 covers the three selectors this adds. --internal is the same scan without the class filter, so the answer without it has to be a subset and strictly smaller -- an MDT that reported none of its own objects would pass a count test and fail the point. --local and --target are checked where the suite has already stopped everything: each has to name the target it could not find rather than scan something else, or report an empty node as an empty filesystem. Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com> Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools Change-Id: Ib25109943ad40afbc2d272a65e70c364c376b2b7
LU-20605 llapi: build find on the scan record
Rebuild the front half of cb_find_init() -- what the directory entry
settles, what to ask the MDT for, and the asking -- on the code
llapi_scan_namespace() uses, so lfs find and the scanner API share one
gather path and one record. scan_rec_dirent() fills what the entry
alone says, find_prefilter() decides what that is enough to reject,
find_want() turns the search into a demand mask, and scan_rec_gather()
asks the MDT for what the mask names.
Four behaviour deltas. The directory-stripe checks now run after the
stat ioctl, so a directory they reject costs one ioctl it did not
before, and the unstriped --foreign shortcut now prints this object's
stat attributes under -printf rather than the previous object's -- it
still takes the same early exit as before, so a project id it did not
fetch and an OST size it did not glimpse are unchanged. An MDT index
lfs find cannot fetch no longer fails the object. And a directory
that is not on Lustre answers ENOTTY, which is now treated as an
answer rather than an error, so the walk descends into it instead of
losing the subtree -- four paths recovered in a tmpfs-under-Lustre
test.
Two of those are bug fixes rather than deltas, and both became
reachable for an ordinary search when -printf put gather_all into the
cb_get_dirstripe() condition.
The last one is why printf_format_lustre() changes too: an object off
Lustre now reaches %LF and the file-layout fetch, and neither should
call that an error. The layout arm tests ENOTTY, which is the
convention llapi_layout_get_by_fd() implements by mapping EOPNOTSUPP
onto it. llapi_path2fid() does not follow that convention -- its only
ENOTTY is the EOVERFLOW arm of llapi_fd2fid(), which means a Lustre
handle would have fit -- so an object off Lustre arrives as ENODATA
from lgetxattr(trusted.lma), or ENOTSUP without server support, and
the %LF arm tests all three. Making llapi_fd2fid() follow the
convention would be the better fix, but it changes a public call for
every caller and belongs in its own patch.
The project-id hunks are two more deltas. get_projid() stops logging
on ENOTTY, and cb_find_init() now decodes that answer, so --projid N
rejects an object with no project id at all instead of failing the
walk, ! --projid N accepts it, and -printf %LP prints 0 for it.
An object with no project id is answered by the negated form only:
comparing the stand-in 0 against the number asked for made --projid 0
and ! --projid 0 both reject it, where the arm says the negated form
can always be answered.
One delta not listed above, in -name. It now tests rec->lfsr_name,
which scan_rec_dirent() sets to the whole path when the last '/' is a
trailing one; the old code took everything after the last '/', which
is "" for a start point written /mnt/lustre/. So --name '*lustre*'
now matches that start point where it did not, and --name '*/' too.
Neither is find(1), which tests the basename with the slash stripped
and so matches --name lustre and not --name '*/'; the value of
lfsr_name for a trailing-slash start point is worth settling on its
own rather than inside this patch.
sanity 56El covers the recovered subtree: a tmpfs mounted under the
Lustre mount, three files below it, and a walk that must both find
them and say nothing on stderr while printing %LF and %Lc for them.
The stderr half is the half that fails without the ENODATA arm above.
sanity 56* is unchanged before and after.
find_prefilter() answers a bool. Nothing in it can fail -- it is
fnmatch() and a mode test, with no allocation and no I/O -- so there
is
no errno for it to return, and a bool is the one type a caller cannot
mistake for one. true rejects the object here; false goes on to
gather.
Fixes: 6b8e97b76c47 ("LU-10378 utils: add formatted printf to lfs find")
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools
Change-Id: Ica2b5a8bd7a55f8ee00bd55c5b7771bb09a208fc
LU-20603 llapi: namespace scanner API Add llapi_scan_namespace(), which walks a mounted filesystem and hands a consumer one record per object rather than a formatted line. The record is versioned twice over: lfsp_size says how much of the parameter block the caller built, and a validity mask says which fields the scanner could answer for, so a field it could not fetch leaves its bit clear instead of reading as zero. lfsp_want is a demand mask, so a consumer pays only for what it asks for -- a name test costs no ioctl. An lfsp_flags bit this library does not define is refused, because setting a flag leaves lfsp_size unchanged and nothing else would keep a newer caller's flag from being ignored in silence. The reserved bytes are must-be-zero for the same reason: they are already counted in lfsp_size, so a field carved out of them later can only be refused here, never noticed. A foreign directory's LMV is left alone. LL_IOC_LMV_GETSTRIPE answers 0 with a struct lmv_foreign_md in the same buffer, so fp_get_lmv is set for one, and lum_stripe_offset and lfm_type both sit at offset 8, so storing the MDT index there would land on the foreign type. The record publishes that buffer as lfsr_lmv with LLAPI_SCAN_LMV_FOREIGN set, so a consumer reading exactly what the bit promises would have got an MDT index where the type belongs. A stat answers for an object the ioctl could not, and lmd_fid then holds whatever was written there last: for the walk, the name it passed to the ioctl, which fid_is_sane() reads as an IGIF. So a record off Lustre came back with LLAPI_SCAN_FID set and a FID built from the object's own name, [0x676962:0x0:0x0] for a file called "big". Cleared where the stat answer is built, in convert_lmd_statx(), which is the fallback's last step and also runs on the V1-ioctl path, so both callers are covered by the one clear. llapi_scan_get_lmv() clears lum_pool_name for the same reason. A directory that answers ENODATA or ENOTTY keeps the caller's one lmv buffer, and the four fields a consumer reads for shape are zeroed there; the pool name was not, so what stayed was the previous directory's. No consumer can observe that yet. A directory with no stripe of its own leaves lfsr_lmv NULL, so nothing reads the buffer at this patch, and the clear is what makes the helper safe to share rather than a wrong answer being fixed. cb_find_init() fills the lmv itself here and joins the helper in LU-20605, which is where the clear first reaches a caller -- lfs find -printf %Lp. That makes the HLD's POSIX Input Scanner a contract rather than an accident: the same scanner with a different attribute source, filling what a stat answers and leaving FID, layout, LMV, MDT index and HSM clear. test10 asserts both halves on a tree that is not Lustre, and skips itself where the tree it was given is Lustre, the FID and the layout being present there for a good reason. The copy-in takes whole fields only. lfsp_size is the caller's and the range test alone does not bound it to a field: lfsp_filter is a function pointer ending at 32 while the minimum is 24, so a size of 25..31 passed and memcpy left the pointer built from some of the caller's bytes and some zeros -- which the scan then calls. scan_param_whole() rounds the length down to the last field that fits, which is what a short size already means, and the field it stops inside is exactly the one to drop. get_projid() loses its static and moves into lustreapi_internal.h. The gather needs it, and it is the only function this patch takes out of liblustreapi_pfind.c's file scope. Its own per-object messages drop to LLAPI_MSG_DEBUG at the same time, marking them diagnostic: a scan calls it once per object where lfs find called it once per match, and scan_rec_gather() answers the failure by leaving LLAPI_SCAN_PROJID clear rather than by reporting. The level does not quiet them by itself -- llapi_msg_level starts at LLAPI_MSG_MAX and neither lfs nor lfind lowers it -- so what changes for those two is the dropped "warning: " prefix; what the level buys is a caller able to silence them with llapi_msg_set_level(). STATX_INO joins LLAPI_SCAN_MDT_MASK. LLAPI_SCAN_STATX_MASK promises the whole low half whether this API names the bit or not, and the ioctl fills stx_ino, but nothing else does and the bit was in neither that mask nor LLAPI_SCAN_DIRENT_MASK -- so lfsp_want = STATX_INO alone returned before any fetch and the field arrived zero with its bit clear, while STATX_INO|STATX_SIZE answered it. The lmd_fid clear moves out of convert_lmd_statx() to the two callers that have no FID to give. The third, cb_find_init() under gather_all, runs after the V2 ioctl has put a real one there, so clearing inside the shared helper threw it away. Nothing reads lmd_fid on that path today; the patch after this one moves lfs find onto the record, where lfsr_fid is filled from it. llapi_scan_namespace.3 says how long each part of a record lasts, rather than only that it does not outlive the callback. The answers differ and two are shorter than that rule reads: lfsr_name points into lfsr_path rather than beside it, and a directory's lfsr_path is rewritten by its own descent before any callback below it runs, while lfsr_fd for anything but a directory is closed the moment the callback returns. The layout and LMV buffers last longer than the rule instead, until the next object is gathered, which is the more dangerous half -- code that keeps them works until it does not. The record begins with its statx, so a consumer can cast it to a struct statx and read the Lustre fields past it. Two static_asserts hold that: lfsr_stx at offset 0 and lstatx_t exactly 256 bytes, so a foreign binding that mirrors the layout by hand is caught at compile time rather than reading every field at the wrong offset. lfsp_size no longer gates. A caller built against a longer parameter structure is accepted when the bytes past this library's definition are zero, rather than refused: the size says what the caller knows, not which version it must be. lfsp_want becomes advisory the same way -- a bit this library does not know, or knows but this scan cannot answer for, is dropped rather than refused -- and lfsp_got reports the subset that was answered, so an application built against a newer header can ask an older library for everything it might want and find out what it got. lfsr_padding names the hole the compiler was leaving. Three consecutive __u32 before a pointer left four bytes unnamed inside lfsr_size, and a field carved out of them later can only be used if a consumer could not have been reading something else there. The prefixes are lfsr_ and lfsp_ rather than sr_ and sp_. sp_ is already md_op_spec's and ptlrpc_sec_policy's, and a public header should not make a reader guess which one a field belongs to. Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com> Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools Change-Id: I0939ae4ea94e094f93c738027c9c6662e6a4f780
LU-20611 llapi: run find over a device scan Add llapi_find_device(), which puts llapi_scan_device()'s records through the same checks llapi_find() applies to a namespace walk. The checks are not reimplemented: the record is presented as the lov_user_mds_data every find_check_* helper already reads. A field the scan could not answer for is left out of stx_mask, as llite leaves it out when the MDT's answer is not authoritative. Both the layout and the directory-stripe buffers live for the whole scan, so an object without one has to say so rather than inherit the last one's. A search needing something a target scan has not -- a path (-printf), a mounted filesystem (--ost, --mdt), an xattr read, or a walk to describe (--maxdepth, --threads) -- is refused before the scan starts, because afterwards "no matches" and "cannot answer" look alike. A field the inode format has no room for, a project id or a birth time, counts as undecided and is reported at the end rather than ending the search. sp_filter is cleared with the other fields the search decides: there is no caller data to pair a callback with, since the scan carries this function's own state. The layout is bounded before it is swabbed, not after. layout_swab_lov_user_md() walks lcm_entry_count and stores through lcme_offset, both read from bytes that came off a target something may have been writing, so find_lmm_fits() has to see them first. It reads every field through whichever byte order it found, so running ahead of the swab costs it nothing. That is the order llapi_layout_get_by_xattr() uses on the same bytes. A top-level foreign magic in the target's byte order is refused there rather than accepted, because layout_swab_lov_user_md() does not swab one: accepting it would leave the EA swapped with lmd_lmmsize set, and --foreign would then miss every HSM-released or PCC file on a big- endian host. Refusing sends it down the no-layout path instead. The floor that check starts from is per-magic, not a blanket sizeof(lov_user_md_v1). A foreign layout's is the smaller offsetof(lov_foreign_md, lfm_value): llapi_file_create_foreign() accepts any non-zero length and lod_verify_striping() checks the foreign magic before the 32-byte test that covers the normal ones, so `lfs setstripe --foreign=none --xattr=abc` stores a legal 19-byte trusted.lov. A blanket floor refused it before the magic was read, sending the file down the no-layout path to be answered off a forged default -- so --foreign missed it and --stripe-count answered from the forgery, which is the silent wrong answer the SEL and SPECIFIC arms below exist to avoid. Only lmm_magic is read before the switch, and every arm carries its own floor. Three shapes only a target scan meets are accepted there. A directory's default layout has lmm_stripe_count set and no lmm_objects[] at all -- lod_gen_component_ea() sizes the EA with op_count, 0 for a directory -- so `lfs setstripe -c 4 dir` is 32 bytes claiming four stripes, and bounding by the count refused it. The array is measured by the buffer instead, as llapi_layout_objects_in_lum() measures it. LOV_MAGIC_SEL and LOV_USER_MAGIC_SPECIFIC are the other two: lod writes both and rewrites neither for a client, so they reached the no-layout path and every --stripe-*, --pool and --component-* answer for such an object was compared against a forged filesystem default. Both are given the form a client would have been shown, SEL as COMP_V1 and SPECIFIC as V3, so the checks below read one shape. The linkea side of the diff. scan_linkea() gains a scan_linkea_entry() that indexes into trusted.link rather than always reading the first record, exported through lustreapi_internal.h, and find_device_prefilter() runs --name against every name the object has. A walk meets a hardlinked file once per link and tests the name it arrived by; a scan meets the object once, so testing one name would answer for a file by whichever link the linkea happened to hold first. The object is still reported once. llapi_find_device.3 says so beside the size and time notes, those being the other places a target scan answers differently. That changes one thing worth naming: a linkea whose header parses but whose first entry does not no longer sets LLAPI_SCAN_LINKEA, where it used to. A record claiming a linkea nothing can read is worse than one admitting it has none, and --name now has entries beyond the first to lose. A torn linkea further in is the same case and is counted the same way. leh_reccount is capped by the smallest entry that could fit, so a buffer claiming three names can spell one and still report three; the loop over the names then ran out partway and fell through to a rejection, reporting the object as a non-match on a name list it never finished reading. It asks the other predicates and counts the object undecided instead, which is what the no-name case beside it does. find_device_prefilter() asks the other predicates before calling an object undecided. Answering 2 for "no name anywhere" without running them would, over an OST target -- where no record has a linkea -- count every data object undecided for `-name X -type d` even though -type rejects all of them, and llapi_find_device() would then warn that N objects could not be decided for a search whose answer was complete. Every other undecided point sits after the checks that precede it, and this one does too. find_prefilter() leaves -name alone for a record with no name, so asking it costs nothing and answers exactly the other predicates. It has to: fnmatch() dereferences its string argument, so testing a NULL sr_name would fault rather than not match, and an OST data object reaches it that way -- the object carries no linkea, and neither do the CLS_INTERNAL, CLS_NO_LMA and CLS_AGENT records LLAPI_SCAN_F_INTERNAL delivers. A foreign directory answers --links here. find_decide()'s nlink gate reads fp_lmv_md->lum_stripe_count to decide whether a stat is needed, and lmv_foreign_md's lfm_length sits at that offset, so a foreign LMV's value length was read as a stripe count and asked for one. A walk pays the stat and answers correctly; a scan has neither path nor descriptor, so the object went undecided and was dropped out of both --links N and its negation. The gate now tests lmv_is_foreign() first, as the --mdt-count, --hash-type and --hash-flag arms beside it already do. llapi_find_device.3 describes an object by its FID, and by its object id where there is no FID. It does not describe one with neither: the scan sets the object id on every record, so the second form always applies where the first does not. Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com> Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools Change-Id: Ibf2dcf71e5444914f306ad37a551262cddd8e1de
LU-20605 llapi: trim a start point's trailing slashes -name tests rec->lfsr_name, and lfsr_name points into the walked path, so for a start point spelled "/mnt/lustre/d/" the basename is not a suffix of the string and no pointer into it spells "d". It goes in llapi_find_with_cb(), which is the one way into the traversal: llapi_find() reaches it and so does llapi_scan_namespace(), which had been carrying the loop itself since the scanner landed. One place, so the two cannot answer differently for the same spelling, and an external caller of the exported walk gets the fix without asking. llapi_scan_fid() and llapi_find_since() keep loops that look like this one and are not: neither walks anything. They trim a mount path and a caller's spelling before *composing* a name from it, where a trailing slash doubles a separator rather than hiding a basename. Folding them into the traversal would be a shape match, not a common cause. This is not a regression of the record work. Measured against a stock lfs built from this series' base, 2.17.57_43_g5afbab2: for a trailing-slash start point stock matches nothing at all, its fname being "", so against find(1) stock answers 1 of 3 patterns correctly and the record-based -name also answers 1 of 3 -- different wrong answers rather than worse ones. With the trim it is 3 of 3. The caller's buffer is left alone, since it is argv in lfs(1), and both param_callback() and pfind_param_callback() copy what they are given. Trimming also removes the doubled separator from every child line below such a start point, which brings them to what find(1) prints. The one remaining difference is the start-point line itself: find(1) echoes the spelling it was given and this prints the trimmed form. That separator is composed by an unconditional strcat(path, "/") in llapi_semantic_traverse() which predates this series and is unchanged by it -- stock doubles it identically, including for "/" -- so making the start-point line agree as well belongs to a fix for that, not here. Covered out of tree for now, over all three spellings and the root: 13 cases, validated against the stock build, where 7 of them fail while the no-trailing-slash control passes either way. A sanity case for it is worth adding. llapi_find() answers -ENAMETOOLONG for a path that does not fit, where param_callback() answered -EINVAL for the same path further in. The limit is unchanged and the diagnostic naming the path is kept; only the errno a caller sees is better. Test-Parameters: trivial Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com> Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools Change-Id: I01729e3816785c298fd44f9fa93596d578834883
LU-20624 utils: fix stale fd in cb_get_dirstripe
cb_get_dirstripe() closed the descriptor it was given and stored an
O_NOFOLLOW reopen through its argument on the ENOTTY retry, which
fires for a foreign file or directory and for any directory not on
Lustre. cb_find_init() and cb_getstripe() pass the address of a local
copy, so llapi_semantic_traverse() kept a number that had been closed:
it closed that number a second time on the way out and leaked the
reopen. The second close is the dangerous one, since with lfs find
--threads the number may by then belong to another worker.
Keep the reopen on the descriptor the caller already holds, with
dup2(), rather than asking every caller to notice that its copy was
replaced.
The number no longer changes, so the parameter is read only and the
signature now says so: int rather than int *. The pointer advertised
exactly the swap that caused this, and an assignment added back
through it would break the same callers in the same way without any
of them having to change.
Not fixed here: for a directory off Lustre the retried ioctl answers
ENOTTY as well and cb_find_init() returns that error, so the subtree
is lost to the error return rather than to the descriptor. Measured
with strace on the main process: lfs getdirstripe -r goes from one
EBADF close to none.
Fixes: 15d44e787e17 ("LU-12682 llite: fake symlink type of foreign file/dir")
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools
Change-Id: I19a3d62d231e9a932fba12a4d56fbbe691912cc9
LU-20611 llapi: split cb_find_init's decider out Move the deciding half of cb_find_init() into find_decide(), taking a struct find_ctx instead of the traversal's locals, so a caller holding a record and no path can reach the same predicates. What the traversal used to read from its own stack -- the path, the parent and directory descriptors, the stripe count, whether the object is on Lustre -- the context carries, and a caller with none of it leaves them NULL or -1 rather than being unable to call at all. The project id comes out with it, as find_get_projid(). A walk opens the object and asks the ioctl; a record that already carries one is answered from it; and a caller with nothing to open is told so, which is the case find_decide() reads as undecided rather than as a project id of zero. No behaviour change: the checks, their order and their early exits are the ones that were there. Line for line against the old function, the only edits are the "goto decided;" turned into "return 0;", the "goto out;" turned into "return ret;", and the declarations that moved into cb_find_init(). find_decide() returns 0 or a negative errno, and nothing else. Whether the object matched is not reported: printing is the answer, and no caller reads it. A positive would be the third meaning of 1 among neighbouring functions here -- find_prefilter() rejects with one, cb_find_init() returns one for "do not descend" -- and it is cb_find_init()'s own ret that this value lands in, safe today only because the "decided:" label overwrites it first. Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com> Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools Change-Id: I6c1550b715bb505a3160c21bb68a0521c7a399da
LU-20611 lfs: share find's predicate parsing
Move lfs find's option table, the loop that reads it and the argument
helpers it calls into lfs_find_parse.c, arranged so lfind can compile
it the way callvpe.c is already shared between lfs and lustre_rsync.
A second copy of that vocabulary would be a second copy that drifts.
Nothing is exported from the library and no ABI is touched.
The helpers move verbatim. The loop moves with four differences and
no others: param. becomes param->, every goto err becomes goto out,
the three out-parameters are initialised before it and pathstart and
pathend are written back after it, and in the --foreign arm the two
returns of CMD_HELP become a goto out -- which is what makes the
stopped flag reliable there and routes the path through
lfs_find_parse_fini().
@pathstartp and @pathendp are the subtle half of the new interface,
because -1 does not mean the same thing in the two: in @pathstartp it
is "no path at all", and in @pathendp it is "no option followed the
paths", which the caller has to read as argc. That is the ordinary
case for "lfs find -type f /mnt/a /mnt/b", and a caller that takes it
literally walks only the first path. The header says so; it is worth
knowing before reading the loop.
Two things come with it. Several failures in that loop give up
without setting ret, so lfs find has always exited 0 after a bad
--comp-flags; the parser now says so through a stopped flag, preserved
rather than fixed. And lfs_find_parse_fini() frees the four
find_param fields a parse can leave on the heap, so a second caller
does not have to copy the cleanup block out of lfs_find().
The input half of the contract is a call, not an initialiser. Two
fields have to arrive non-zero -- fp_max_depth, which llapi_find()
reads, and fp_time_margin, which set_time() does -- so a second front
end handing over a zeroed structure would walk the top directory only
and date a "-mtime +1y" differently. lfs_find_parse_init() sets
those, carries fp_min_depth and fp_quiet as lfs_find() set them, and
zeroes the rest, beside the fini() that was already owed, so what a
caller has to copy across is the call.
An optind of 0 is tolerated rather than mis-read. The interactive
shell's execute_line() sets it to 0 before calling the command, which
getopt() takes as "reinitialise" and answers by scanning from argv[1]
anyway; seeding prev_optind from that 0 made pathstart 0, so
`lfs> find /mnt/lustre -type f` searched argv[0] -- "find" -- and
reported it missing before searching the path. That is upstream's,
lfs_find() having read optind the same way, but the contract this
header states is not true of a caller in tree unless it is fixed.
Fixes: f7ce2a0ae35f ("LU-16944 utils: lfs find: handle multiple paths correctly")
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools
Change-Id: Ifa24de52c0b688f5c107a9efc96ad5274c7ec39a
LU-19982 doc: fix lustreapi.7 SEE ALSO order and AVAILABILITY
Five findings checkpatch-man.pl reports on the page, left once the
subsection warnings stop hiding them. With the subsection fix in and
this patch out, the page draws 1 error, 4 warnings and 4 checks; with
this patch it draws 0 errors, 0 warnings and 1 check.
Two references are out of order. The ordering compares names with '_'
and '-' read as a space, so llapi_fid_hash sorts before llapi_fid2path
and llapi_pcc_state_get before llapi_pccdev_get; both sat after.
AVAILABILITY names '.B lustre (8)'. The reference is to lustre(7),
not to the configuration utilities, and it is a reference, so it takes
.BR. The wording beside it becomes the one llapi_layout.7 uses for
the same library.
The page source in .TH was Lustre where every other page describing
this library says "Lustre User API". The modification date on the same
line moves with it, 2024-08-28 to 2026-09-03, which is the fifth
finding: a page that changes is expected to say when.
The manual section string is left alone: "The Lustre API library"
predates the series and renaming an existing page's header is not ours
to do.
AVAILABILITY says 'commit 0.9.1' where it said 'release 0.9.1'. The
checker's model is a commit version and the release that follows it,
and the line above already gives that release as 0.10.0 -- so the page
was saying two different things about one fact. 0.9.1 is a tag in
this tree:
commit c5050e412572 ("land v0.9.1 on HEAD, in preparation
for a 1.0.x branch")
so there is a commit version to name. That one word alone -- measured
with the other three already applied -- takes the page from 1 error, 0
warnings, 2 checks to 0 errors, 0 warnings, 1 check, because it clears
both the remaining ERROR and the AVAILABILITY_FORMAT_RELEASE check on
its own line. What is left is the manual section string above.
Test-Parameters: trivial
Fixes: 155cf6d41dac ("LU-4315 doc: updating ls-tu man page style")
Fixes: 8e58f4e72b50 ("LU-19983 doc: Add missing llapi_* references to lustreapi.7")
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools
Change-Id: Ib002aa3d2334c492545e35e598b319f6f8abaa1c
LU-20119 selinux: add lustre_helpers SELinux policy module On el10+ kernel-launched helpers run in kernel_t with no usable domain, so l_getsepol, l_getidentity, l_getauth, l_foreign_symlink and lctl are denied the policy and identity-lookup access they need. Add a lustre_helpers policy module transitioning them into lustre_helper_t, and granting keyutils_request_t the socket access NID resolution needs. configure enables it on el10+, overridable with --with/--without-selinux-policy. The el10.0 part-2 and el10.1-server sessions are not requested: both fail for reasons unrelated to SELinux (LU-20542, LU-16330). Test-Parameters: trivial Test-Parameters: testgroup=review-dne-selinux-ssk-part-1 clientdistro=el10.0 Test-Parameters: testgroup=review-dne-selinux-ssk-part-1 clientdistro=el10.1 Test-Parameters: testgroup=review-dne-selinux-ssk-part-2 clientdistro=el10.1 Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com> Change-Id: I1ad770867d2e48be9b72c128d50d2f246f79bdfe
LU-4315 contrib: let SEE ALSO carry subsections
LU-19982 grouped lustreapi.7's references into .SS subsections, each
introduced by a description line and a .PP. checkpatch-man.pl's SEE
ALSO checker accepts only a flat run of '.BR page (N),' lines, so it
reports every one of those description lines and every .PP as a
malformed reference, and then complains that the .PP does not end with
a comma. On lustreapi.7 that is 39 warnings -- 13 description lines
and 13 .PP reported as malformed, and 13 more for the comma -- out of
the 42 the page draws in total, and it is the tool that is wrong: the
grouping was reviewed and landed deliberately.
Treat a .SS inside SEE ALSO as starting its own run of references, and
allow the description line and the .PP that separate it from them.
Ordering is still checked within each run, which is where it means
something; the runs are thematic and not sorted against each other.
The comma and ordering checks now run only when the previous line is
itself a reference. They were reached with a .PP as the previous line
and no reference to compare against, which is what produced the
spurious comma warning -- and, from the two unguarded captures, 39
uninitialized-value warnings from perl. Three on the first reference
of each of the page's 13 subsections, one for $prevfile and two for
$prevnum; not one per reference, of which the page has 141.
$llapi also selects the wording AVAILABILITY is expected to use, and
it matched the filename against llapi alone, so lustreapi.7 was asked
for 'filesystem package' where its sibling llapi_layout.7 is asked for
'user application interface library'.
The macro the section header matched is captured before it is used.
Two regex matches sit between, so $1 held whichever of them last
succeeded rather than SH or SS. Two pages do put a spacing macro
directly before a .SS -- llapi_changelog_recv.3 and lnetctl.8 -- but
both headers are quoted, and the quoted-header match between resets $1
to the right SS before it is read. An *unquoted* .SS after a spacing
macro is the input that reached the bug, and the tree has none.
A .SS also ends the run before it, so its last reference takes the
same no-trailing-comma test as the last reference of the whole
section. The .SS branch returned before the SEE ALSO block could
apply it, so a run ending '.BR aaa (3),' passed silently. And .P and
.LP are exact synonyms of .PP in man(7) -- the script's own EMPTY_LINE
warning asks for .P -- so the preamble takes all three, where a .P was
reported as a malformed reference.
The .PP ends the preamble rather than being skipped alongside it. Any
number of description lines and .PP lines in any order were accepted
until the first reference, so prose sitting after the .PP went
unreported where it used to be caught. Clearing the flag at the .PP
keeps the run boundary the comment describes and still lets the
description through. Nothing in the tree trips the tightened rule:
the 378 pages were swept again with these changes and not one count
moved.
Widening $llapi has one visible consequence before the companion page
fix lands: it also selects the page source .TH is expected to carry,
so lustreapi.7 reports 'Page source Lustre not "Lustre User API"'
until LU-19982 sets it.
Every man page under Documentation was checked before and after: 378
pages, and lustreapi.7 is the only one whose count changes.
Fixes: d15955d374d5 ("LU-4315 doc: add man pages to checkpatch")
Test-Parameters: trivial
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools
Change-Id: I1b2ccbddebc8832e400647133fb447e0417ac600
LU-20238 ec: GFNI-accelerated EC primitives
Add a runtime-dispatched GFNI fast path to libec.a (LU-19465).
On x86_64 with GFNI, ec_encode_data(), ec_encode_data_update(),
ec_init_tables(), gf_vect_dot_prod() and gf_vect_mad() route to
AVX-512 or AVX2 GFNI kernels; everywhere else the existing
ec_*_base() path is used unchanged.
The kernels are C intrinsics (_mm{256,512}_gf2p8affine_epi64_epi8)
gated per-function with __attribute__((target(...))), so the build
needs only GCC >= 8 / clang >= 8 and no NASM. They use only the
GF(2^8) affine instruction and XOR/load/store, not the PSHUFB
nibble-table lookup the ISA-L AVX2/SSE kernels rely on.
The multi-output AVX-512 kernels take the coefficient as an EVEX
{1to8} embedded broadcast to keep it off Port5; compilers will not
emit {1to8} from intrinsics, so a short inline-asm macro is used.
Against ISA-L's NASM kernels this reaches parity on AVX-512 (+1%
to -5%) and matches or beats NASM on 4 of 5 AVX2 benchmarks, the
one slower case by 6%.
A configure probe (EC_GFNI_SUPPORT) compile-tests the intrinsic
and defines LUSTRE_EC_GFNI for the libec.a build only. Every
dispatch block is guarded by the pair defined(LUSTRE_EC_GFNI) &&
!defined(__KERNEL__), so no shipped consumer reaches a GFNI kernel
yet: ec.ko and liblustreapi both take the base path, and the
numbers above compare kernels directly rather than describing a
user-visible speedup. Follow-on patches add the Kbuild plumbing
and the liblustreapi wiring.
gfni_ec_test checks that the GFNI and base paths produce identical
output, and exits 77 without GFNI so a caller can skip instead of
recording a base-against-base pass. It installs with the test
suite; sanity-ec.sh test_0 sweeps geometries and lengths through
it. -i picks the kernel family directly, because the runtime
dispatch always prefers AVX-512 and would otherwise leave the
AVX2 kernels untested on any CPU that has both.
The vendored gf_*_gfni.c kernels are reflowed for lustre
checkpatch; see lustre/utils/erasurecode/GFNI_VENDORED.md.
Test-Parameters: testlist=sanity-ec
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Change-Id: Ia40a743c8e47c018250728668090215c7bd8365b
| unique failing test | history |
|---|---|
| recovery-small@ldiskfs:test_155 | seen in 10 other reviews |
(minor) What is this barrier pairing with? `dev->od_scrub.os_oic` is written here and consumed only by the same scrub thread, via osd_scrub_next()->osd_scrub_exec()->osd_scrub_check_update(); the LFSCK/otable side reads its own it->ooi_cache.ooc_cache[], which osd_preload_next() fills in the LFSCK thread. None of the readers of os_pos_current -- osd_preload_next(), osd_scrub_has_window(), osd_otable_it_next() -- has a matching read-side barrier either. So oic_fid/oic_lid are never published to another thread and nothing observes the ordering. If there is a reader this is meant to pair with, it isn't obvious; otherwise it looks droppable.
(defect) Does publishing the position here close the whole window? The OI mapping for `next` is only repaired afterwards, in osd_scrub_exec()->osd_scrub_check_update(), so this still advertises an inode the scrub has not finished with.
The consumer tests its gate before it advances:
if (scrub->os_running &&
ooc->ooc_pos_preload >= scrub->os_pos_current)
return SCRUB_NEXT_EXIT;
rc = osd_iit_next(param, &ooc->ooc_pos_preload);
A preload sitting below os_pos_current passes the gate and osd_iit_next() then lands it on exactly os_pos_current, where it igets and caches that inode. Because the batch only ends once ooc_pos_preload >= os_pos_current, hitting that inode is how each preload batch normally terminates, not a corner case.
osd_iit_iget() is also the cheap half of the per-inode work: osd_scrub_check_update() does another osd_iget(), osd_oi_lookup() and the osd_scrub_refresh_mapping() transaction. So the larger part of the window is untouched.
Would moving the assignment past osd_scrub_exec(), or restoring the pre-e2ad44de38 order in osd_preload_next(), be the more complete fix?
LU-20505 lfsck: fix osd-scrub vs lfsck race
osd scrub and lfsck share same iterator, where current position is
stored in os_pos_current and corresponding fid/ino in oic_fid/oic_lid.
osd_scrub_next() advances os_pos_current, but oic_fid/oic_lid are
updated later in osd_iit_iget(). the race should go away if we update
os_pos_current after oic_fid/oic_lid.
Test-Parameters: env=ONLY=5,ONLY_REPEAT=50 testlist=sanity-lfsck
Test-Parameters: env=ONLY=5,ONLY_REPEAT=50 testlist=sanity-lfsck
Test-Parameters: env=ONLY=5,ONLY_REPEAT=50 testlist=sanity-lfsck
Test-Parameters: testlist=sanity-lfsck
Test-Parameters: testlist=sanity-lfsck
Test-Parameters: testlist=sanity-lfsck
Fixes: e2ad44de38a4 ("LU-9488 scrub: reset noslot for full speed scrub")
Signed-off-by: Alex Zhuravlev <bzzz@whamcloud.com>
Assisted-by: ClaudeCode:Opus-5
Change-Id: I6f409b40173191efef6931c356a0271df1ae86e4
(minor) it is confusing to me that this is using `p` = parity and `m` = total stripe count. I thought the standard was `k` = data, `m` = parity, as with patch https://review.whamcloud.com/61965 ("LU-12187 lfs: add EC setstripe support with k+m notation").
It would be useful if this printed some information about the local CPU architecture (e.g. `model name` and `cpu MHz` from `/proc/cpuinfo` or equivalent from `lscpu`) and the EC implementation that was selected, so that we have some way to identify the results that are generated. Otherwise, we get a bunch of numbers and no way to identify how they relate.
LU-20016: Simple benchmark tool for EC computations Note that in the ISL-L library, computing parities and recovering lost data stripes is the same operation: ec_encode_data(). The difference is not in the computations but rather the content of the coefficient matrix. The cost of these operations are primarily bound by the number of stripes to compute which amusingly means that recovering a single lost stripe should be FASTER than generating two parities. Test-Parameters: trivial testlist=sanity-ec Signed-off-by: Ronnie Sahlberg <rsahlberg@whamcloud.com> Change-Id: I61f409b631a8f4aefe0ceb2628e56f58f4c3a07c
| unique failing test | history |
|---|---|
| conf-sanity4@ldiskfs+DNE:test_152 | seen in 1 other review |
| failed enforced test | platform | detail | |
|---|---|---|---|
| custom-1001 | RHEL 9.7 / x86_64 | ran 3 tests. 1 tests failed: sanity-krb5. | session |
(style) The body doesn't name any of the symbols it adds or changes, so this is hard to find later with `git log -S`. Worth mentioning `ctx_timeout_work()` and `ctx_upcall_timeout_kr()` by name. Three hunks are also unexplained: `import_set_state_nolock()` loses `static` and gains an `EXPORT_SYMBOL`, and `ptlrpc_abort_inflight()` / `ptlrpc_pinger_wake_up()` gain `EXPORT_SYMBOL` so ptlrpc_gss can reach them. A sentence saying the new work runs in the ptlrpc_gss module and therefore needs those exports would cover them.
(suggestion) ptlrpc_pinger_force() already packages the IMPF_FORCE_VERIFY set_bit with the pinger wakeup, and it is exported and declared in lustre_ha.h, which lustre_net.h already pulls into this file:
void ptlrpc_pinger_force(struct obd_import *imp)
{
set_bit(IMPF_FORCE_VERIFY, imp->imp_flags);
smp_mb__after_atomic();
if (imp->imp_state != LUSTRE_IMP_CONNECTING)
ptlrpc_pinger_wake_up();
}
The state is already DISCON when this runs, so the wakeup would still happen and the behavior would be the same. Using it here, and dropping the set_bit above, would also make the new EXPORT_SYMBOL(ptlrpc_pinger_wake_up) in pinger.c unnecessary, and would pick up the smp_mb__after_atomic() that the neighbouring IMPF_ updates in import.c pair with. If the patch is refreshed, worth considering.
ptlrpc_pinger_force(), already exported, would be better here instead of exporting this new one.
This may trigger a new CONNECT while the current CONNECT, which is timeouting in GSS, is still on imp_send_list. When the GSS context will be refreshed, both CONNECT RPCs will race and the old CONNECT could do unwanted stuff (back to DISCON, eviction, ...).
hmmm, don't you think that this case is covered by the imp_generation and imp_conn_cnt checks ?
> hmmm, don't you think that this case is covered by the imp_generation and imp_conn_cnt checks ?
LU-20296 gss: also force reconnect if ctx nego timeout Force disconnect if a timeout occurs during security context negotiation. But do it in work queue instead in current IRQ/timer context. This will trigger a new connection choice instead of looping on the same wrong one when alternate and working ones could be available. Test-Parameters: kerberos=true testlist=sanity-krb5 Test-Parameters: testgroup=review-dne-selinux-ssk-part-2 Signed-off-by: Bruno Faccini <bfaccini@nvidia.com> Change-Id: I81912a4b0679fa87f35b84c259581dc0493566ea
It would make sense to put an LLM to work on converting the NASM .asm files int GCC .S files. It should be possible to have it iterate on getting the GCC-compiled code to generate the same x86 byte code, or at least compile and run correctly to pass whatever correctness tests exist for this code.
In fact, a quick search shows that NASM already has a tool to do this:
Automatically convert Intel NASM assembly to GNU Assembler (GAS/GASM)
syntax using the intel2gas tool, which converts between NASM and AT&T
syntax, or by utilizing NASM's built-in output capabilities to generate
GAS-compatible objects.
$ intel2gas -i input.asm -o output.s
These generated/converted .S files should be stored alongside the original .asm files in Git. If NASM is available the originals can be compiled, but in the common case where NASM is not available the .S files would be compiled and linked into the kernel modules.
I think it is worthwhile to keep both, since it would be easier to update the .asm files from upstream ISA-L, but it might be some time before the .S files are update to match.
Are these bugs in the upstream ISA-L code that should be pushed back to them?
Similarly, is this code from upstream ISA-L, or a test wrapper that we developed?
This should probably use LU-19905, or change the description of LU-20016?
This table is the same between the x86 and aarch64 patches. It would be useful to fix one or the other to have the correct data for that CPU architecture.
(minor) this should have a warning that the C version may only be 1/20-1/30th as fast, at least until the .S versions are available.
(minor) It would be better to specify which kernel version is non-functional, so that this can become conditional upon a newer kernel (assuming there is a benefit to use SVE-optimized versions).
(minor) Again, please specify kernel versions so that this can be fixed in the future for newer kernel versions.
(style) `bool`?
(style) externs should be avoided in .c files
These warnings are correct and the prototypes should be moved to a .h file that is included here and into the file where these functions are implemented. Having the function prototype in a local .c file prevents the compiler from checking argument type/count against the implementation, and can result in hard-to-find bugs in the future.
(defect?) According to the comments for `kernel_neon_begin()`: ``` * Unless called from non-preemptible task context, @state must point to a * caller provided buffer that will be used to preserve the task's kernel mode * FPSIMD context when it is scheduled out, or if it is interrupted by kernel * mode FPSIMD occurring in softirq context. May be %NULL otherwise. */ void kernel_neon_begin(struct user_fpsimd_state *state) ``` I'm thinking that this would be declared on the stack in e.g. `gf_vect_dot_prod()` and passed to `ec_neon_usable()->kernel_neon_begin()` so that it can also be passed (as required) to `kernel_neon_end()`: ``` * The value of @state must match the value passed to the preceding call to * kernel_neon_begin(). */ void kernel_neon_end(struct user_fpsimd_state *state) ``` The `struct user_fpsimd_state` is 528 bytes, so a _bit_ heavy to put on the stack, but at the same time this is (very likely) to be on a PAGE_SIZE=64KiB system so stack space should be available for this, and it will not have a deep call chain below this point. I think doing a `kmalloc()` and `kfree()` for each call would add noticeable overhead and should be avoided it possible. Alternatives would include having a dedicated slab for this, so that there is a per-CPU cache (528 does not fit into standard slabs well) and it likely has local allocations objects cached in the slab.
(style) should this be changed to `rows >= 6` and remove the `case 6:` in the switch, as is done in `ec_encode_data_neon()` above?
LU-20016 ec: ISA-L SIMD for userspace and kernel
Integrate Intel ISA-L optimized assembly into Lustre's
erasure coding library for both userspace and kernel,
on x86_64 and aarch64. Add debugfs kernel benchmark
for measuring FPU save/restore overhead.
Userspace x86_64 (libec.a):
- 72 NASM assembly files (SSE/AVX/AVX2/AVX-512/GFNI)
- ec_multibinary.asm: runtime CPUID dispatch
- ec_highlevel_func.c: N-vector dispatch layer
- Falls back to C scalar when NASM unavailable
Userspace aarch64 (libec.a):
- NEON + SVE .S assembly + SVE C intrinsics
- ec_aarch64_dispatcher.c: getauxval() dispatch
- ec_aarch64_highlevel_func.c: N-vector dispatch
Kernel x86_64 (ec.ko):
- Pre-assemble ISA-L NASM .asm files into .o,
link into ec.ko (71 assembly objects)
- ec_dispatch.c: boot_cpu_has() selects AVX2/AVX/SSE
at module init, kernel_fpu_begin/end wrapping
- ec_highlevel_func.c: N-vector dispatch layer
- Falls back to C scalar in interrupt context
Kernel aarch64 (ec.ko):
- NEON .S assembly files (GAS format, direct kbuild)
- ec_aarch64_neon.c: kernel_neon_begin/end wrapping
with may_use_simd() check, hwcap detection
- SVE excluded from kernel (toolchain portability)
Build system:
- lustre-erasurecode.m4: detect NASM, aarch64
- erasurecode/autoMakefile.am: three-way dispatch
- utils/Makefile.am: link against libec.a
- ec/Makefile.in: NASM pre-assembly for kernel
Bug fixes in ec_perf_bench.c:
- -p flag was setting k instead of p
- frag_ptrs allocated stripe_size pointers not m
- Inverted exit code and stale rc from getopt
- Use aligned_alloc(64) for data buffers
Kernel benchmark (debugfs):
- /sys/kernel/debug/lustre/ec/benchmark
- 4 phases: FPU overhead, SIMD+FPU per-call,
SIMD+FPU amortized, C scalar baseline
- Input format (write to debugfs entry):
echo 1 > .../ec/benchmark (defaults)
echo "k=5 p=2 s=128" > .../ec/benchmark
cat .../ec/benchmark
Parameters (key=value, space separated):
k data stripes (default 5, max k+p=16)
p parity stripes (default 2)
s stripe size in KB (default 128, max 65536)
"echo 1" runs with all defaults. Unrecognized
input returns -EINVAL. cat shows results or
usage instructions if not yet run.
Fix gf_vect_mul_init naming to use _base suffix
consistently (aligns with ISA-L convention) in
both userspace and kernel ec_base.c.
Performance (5+2, 1 thread, QEMU VM, AVX2):
Stripe | Kernel | Kernel | Userspace | SIMD | Kernel vs
Size | SIMD | Scalar | SIMD | Speedup | Userspace
| | | | in Kernel|
-------|---------|---------|-----------|----------|----------
64 KB | 17099 | 540 | 404345* | 31.7x | 0.04x*
128 KB | 16888 | 531 | 197511* | 31.8x | 0.09x*
256 KB | 17032 | 539 | 98401* | 31.6x | 0.17x*
1 MB | 17142 | 538 | 24830 | 31.9x | 0.69x
4 MB | 10309 | 533 | 5385 | 19.3x | 1.91x
16 MB | 10954 | 534 | 1048 | 20.5x | 10.5x
All throughput in MB/s. (*) Userspace numbers at
small stripes are inflated by cache residency.
At 4-16MB the kernel is faster -- its kvmalloc
pages are physically scattered, matching real
Lustre page cache behavior. Replicating this in
userspace is non-trivial and not worth the effort;
the kernel benchmark is authoritative.
FPU save/restore overhead was measured at 25-54 ns
on this platform (Ryzen 3700X) -- negligible vs
encode time (~39us at 128KB). The benchmark
measures this separately (Phase 1) so it can be
characterized on other hardware where it may be
more significant.
Generated with Claude Code + Tools
Test-Parameters: testlist=sanity-ec
Change-Id: If4c57c328c0e23bb6769dcbc45d52d43b03a7837
Signed-off-by: Patrick Farrell <pfarrell@whamcloud.com>
| failed enforced test | platform | detail | |
|---|---|---|---|
| review-dne-zfs-part-5 failed 2× crashed | RHEL 9.7 / x86_64 | ran 5 tests. 1 tests failed: sanityn. %% THIS TEST SESSION CRASHED %% | session |
(defect) The deprecation paragraph is gone, but this list of known modules still names `nss_files`. After this patch that token no longer has a special case, so `lookup nss_files` falls through to init_nss_lib_module(newmod, "nss_files") and tries to dlopen `libnss_nss_files.so.2`, which fails and exits l_getidentity(1) -- the identity upcall then fails for every user on that MDT.
Should this read `files`? That matches what the removed warning told admins ("Use 'lookup files'"), and `files` has been the working spelling since LU-17139.
(minor) do_warn_interval() was the only user of `struct stat`/stat()/futimens() in this file; nothing left uses <sys/stat.h> now. If the patch is refreshed, this include could go with it.
(defect) This part of the comment describes the alias the patch just deleted -- it still tells the reader that `nss_files` must be selected to enable libnss_files. Since LU-17139 that is done with plain `files`, and after this change `nss_files` is not recognized at all. Can the last sentence be dropped or reworded to say `files` maps to libnss_files and `lustre` to /etc/lustre/passwd,group?
LU-17140 utils: l_getidentity remove 'nss_files' alias Fully remove the old 'nss_files' alias and associated warning code as it is not needed. Signed-off-by: Shaun Tancheff <shaun.tancheff@hpe.com> Change-Id: I32cc37a8cbae7586f8c1d53f72d1f6f21900f1e0
LU-20551 tests: fix persistent sepol cleanup in sanity-selinux
test_21c cleans up with "lctl set_param -P sptlrpc.send_sepol=0",
which does not undo the test: it leaves a persistent MGS config
log entry where there was none before. Clients replay that log on
every mount, so a later test that sets send_sepol locally loses
it on the next mount, then exercises nothing while still passing.
Delete the entry instead, and reset the running value on the
clients. A second "-P" write is racy: "-P -d" marks the matching
sections skipped in place, including a "=0" written moments
earlier, so the restore can be dropped before any node applies
it, leaving send_sepol=-1 for every later suite. send_sepol is a
ptlrpc module global that umount does not reset, and only clients
expose sptlrpc.send_sepol, so the reset is scoped there.
"set_param -P -d" returns an error when there is no entry to
delete. Guard both call sites that run from an EXIT trap, where
errexit is active and a non-zero status skips the rest of the
handler: the new one in test_21c, and remove_nodemap(), which
cleanup_21b calls before test_21b has necessarily set a nodemap
sepol -- there it would leak the nodemap and leave the client
unmounted.
Fixes: b5bbdb49faca ("LU-20046 sptlrpc: expose send_sepol via lctl get/set_param")
Test-Parameters: trivial testgroup=review-dne-selinux-ssk-part-2
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Assisted-by: Claude:Opus-5 claude-code
Change-Id: I53cfb326a16f14a944715dc3f7c76b107e010fe0
Reviewed-on: https://review.whamcloud.com/c/fs/lustre-release/+/67689
Reviewed-by: Andreas Dilger <adilger@thelustrecollective.com>
Tested-by: jenkins <devops@whamcloud.com>
Reviewed-by: Sebastien Buisson <sbuisson@ddn.com>
Reviewed-by: Oleg Drokin <green@whamcloud.com>
Tested-by: Maloo <maloo@whamcloud.com>