Skip to content

Commit f9f39e5

Browse files
Fixing proc/self/mountinfo cosistentRead that is required in the check of isMounted
Signed-off-by: Shashank Pal <[email protected]>
1 parent db9b463 commit f9f39e5

File tree

4 files changed

+530
-140
lines changed

4 files changed

+530
-140
lines changed

utils/mount/mount_linux.go

Lines changed: 220 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,13 @@ func (client *LinuxClient) IsMounted(ctx context.Context, sourceDevice, mountpoi
9999
Logc(ctx).WithFields(logFields).Debug(">>>> mount_linux.IsMounted")
100100
defer Logc(ctx).WithFields(logFields).Debug("<<<< mount_linux.IsMounted")
101101

102+
sourceDevice = strings.TrimPrefix(sourceDevice, "/dev/")
103+
104+
// Ensure at least one arg was specified
105+
if sourceDevice == "" && mountpoint == "" {
106+
return false, errors.New("no device or mountpoint specified")
107+
}
108+
102109
// Get device path if source is already linked
103110
devicePath, err := client.filepath.EvalSymlinks(sourceDevice)
104111
if err != nil {
@@ -108,67 +115,23 @@ func (client *LinuxClient) IsMounted(ctx context.Context, sourceDevice, mountpoi
108115
}
109116
devicePath = strings.TrimPrefix(devicePath, "/dev/")
110117

111-
sourceDevice = strings.TrimPrefix(sourceDevice, "/dev/")
112-
113-
// Ensure at least one arg was specified
114-
if sourceDevice == "" && mountpoint == "" {
115-
return false, errors.New("no device or mountpoint specified")
116-
}
117-
118-
// Read the system mounts
119-
procSelfMountinfo, err := client.ListProcMountinfo()
118+
mountInfo, err := client.ReadMountProcInfo(ctx, mountpoint, sourceDevice, devicePath)
120119
if err != nil {
121-
Logc(ctx).WithFields(logFields).Errorf("checking mounts failed; %s", err)
122-
return false, fmt.Errorf("checking mounts failed; %s", err)
120+
Logc(ctx).WithFields(logFields).WithError(err).Debug("Checking mounts failed.")
121+
return false, err
123122
}
124123

125-
// Check each mount for a match of source device and/or mountpoint
126-
for _, procMount := range procSelfMountinfo {
127-
128-
// If mountpoint was specified and doesn't match proc mount, move on
129-
if mountpoint != "" {
130-
if !strings.Contains(procMount.MountPoint, mountpoint) {
131-
continue
132-
}
133-
Logc(ctx).WithFields(logFields).Debugf("Mountpoint found: %v", procMount)
134-
}
135-
136-
// If sourceDevice was specified and doesn't match proc mount, move on
137-
if sourceDevice != "" {
138-
139-
procSourceDevice := strings.TrimPrefix(procMount.Root, "/")
140-
141-
if strings.HasPrefix(procMount.MountSource, "/dev/") {
142-
procSourceDevice = strings.TrimPrefix(procMount.MountSource, "/dev/")
143-
if sourceDevice != procSourceDevice && devicePath != procSourceDevice {
144-
// Resolve any symlinks to get the real device, if device path has not already been found
145-
procSourceDevice, err = client.filepath.EvalSymlinks(procMount.MountSource)
146-
if err != nil {
147-
Logc(ctx).WithFields(logFields).WithError(err).Debug("Could not resolve device symlink")
148-
continue
149-
}
150-
procSourceDevice = strings.TrimPrefix(procSourceDevice, "/dev/")
151-
}
152-
}
153-
154-
if sourceDevice != procSourceDevice && devicePath != procSourceDevice {
155-
continue
156-
}
157-
158-
Logc(ctx).WithFields(logFields).Debugf("Device found: %v", sourceDevice)
159-
160-
if err = checkMountOptions(ctx, procMount, mountOptions); err != nil {
161-
Logc(ctx).WithFields(logFields).WithError(err).Warning("Checking mount options failed.")
162-
}
163-
164-
}
124+
if mountInfo == nil {
125+
Logc(ctx).WithFields(logFields).Debug("Mount information not found.")
126+
return false, nil
127+
}
165128

166-
Logc(ctx).WithFields(logFields).Debug("Mount information found.")
167-
return true, nil
129+
if err = checkMountOptions(ctx, mountInfo, mountOptions); err != nil {
130+
Logc(ctx).WithFields(logFields).WithError(err).Warning("Checking mount options failed.")
168131
}
169132

170-
Logc(ctx).WithFields(logFields).Debug("Mount information not found.")
171-
return false, nil
133+
Logc(ctx).WithFields(logFields).Debug("Mount information found.")
134+
return true, nil
172135
}
173136

174137
// PVMountpointMappings identifies devices corresponding to published paths
@@ -349,8 +312,9 @@ func (client *LinuxClient) MountDevice(ctx context.Context, device, mountpoint,
349312
}
350313

351314
if !mounted {
352-
if _, err := client.command.Execute(ctx, "mount", args...); err != nil {
353-
Logc(ctx).WithField("error", err).Error("Mount failed.")
315+
if out, err := client.command.Execute(ctx, "mount", args...); err != nil {
316+
Logc(ctx).WithError(fmt.Errorf("exit error: %v, mount command output: %s", err, string(out))).Error("Mount failed.")
317+
return fmt.Errorf("mount failed: %v, output: %s", err, out)
354318
}
355319
}
356320

@@ -406,6 +370,9 @@ func (client *LinuxClient) EnsureFileExists(ctx context.Context, path string) er
406370
func (client *LinuxClient) EnsureDirExists(ctx context.Context, path string) error {
407371
fields := LogFields{"path": path}
408372

373+
Logc(ctx).WithFields(fields).Debug(">>>> EnsureDirExists")
374+
defer Logc(ctx).WithFields(fields).Debug("<<<< EnsureDirExists")
375+
409376
if info, err := client.os.Stat(path); err == nil {
410377
if !info.IsDir() {
411378
Logc(ctx).WithFields(fields).Error("Path exists but is not a directory")
@@ -590,48 +557,18 @@ func parseProcMountInfo(content []byte) ([]models.MountInfo, error) {
590557
// The last split() item is empty string following the last \n
591558
continue
592559
}
593-
fields := strings.Fields(line)
594-
numFields := len(fields)
595-
if numFields < minNumProcSelfMntInfoFieldsPerLine {
596-
return nil, fmt.Errorf("wrong number of fields (expected at least %d, got %d): %s",
597-
minNumProcSelfMntInfoFieldsPerLine, numFields, line)
598-
}
599-
600-
// separator must be in the 4th position from the end for the line to contain fsType, mountSource, and
601-
// superOptions
602-
if fields[numFields-4] != "-" {
603-
return nil, fmt.Errorf("malformed mountinfo (could not find separator): %s", line)
604-
}
605-
606-
// If root value is marked deleted, skip the entry
607-
if strings.Contains(fields[3], "deleted") {
608-
continue
609-
}
610-
611-
mp := models.MountInfo{
612-
DeviceId: fields[2],
613-
Root: fields[3],
614-
MountPoint: fields[4],
615-
MountOptions: strings.Split(fields[5], ","),
616-
}
617560

618-
mountId, err := strconv.Atoi(fields[0])
561+
mp, err := parseProcMount(line)
619562
if err != nil {
620563
return nil, err
621564
}
622-
mp.MountId = mountId
623565

624-
parentId, err := strconv.Atoi(fields[1])
625-
if err != nil {
626-
return nil, err
566+
if mp == nil {
567+
// For the case, where err == nil && mp == nil, this can happen when root is marked as deleted.
568+
continue
627569
}
628-
mp.ParentId = parentId
629570

630-
mp.FsType = fields[numFields-3]
631-
mp.MountSource = fields[numFields-2]
632-
mp.SuperOptions = strings.Split(fields[numFields-1], ",")
633-
634-
out = append(out, mp)
571+
out = append(out, *mp)
635572
}
636573
return out, nil
637574
}
@@ -681,3 +618,194 @@ func parseProcMounts(content []byte) ([]models.MountPoint, error) {
681618
}
682619
return out, nil
683620
}
621+
622+
// ReadMountProcInfo tries the consistent read for the given mount information from the "/proc/self/mountinfo"
623+
func (client *LinuxClient) ReadMountProcInfo(ctx context.Context, mountpoint, sourceDevice, devicePath string) (*models.MountInfo, error) {
624+
return client.ConsistentReadMount(ctx, "/proc/self/mountinfo", mountpoint, sourceDevice, devicePath, maxListTries)
625+
}
626+
627+
// ConsistentReadMount verifies whether a specific mountpoint and source device consistently appear in the mount file.
628+
// It reads the file up to maxAttempts times, returning parsedEntry if the same matching entry is found in two consecutive reads.
629+
// Returns nil and an error if either the entry was present but not consistently found within the allowed attempts or there was an error.
630+
// Returns nil and nil if the entry was never found.
631+
func (client *LinuxClient) ConsistentReadMount(ctx context.Context, filename, mountpoint, sourceDevice, devicePath string, maxAttempts int) (*models.MountInfo, error) {
632+
logFields := LogFields{
633+
"filename": filename,
634+
"mountpoint": mountpoint,
635+
"maxAttempts": maxAttempts,
636+
}
637+
638+
Logc(ctx).WithFields(logFields).Debug(">>>> ConsistentReadMount")
639+
defer Logc(ctx).WithFields(logFields).Debug("<<<< ConsistentReadMount")
640+
641+
if maxAttempts < 1 {
642+
Logc(ctx).WithFields(logFields).Errorf("maxAttempts has to be equal or greater than 1, currently set to :%d", maxAttempts)
643+
return nil, fmt.Errorf("maxAttempts has to be equal or greater than 1, currently set to :%d", maxAttempts)
644+
}
645+
646+
var (
647+
previousMountEntry string
648+
entryFound = false
649+
)
650+
651+
// First iteration is to actually read the file for the first time.
652+
// And maxAttempts are made for the comparison thereafter.
653+
// Hence, +1
654+
for i := 0; i < (maxAttempts + 1); i++ {
655+
select {
656+
case <-ctx.Done():
657+
Logc(ctx).WithError(ctx.Err()).Error("Exiting early as the context has been cancelled or timed out.")
658+
return nil, ctx.Err()
659+
default:
660+
}
661+
662+
var (
663+
mountFound bool
664+
currentMount string
665+
parsedEntry *models.MountInfo
666+
)
667+
668+
content, err := client.os.ReadFile(filename)
669+
if err != nil {
670+
Logc(ctx).WithError(err).Error("Failed to read mount file.")
671+
return nil, err
672+
}
673+
674+
// Ranging over each entry.
675+
lines := strings.Split(string(content), "\n")
676+
for _, currentMount = range lines {
677+
if currentMount == "" {
678+
// The last split() item is empty string following the last \n
679+
continue
680+
}
681+
682+
// Parsing the line that we retrieved to models.MountInfo struct
683+
parsedEntry, err = parseProcMount(currentMount)
684+
if err != nil || parsedEntry == nil {
685+
// We'll keep continuing ranging over remaining lines.
686+
Logc(ctx).WithField("line", currentMount).WithError(err).Warn("Unable to parse the entry.")
687+
continue
688+
}
689+
690+
// Comparing the parsedEntry with the information that we have.
691+
mountFound, err = client.compareMount(ctx, mountpoint, sourceDevice, devicePath, parsedEntry)
692+
if err != nil {
693+
Logc(ctx).WithField("line", currentMount).WithError(err).Warn("Unable to compare the mount.")
694+
continue
695+
}
696+
697+
if mountFound {
698+
break
699+
}
700+
}
701+
702+
if mountFound {
703+
entryFound = true
704+
if previousMountEntry == currentMount {
705+
return parsedEntry, nil
706+
}
707+
}
708+
709+
// Changing previousMountEntry every iteration ensures, that when we're comparing,
710+
// it is between consecutive reads.
711+
previousMountEntry = currentMount
712+
}
713+
714+
if entryFound {
715+
return nil, fmt.Errorf("could not find consistent mount entry in %s for mountpoint %s after %d attempts", filename, mountpoint, maxAttempts)
716+
}
717+
718+
return nil, nil
719+
}
720+
721+
// parseProcMount parses single line/entry of the /proc/self/mountinfo file.
722+
func parseProcMount(line string) (*models.MountInfo, error) {
723+
// Ex: 26 29 0:5 / /dev rw,nosuid,relatime shared:2 - devtmpfs udev rw,size=4027860k,nr_inodes=1006965,mode=755,inode64
724+
725+
fields := strings.Fields(line)
726+
numFields := len(fields)
727+
if numFields < minNumProcSelfMntInfoFieldsPerLine {
728+
return nil, fmt.Errorf("wrong number of fields (expected at least %d, got %d): %s",
729+
minNumProcSelfMntInfoFieldsPerLine, numFields, line)
730+
}
731+
732+
// separator must be in the 4th position from the end for the line to contain fsType, mountSource, and
733+
// superOptions
734+
if fields[numFields-4] != "-" {
735+
return nil, fmt.Errorf("malformed mountinfo (could not find separator): %s", line)
736+
}
737+
738+
// If root value is marked deleted, skip the entry
739+
if strings.Contains(fields[3], "deleted") {
740+
return nil, nil
741+
}
742+
743+
mp := models.MountInfo{
744+
DeviceId: fields[2],
745+
Root: fields[3],
746+
MountPoint: fields[4],
747+
MountOptions: strings.Split(fields[5], ","),
748+
}
749+
750+
mountId, err := strconv.Atoi(fields[0])
751+
if err != nil {
752+
return nil, err
753+
}
754+
mp.MountId = mountId
755+
756+
parentId, err := strconv.Atoi(fields[1])
757+
if err != nil {
758+
return nil, err
759+
}
760+
mp.ParentId = parentId
761+
762+
mp.FsType = fields[numFields-3]
763+
mp.MountSource = fields[numFields-2]
764+
mp.SuperOptions = strings.Split(fields[numFields-1], ",")
765+
766+
return &mp, nil
767+
}
768+
769+
// compareMount compares the given mountpoint and sourceDevice with the retrieved entry from /proc/self/mountinfo
770+
func (client *LinuxClient) compareMount(ctx context.Context, mountpoint, sourceDevice, devicePath string, procMount *models.MountInfo) (bool, error) {
771+
logFields := LogFields{
772+
"mountPoint": mountpoint,
773+
"procMount": procMount,
774+
}
775+
776+
var err error
777+
778+
if mountpoint != "" {
779+
if !strings.Contains(procMount.MountPoint, mountpoint) {
780+
return false, nil
781+
}
782+
Logc(ctx).WithFields(logFields).Debugf("Mountpoint found: %v", procMount)
783+
}
784+
785+
// If sourceDevice was specified and doesn't match proc mount, move on
786+
if sourceDevice != "" {
787+
788+
procSourceDevice := strings.TrimPrefix(procMount.Root, "/")
789+
790+
if strings.HasPrefix(procMount.MountSource, "/dev/") {
791+
procSourceDevice = strings.TrimPrefix(procMount.MountSource, "/dev/")
792+
if sourceDevice != procSourceDevice && devicePath != procSourceDevice {
793+
// Resolve any symlinks to get the real device, if device path has not already been found
794+
procSourceDevice, err = client.filepath.EvalSymlinks(procMount.MountSource)
795+
if err != nil {
796+
Logc(ctx).WithFields(logFields).WithError(err).Debug("Could not resolve device symlink")
797+
return false, err
798+
}
799+
procSourceDevice = strings.TrimPrefix(procSourceDevice, "/dev/")
800+
}
801+
}
802+
803+
if sourceDevice != procSourceDevice && devicePath != procSourceDevice {
804+
return false, nil
805+
}
806+
807+
Logc(ctx).WithFields(logFields).Debugf("Device found: %v", sourceDevice)
808+
}
809+
810+
return true, nil
811+
}

0 commit comments

Comments
 (0)