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