001    /**
002     * Licensed to the Apache Software Foundation (ASF) under one
003     * or more contributor license agreements.  See the NOTICE file
004     * distributed with this work for additional information
005     * regarding copyright ownership.  The ASF licenses this file
006     * to you under the Apache License, Version 2.0 (the
007     * "License"); you may not use this file except in compliance
008     * with the License.  You may obtain a copy of the License at
009     *
010     *     http://www.apache.org/licenses/LICENSE-2.0
011     *
012     * Unless required by applicable law or agreed to in writing, software
013     * distributed under the License is distributed on an "AS IS" BASIS,
014     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015     * See the License for the specific language governing permissions and
016     * limitations under the License.
017     */
018    package org.apache.hadoop.io;
019    
020    import java.io.File;
021    import java.io.FileDescriptor;
022    import java.io.FileInputStream;
023    import java.io.FileOutputStream;
024    import java.io.IOException;
025    
026    import org.apache.hadoop.conf.Configuration;
027    import org.apache.hadoop.fs.FileStatus;
028    import org.apache.hadoop.fs.FileSystem;
029    import org.apache.hadoop.fs.Path;
030    import org.apache.hadoop.fs.permission.FsPermission;
031    import org.apache.hadoop.io.nativeio.Errno;
032    import org.apache.hadoop.io.nativeio.NativeIO;
033    import org.apache.hadoop.io.nativeio.NativeIOException;
034    import org.apache.hadoop.io.nativeio.NativeIO.Stat;
035    import org.apache.hadoop.security.UserGroupInformation;
036    
037    /**
038     * This class provides secure APIs for opening and creating files on the local
039     * disk. The main issue this class tries to handle is that of symlink traversal.
040     * <br/>
041     * An example of such an attack is:
042     * <ol>
043     * <li> Malicious user removes his task's syslog file, and puts a link to the
044     * jobToken file of a target user.</li>
045     * <li> Malicious user tries to open the syslog file via the servlet on the
046     * tasktracker.</li>
047     * <li> The tasktracker is unaware of the symlink, and simply streams the contents
048     * of the jobToken file. The malicious user can now access potentially sensitive
049     * map outputs, etc. of the target user's job.</li>
050     * </ol>
051     * A similar attack is possible involving task log truncation, but in that case
052     * due to an insecure write to a file.
053     * <br/>
054     */
055    public class SecureIOUtils {
056    
057      /**
058       * Ensure that we are set up to run with the appropriate native support code.
059       * If security is disabled, and the support code is unavailable, this class
060       * still tries its best to be secure, but is vulnerable to some race condition
061       * attacks.
062       *
063       * If security is enabled but the support code is unavailable, throws a
064       * RuntimeException since we don't want to run insecurely.
065       */
066      static {
067        boolean shouldBeSecure = UserGroupInformation.isSecurityEnabled();
068        boolean canBeSecure = NativeIO.isAvailable();
069    
070        if (!canBeSecure && shouldBeSecure) {
071          throw new RuntimeException(
072            "Secure IO is not possible without native code extensions.");
073        }
074    
075        // Pre-cache an instance of the raw FileSystem since we sometimes
076        // do secure IO in a shutdown hook, where this call could fail.
077        try {
078          rawFilesystem = FileSystem.getLocal(new Configuration()).getRaw();
079        } catch (IOException ie) {
080          throw new RuntimeException(
081          "Couldn't obtain an instance of RawLocalFileSystem.");
082        }
083    
084        // SecureIO just skips security checks in the case that security is
085        // disabled
086        skipSecurity = !canBeSecure;
087      }
088    
089      private final static boolean skipSecurity;
090      private final static FileSystem rawFilesystem;
091    
092      /**
093       * Open the given File for read access, verifying the expected user/group
094       * constraints if security is enabled.
095       *
096       * Note that this function provides no additional checks if Hadoop
097       * security is disabled, since doing the checks would be too expensive
098       * when native libraries are not available.
099       *
100       * @param f the file that we are trying to open
101       * @param expectedOwner the expected user owner for the file
102       * @param expectedGroup the expected group owner for the file
103       * @throws IOException if an IO Error occurred, or security is enabled and
104       * the user/group does not match
105       */
106      public static FileInputStream openForRead(File f, String expectedOwner, 
107          String expectedGroup) throws IOException {
108        if (!UserGroupInformation.isSecurityEnabled()) {
109          return new FileInputStream(f);
110        }
111        return forceSecureOpenForRead(f, expectedOwner, expectedGroup);
112      }
113    
114      /**
115       * Same as openForRead() except that it will run even if security is off.
116       * This is used by unit tests.
117       */
118      static FileInputStream forceSecureOpenForRead(File f, String expectedOwner,
119          String expectedGroup) throws IOException {
120    
121        FileInputStream fis = new FileInputStream(f);
122        boolean success = false;
123        try {
124          Stat stat = NativeIO.fstat(fis.getFD());
125          checkStat(f, stat.getOwner(), stat.getGroup(), expectedOwner,
126              expectedGroup);
127          success = true;
128          return fis;
129        } finally {
130          if (!success) {
131            fis.close();
132          }
133        }
134      }
135    
136      private static FileOutputStream insecureCreateForWrite(File f,
137          int permissions) throws IOException {
138        // If we can't do real security, do a racy exists check followed by an
139        // open and chmod
140        if (f.exists()) {
141          throw new AlreadyExistsException("File " + f + " already exists");
142        }
143        FileOutputStream fos = new FileOutputStream(f);
144        boolean success = false;
145        try {
146          rawFilesystem.setPermission(new Path(f.getAbsolutePath()),
147            new FsPermission((short)permissions));
148          success = true;
149          return fos;
150        } finally {
151          if (!success) {
152            fos.close();
153          }
154        }
155      }
156    
157      /**
158       * Open the specified File for write access, ensuring that it does not exist.
159       * @param f the file that we want to create
160       * @param permissions we want to have on the file (if security is enabled)
161       *
162       * @throws AlreadyExistsException if the file already exists
163       * @throws IOException if any other error occurred
164       */
165      public static FileOutputStream createForWrite(File f, int permissions)
166      throws IOException {
167        if (skipSecurity) {
168          return insecureCreateForWrite(f, permissions);
169        } else {
170          // Use the native wrapper around open(2)
171          try {
172            FileDescriptor fd = NativeIO.open(f.getAbsolutePath(),
173              NativeIO.O_WRONLY | NativeIO.O_CREAT | NativeIO.O_EXCL,
174              permissions);
175            return new FileOutputStream(fd);
176          } catch (NativeIOException nioe) {
177            if (nioe.getErrno() == Errno.EEXIST) {
178              throw new AlreadyExistsException(nioe);
179            }
180            throw nioe;
181          }
182        }
183      }
184    
185      private static void checkStat(File f, String owner, String group, 
186          String expectedOwner, 
187          String expectedGroup) throws IOException {
188        if (expectedOwner != null &&
189            !expectedOwner.equals(owner)) {
190          throw new IOException(
191            "Owner '" + owner + "' for path " + f + " did not match " +
192            "expected owner '" + expectedOwner + "'");
193        }
194        if (expectedGroup != null &&
195            !expectedGroup.equals(group)) {
196          throw new IOException(
197            "Group '" + group + "' for path " + f + " did not match " +
198            "expected group '" + expectedGroup + "'");
199        }
200      }
201    
202      /**
203       * Signals that an attempt to create a file at a given pathname has failed
204       * because another file already existed at that path.
205       */
206      public static class AlreadyExistsException extends IOException {
207        private static final long serialVersionUID = 1L;
208    
209        public AlreadyExistsException(String msg) {
210          super(msg);
211        }
212    
213        public AlreadyExistsException(Throwable cause) {
214          super(cause);
215        }
216      }
217    }